Aspectgg commited on
Commit
d1b7226
·
1 Parent(s): 8c81e76

UI REFINEMENT

Browse files
core/api_handlers.py CHANGED
@@ -201,8 +201,11 @@ def _build_followup_messages(
201
  {"role": "system", "content": system_prompt},
202
  ]
203
 
204
- # Replay conversation history as role turns (strip attack_tag metadata)
205
- for entry in history:
 
 
 
206
  role = entry.get("role", "user")
207
  content = entry.get("content", "")
208
  if role == "assistant":
@@ -795,6 +798,148 @@ def _confidence_from_fill(ctx: dict[str, str]) -> str:
795
  return "low"
796
 
797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
798
  def _structure_pitch_local_fallback(pitch_text: str) -> dict[str, Any]:
799
  """Heuristic extraction when Nemotron is unavailable."""
800
  text = pitch_text.strip()
@@ -874,34 +1019,29 @@ def _structure_pitch_local_fallback(pitch_text: str) -> dict[str, Any]:
874
  ctx["ask"] = sentence
875
  break
876
 
877
- missing = _missing_startup_fields(ctx)
878
  summary = " ".join(sentences[:2])[:220] if sentences else text[:220]
 
879
 
880
  return {
881
  "ok": True,
882
  "startup_context": ctx,
883
- "missing_fields": missing,
884
- "confidence": _confidence_from_fill(ctx),
 
 
885
  "brief_summary": summary,
886
  "source": "local_fallback",
887
  }
888
 
889
 
890
  def _parse_structure_pitch_response(raw: str) -> dict[str, Any] | None:
 
 
891
  parsed, _ = parse_model_json(raw)
892
  if not isinstance(parsed, dict):
893
  return None
894
 
895
  ctx = _normalize_startup_context(parsed.get("startup_context"))
896
- missing = parsed.get("missing_fields")
897
- if not isinstance(missing, list):
898
- missing = _missing_startup_fields(ctx)
899
- else:
900
- missing = [str(f).strip() for f in missing if str(f).strip() in _STARTUP_CONTEXT_FIELDS]
901
-
902
- confidence = str(parsed.get("confidence", "")).strip().lower()
903
- if confidence not in ("low", "medium", "high"):
904
- confidence = _confidence_from_fill(ctx)
905
 
906
  summary = str(parsed.get("brief_summary", "")).strip()
907
  if not summary:
@@ -909,8 +1049,6 @@ def _parse_structure_pitch_response(raw: str) -> dict[str, Any] | None:
909
 
910
  return {
911
  "startup_context": ctx,
912
- "missing_fields": missing,
913
- "confidence": confidence,
914
  "brief_summary": summary,
915
  }
916
 
@@ -942,11 +1080,16 @@ def handle_structure_pitch(payload: dict[str, Any]) -> dict[str, Any]:
942
  structured = _parse_structure_pitch_response(repair["content"])
943
 
944
  if structured is not None:
 
 
 
945
  return {
946
  "ok": True,
947
  "startup_context": structured["startup_context"],
948
- "missing_fields": structured["missing_fields"],
949
- "confidence": structured["confidence"],
 
 
950
  "brief_summary": structured["brief_summary"],
951
  "source": "nemotron",
952
  }
 
201
  {"role": "system", "content": system_prompt},
202
  ]
203
 
204
+ # Replay conversation history as role turns (strip attack_tag metadata).
205
+ # Cap at last 6 entries (3 full Q&A exchanges) so late-round input token
206
+ # growth never crowds out the output budget on the opponent mode call.
207
+ trimmed_history = history[-6:] if len(history) > 6 else history
208
+ for entry in trimmed_history:
209
  role = entry.get("role", "user")
210
  content = entry.get("content", "")
211
  if role == "assistant":
 
798
  return "low"
799
 
800
 
801
+ # ---------------------------------------------------------------------------
802
+ # Deterministic structure confidence (Part A of confidence-consistency fix)
803
+ # ---------------------------------------------------------------------------
804
+
805
+ _CONFIDENCE_FIELD_WEIGHTS: dict[str, int] = {
806
+ "name": 10,
807
+ "problem": 15,
808
+ "target_users": 12,
809
+ "solution": 15,
810
+ "why_ai": 10,
811
+ "traction": 15,
812
+ "competitors": 8,
813
+ "ask": 10,
814
+ }
815
+
816
+ # If any of these fields is absent the score cannot exceed its cap value.
817
+ _CONFIDENCE_CAPS: tuple[tuple[str, int], ...] = (
818
+ ("problem", 60),
819
+ ("solution", 60),
820
+ ("target_users", 70),
821
+ ("traction", 74), # cap below 75 so missing traction → at most medium
822
+ ("competitors", 92), # missing competitors → visible gap from 100; still high
823
+ ("why_ai", 90), # missing/nonsense why_ai → max 90; one-word answers caught by min-words
824
+ ("ask", 85),
825
+ )
826
+
827
+ _FILLER_VALUES = frozenset({
828
+ "not specified", "n/a", "none", "unknown", "tbd", "-", "",
829
+ "idk", "i don't know", "i dont know", "not sure", "na", "no idea",
830
+ "dunno", "nothing", "?", "??", "???", "yes", "no", "nope", "yep",
831
+ "to be determined", "to be decided", "will update", "coming soon",
832
+ })
833
+
834
+ # Minimum real-word count for description fields — rejects single-word noise like "idk", "yes", "dunno".
835
+ # Name, competitors, ask intentionally use min=1 (a single-word name or ask is valid).
836
+ _CONF_MIN_WORDS: dict[str, int] = {
837
+ "problem": 2, "solution": 2, "why_ai": 2, "traction": 2, "target_users": 2,
838
+ }
839
+
840
+ _CONFIDENCE_USER_SEG_RE = re.compile(
841
+ r"\b(college students?|university students?|indie developers?|small businesses?|"
842
+ r"enterprise|founders?|educators?|teachers?|researchers?|professionals?|"
843
+ r"teams?|parents?|teenagers?|consumers?|startup founders?)\b",
844
+ re.IGNORECASE,
845
+ )
846
+ _CONFIDENCE_CONCRETE_ASK_RE = re.compile(
847
+ r"(\$[\d,]+[kKmM]?|\d+[kK]\s*(?:usd|dollars?)?|mentorship|campus pilot|"
848
+ r"equity partner|co.?founder|sponsorship|strategic partner)",
849
+ re.IGNORECASE,
850
+ )
851
+
852
+
853
+ def _field_is_filled(field: str, val: str) -> bool:
854
+ """Return True when val contains genuine, substantive content.
855
+
856
+ Two checks:
857
+ 1. Not a known filler phrase ("idk", "n/a", "not specified", …)
858
+ 2. At least _CONF_MIN_WORDS[field] real words — blocks single-word noise
859
+ on description fields while allowing one-word names / ask phrases.
860
+ """
861
+ clean = str(val or "").strip()
862
+ if clean.lower() in _FILLER_VALUES:
863
+ return False
864
+ min_w = _CONF_MIN_WORDS.get(field, 1)
865
+ return len(clean.split()) >= min_w
866
+
867
+
868
+ def calculate_structure_confidence(
869
+ startup_context: dict,
870
+ raw_pitch_text: str = "",
871
+ ) -> dict[str, Any]:
872
+ """Deterministic confidence score from field completeness + raw-text evidence.
873
+
874
+ For the same startup_context and raw_pitch_text the result is always identical —
875
+ no randomness, no model opinion.
876
+ """
877
+ ctx = startup_context or {}
878
+ text = str(raw_pitch_text or "").strip()
879
+ reasons: list[str] = []
880
+
881
+ # --- Field completeness ---
882
+ score = 0
883
+ filled: list[str] = []
884
+ missing: list[str] = []
885
+ for field, weight in _CONFIDENCE_FIELD_WEIGHTS.items():
886
+ if _field_is_filled(field, str(ctx.get(field, "") or "")):
887
+ score += weight
888
+ filled.append(field)
889
+ else:
890
+ missing.append(field)
891
+
892
+ # --- Signal bonus from raw pitch text (pure regex — deterministic) ---
893
+ bonus = 0
894
+ number_hits = re.findall(r"\b\d[\d,]*\b", text)
895
+ if len(number_hits) >= 3:
896
+ bonus += 10
897
+ elif number_hits:
898
+ bonus += 5
899
+
900
+ if _CONFIDENCE_USER_SEG_RE.search(text):
901
+ bonus += 5
902
+
903
+ if _CONFIDENCE_CONCRETE_ASK_RE.search(text):
904
+ bonus += 5
905
+
906
+ bonus = min(bonus, 20)
907
+ score = min(score + bonus, 100)
908
+
909
+ # --- Apply caps for critical missing fields ---
910
+ for field, cap in _CONFIDENCE_CAPS:
911
+ if field in missing:
912
+ score = min(score, cap)
913
+
914
+ score = max(0, min(100, score))
915
+
916
+ # --- Label ---
917
+ if score >= 75:
918
+ label = "high"
919
+ elif score >= 45:
920
+ label = "medium"
921
+ else:
922
+ label = "low"
923
+
924
+ # --- Human-readable reasons ---
925
+ strong = [f for f in ("problem", "solution", "target_users", "traction") if f in filled]
926
+ if strong:
927
+ reasons.append(f"Strong signals: {', '.join(strong)}")
928
+ if bonus >= 10:
929
+ reasons.append("Concrete numbers detected")
930
+ elif bonus >= 5:
931
+ reasons.append("Some evidence detected")
932
+ if missing:
933
+ reasons.append(f"Not in pitch: {', '.join(missing)}")
934
+
935
+ return {
936
+ "confidence": label,
937
+ "confidence_score": score,
938
+ "confidence_reasons": reasons,
939
+ "missing_fields": missing,
940
+ }
941
+
942
+
943
  def _structure_pitch_local_fallback(pitch_text: str) -> dict[str, Any]:
944
  """Heuristic extraction when Nemotron is unavailable."""
945
  text = pitch_text.strip()
 
1019
  ctx["ask"] = sentence
1020
  break
1021
 
 
1022
  summary = " ".join(sentences[:2])[:220] if sentences else text[:220]
1023
+ conf = calculate_structure_confidence(ctx, pitch_text)
1024
 
1025
  return {
1026
  "ok": True,
1027
  "startup_context": ctx,
1028
+ "missing_fields": conf["missing_fields"],
1029
+ "confidence": conf["confidence"],
1030
+ "confidence_score": conf["confidence_score"],
1031
+ "confidence_reasons": conf["confidence_reasons"],
1032
  "brief_summary": summary,
1033
  "source": "local_fallback",
1034
  }
1035
 
1036
 
1037
  def _parse_structure_pitch_response(raw: str) -> dict[str, Any] | None:
1038
+ """Parse Nemotron extraction output. Confidence is NOT taken from the model —
1039
+ it is calculated deterministically by the caller via calculate_structure_confidence."""
1040
  parsed, _ = parse_model_json(raw)
1041
  if not isinstance(parsed, dict):
1042
  return None
1043
 
1044
  ctx = _normalize_startup_context(parsed.get("startup_context"))
 
 
 
 
 
 
 
 
 
1045
 
1046
  summary = str(parsed.get("brief_summary", "")).strip()
1047
  if not summary:
 
1049
 
1050
  return {
1051
  "startup_context": ctx,
 
 
1052
  "brief_summary": summary,
1053
  }
1054
 
 
1080
  structured = _parse_structure_pitch_response(repair["content"])
1081
 
1082
  if structured is not None:
1083
+ conf = calculate_structure_confidence(
1084
+ structured["startup_context"], pitch_text
1085
+ )
1086
  return {
1087
  "ok": True,
1088
  "startup_context": structured["startup_context"],
1089
+ "missing_fields": conf["missing_fields"],
1090
+ "confidence": conf["confidence"],
1091
+ "confidence_score": conf["confidence_score"],
1092
+ "confidence_reasons": conf["confidence_reasons"],
1093
  "brief_summary": structured["brief_summary"],
1094
  "source": "nemotron",
1095
  }
core/nvidia_client.py CHANGED
@@ -50,66 +50,78 @@ class OmniAudioError(RuntimeError):
50
  # rewrite — rewrite utility (thinking on, lighter budget)
51
  # legacy_full_scorecard — diagnostic / legacy path only; not main path (thinking off)
52
  _TASK_DEFAULTS: dict[str, dict[str, Any]] = {
 
 
53
  "opponent": {
54
  "enable_thinking": True,
55
- "reasoning_budget": 512,
56
- "max_tokens": 900,
57
  "temperature": 0.65,
58
  "top_p": 0.95,
59
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  "scorecard_scoring": {
61
  "enable_thinking": False,
62
  "reasoning_budget": 0,
63
- "max_tokens": 1700,
64
  "temperature": 0.1,
65
  "top_p": 0.95,
66
  },
67
  "scorecard_scoring_repair": {
68
  "enable_thinking": False,
69
  "reasoning_budget": 0,
70
- "max_tokens": 1400,
71
  "temperature": 0.0,
72
  "top_p": 0.95,
73
  },
74
  "scorecard_full": {
75
  "enable_thinking": False,
76
  "reasoning_budget": 0,
77
- "max_tokens": 2500,
78
  "temperature": 0.1,
79
  "top_p": 0.95,
80
  },
81
  "scorecard_full_repair": {
82
  "enable_thinking": False,
83
  "reasoning_budget": 0,
84
- "max_tokens": 2200,
85
  "temperature": 0.0,
86
  "top_p": 0.95,
87
  },
88
  "scorecard_coaching": {
89
  "enable_thinking": False,
90
  "reasoning_budget": 0,
91
- "max_tokens": 2400,
92
  "temperature": 0.2,
93
  "top_p": 0.95,
94
  },
95
  "scorecard_coaching_repair": {
96
  "enable_thinking": False,
97
  "reasoning_budget": 0,
98
- "max_tokens": 1600,
99
  "temperature": 0.0,
100
  "top_p": 0.95,
101
  },
102
- "rewrite": {
103
- "enable_thinking": True,
104
- "reasoning_budget": 256,
105
- "max_tokens": 900,
106
- "temperature": 0.45,
107
- "top_p": 0.95,
108
- },
109
  "legacy_full_scorecard": {
110
  "enable_thinking": False,
111
  "reasoning_budget": 0,
112
- "max_tokens": 3000,
113
  "temperature": 0.1,
114
  "top_p": 0.95,
115
  },
@@ -123,71 +135,64 @@ _TASK_DEFAULTS: dict[str, dict[str, Any]] = {
123
  "voice_extraction_repair": {
124
  "enable_thinking": False,
125
  "reasoning_budget": 0,
126
- "max_tokens": 1200,
127
  "temperature": 0.0,
128
  "top_p": 0.95,
129
  },
130
  "voice_turn": {
131
  "enable_thinking": False,
132
  "reasoning_budget": 0,
133
- "max_tokens": 700,
134
  "temperature": 0.0,
135
  "top_p": 0.95,
136
  },
137
  "voice_turn_repair": {
138
  "enable_thinking": False,
139
  "reasoning_budget": 0,
140
- "max_tokens": 600,
141
  "temperature": 0.0,
142
  "top_p": 0.95,
143
  },
144
  "retry_comparison": {
145
  "enable_thinking": False,
146
  "reasoning_budget": 0,
147
- "max_tokens": 1000,
148
  "temperature": 0.15,
149
  "top_p": 0.95,
150
  },
151
  "retry_comparison_repair": {
152
  "enable_thinking": False,
153
  "reasoning_budget": 0,
154
- "max_tokens": 800,
155
  "temperature": 0.0,
156
  "top_p": 0.95,
157
  },
158
  "deal_verdict": {
159
  "enable_thinking": False,
160
  "reasoning_budget": 0,
161
- "max_tokens": 1000,
162
  "temperature": 0.2,
163
  "top_p": 0.95,
164
  },
165
  "deal_verdict_repair": {
166
  "enable_thinking": False,
167
  "reasoning_budget": 0,
168
- "max_tokens": 800,
169
  "temperature": 0.0,
170
  "top_p": 0.95,
171
  },
172
- "deal_round": {
173
- "enable_thinking": True,
174
- "reasoning_budget": 512,
175
- "max_tokens": 900,
176
- "temperature": 0.65,
177
- "top_p": 0.95,
178
- },
179
  # Deal phase: semantic dimension scoring (JSON, split call 1 — scores only)
180
  "deal_scorecard_scoring": {
181
  "enable_thinking": False,
182
  "reasoning_budget": 0,
183
- "max_tokens": 1700,
184
  "temperature": 0.1,
185
  "top_p": 0.95,
186
  },
187
  "deal_scorecard_scoring_repair": {
188
  "enable_thinking": False,
189
  "reasoning_budget": 0,
190
- "max_tokens": 1300,
191
  "temperature": 0.0,
192
  "top_p": 0.95,
193
  },
@@ -195,28 +200,28 @@ _TASK_DEFAULTS: dict[str, dict[str, Any]] = {
195
  "deal_scorecard_coaching": {
196
  "enable_thinking": False,
197
  "reasoning_budget": 0,
198
- "max_tokens": 2200,
199
  "temperature": 0.2,
200
  "top_p": 0.95,
201
  },
202
  "deal_scorecard_repair": {
203
  "enable_thinking": False,
204
  "reasoning_budget": 0,
205
- "max_tokens": 1200,
206
  "temperature": 0.0,
207
  "top_p": 0.95,
208
  },
209
  "structure_pitch": {
210
  "enable_thinking": False,
211
  "reasoning_budget": 0,
212
- "max_tokens": 900,
213
- "temperature": 0.1,
214
  "top_p": 0.95,
215
  },
216
  "structure_pitch_repair": {
217
  "enable_thinking": False,
218
  "reasoning_budget": 0,
219
- "max_tokens": 800,
220
  "temperature": 0.0,
221
  "top_p": 0.95,
222
  },
@@ -265,6 +270,66 @@ def _extract_json_from_reasoning(reasoning: str) -> str | None:
265
  return None
266
 
267
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  def _get_config() -> tuple[str, str, str]:
269
  """Return (api_key, base_url, model). Raises RuntimeError if key is absent."""
270
  api_key = os.getenv("NVIDIA_API_KEY", "").strip()
@@ -357,6 +422,7 @@ def _complete_chat(
357
 
358
  if not content:
359
  if mode in _JSON_MODES and reasoning:
 
360
  extracted = _extract_json_from_reasoning(reasoning)
361
  if extracted:
362
  logger.info(
@@ -366,21 +432,31 @@ def _complete_chat(
366
  content = extracted
367
  else:
368
  logger.warning(
369
- "Nemotron content empty; checked reasoning_content fallback (mode=%s, no JSON found)",
370
  mode,
371
  )
372
- elif reasoning:
 
 
 
 
373
  logger.warning(
374
- "Nemotron content empty; checked reasoning_content fallback (mode=%s)",
 
375
  mode,
376
  )
377
- content = reasoning
378
 
379
  if not content:
380
  raise RuntimeError(
381
- "NVIDIA model returned an empty response. "
382
- "The reasoning model may need a larger max_tokens budget."
383
  )
 
 
 
 
 
 
384
  return content
385
 
386
 
 
50
  # rewrite — rewrite utility (thinking on, lighter budget)
51
  # legacy_full_scorecard — diagnostic / legacy path only; not main path (thinking off)
52
  _TASK_DEFAULTS: dict[str, dict[str, Any]] = {
53
+ # Thinking modes: reasoning_budget=320 — enough room to think cleanly without
54
+ # spilling monologue into content, while leaving 1180 tokens for real output.
55
  "opponent": {
56
  "enable_thinking": True,
57
+ "reasoning_budget": 320,
58
+ "max_tokens": 1500,
59
  "temperature": 0.65,
60
  "top_p": 0.95,
61
  },
62
+ "deal_round": {
63
+ "enable_thinking": True,
64
+ "reasoning_budget": 320,
65
+ "max_tokens": 1500,
66
+ "temperature": 0.65,
67
+ "top_p": 0.95,
68
+ },
69
+ "rewrite": {
70
+ "enable_thinking": True,
71
+ "reasoning_budget": 320,
72
+ "max_tokens": 1200,
73
+ "temperature": 0.45,
74
+ "top_p": 0.95,
75
+ },
76
+ # Scoring modes (thinking off — pure JSON output).
77
+ # max_tokens raised well above realistic output size so late-round battles
78
+ # with long conversation history never truncate mid-JSON.
79
  "scorecard_scoring": {
80
  "enable_thinking": False,
81
  "reasoning_budget": 0,
82
+ "max_tokens": 2500,
83
  "temperature": 0.1,
84
  "top_p": 0.95,
85
  },
86
  "scorecard_scoring_repair": {
87
  "enable_thinking": False,
88
  "reasoning_budget": 0,
89
+ "max_tokens": 2000,
90
  "temperature": 0.0,
91
  "top_p": 0.95,
92
  },
93
  "scorecard_full": {
94
  "enable_thinking": False,
95
  "reasoning_budget": 0,
96
+ "max_tokens": 3500,
97
  "temperature": 0.1,
98
  "top_p": 0.95,
99
  },
100
  "scorecard_full_repair": {
101
  "enable_thinking": False,
102
  "reasoning_budget": 0,
103
+ "max_tokens": 3000,
104
  "temperature": 0.0,
105
  "top_p": 0.95,
106
  },
107
  "scorecard_coaching": {
108
  "enable_thinking": False,
109
  "reasoning_budget": 0,
110
+ "max_tokens": 3200,
111
  "temperature": 0.2,
112
  "top_p": 0.95,
113
  },
114
  "scorecard_coaching_repair": {
115
  "enable_thinking": False,
116
  "reasoning_budget": 0,
117
+ "max_tokens": 2400,
118
  "temperature": 0.0,
119
  "top_p": 0.95,
120
  },
 
 
 
 
 
 
 
121
  "legacy_full_scorecard": {
122
  "enable_thinking": False,
123
  "reasoning_budget": 0,
124
+ "max_tokens": 4000,
125
  "temperature": 0.1,
126
  "top_p": 0.95,
127
  },
 
135
  "voice_extraction_repair": {
136
  "enable_thinking": False,
137
  "reasoning_budget": 0,
138
+ "max_tokens": 1400,
139
  "temperature": 0.0,
140
  "top_p": 0.95,
141
  },
142
  "voice_turn": {
143
  "enable_thinking": False,
144
  "reasoning_budget": 0,
145
+ "max_tokens": 800,
146
  "temperature": 0.0,
147
  "top_p": 0.95,
148
  },
149
  "voice_turn_repair": {
150
  "enable_thinking": False,
151
  "reasoning_budget": 0,
152
+ "max_tokens": 700,
153
  "temperature": 0.0,
154
  "top_p": 0.95,
155
  },
156
  "retry_comparison": {
157
  "enable_thinking": False,
158
  "reasoning_budget": 0,
159
+ "max_tokens": 1500,
160
  "temperature": 0.15,
161
  "top_p": 0.95,
162
  },
163
  "retry_comparison_repair": {
164
  "enable_thinking": False,
165
  "reasoning_budget": 0,
166
+ "max_tokens": 1200,
167
  "temperature": 0.0,
168
  "top_p": 0.95,
169
  },
170
  "deal_verdict": {
171
  "enable_thinking": False,
172
  "reasoning_budget": 0,
173
+ "max_tokens": 1500,
174
  "temperature": 0.2,
175
  "top_p": 0.95,
176
  },
177
  "deal_verdict_repair": {
178
  "enable_thinking": False,
179
  "reasoning_budget": 0,
180
+ "max_tokens": 1200,
181
  "temperature": 0.0,
182
  "top_p": 0.95,
183
  },
 
 
 
 
 
 
 
184
  # Deal phase: semantic dimension scoring (JSON, split call 1 — scores only)
185
  "deal_scorecard_scoring": {
186
  "enable_thinking": False,
187
  "reasoning_budget": 0,
188
+ "max_tokens": 2500,
189
  "temperature": 0.1,
190
  "top_p": 0.95,
191
  },
192
  "deal_scorecard_scoring_repair": {
193
  "enable_thinking": False,
194
  "reasoning_budget": 0,
195
+ "max_tokens": 2000,
196
  "temperature": 0.0,
197
  "top_p": 0.95,
198
  },
 
200
  "deal_scorecard_coaching": {
201
  "enable_thinking": False,
202
  "reasoning_budget": 0,
203
+ "max_tokens": 3000,
204
  "temperature": 0.2,
205
  "top_p": 0.95,
206
  },
207
  "deal_scorecard_repair": {
208
  "enable_thinking": False,
209
  "reasoning_budget": 0,
210
+ "max_tokens": 2000,
211
  "temperature": 0.0,
212
  "top_p": 0.95,
213
  },
214
  "structure_pitch": {
215
  "enable_thinking": False,
216
  "reasoning_budget": 0,
217
+ "max_tokens": 1000,
218
+ "temperature": 0.0,
219
  "top_p": 0.95,
220
  },
221
  "structure_pitch_repair": {
222
  "enable_thinking": False,
223
  "reasoning_budget": 0,
224
+ "max_tokens": 900,
225
  "temperature": 0.0,
226
  "top_p": 0.95,
227
  },
 
270
  return None
271
 
272
 
273
+ # Phrases that only appear in internal reasoning monologue, never in a real response.
274
+ _REASONING_LEAK_SIGNALS = (
275
+ "need to keep under",
276
+ "check constraints",
277
+ "that's one sentence",
278
+ "actually it's",
279
+ "make sure we reference",
280
+ "under 3 sentences",
281
+ "the question itself",
282
+ "so okay.",
283
+ "let me ",
284
+ "i need to ",
285
+ "i should ",
286
+ "i'll ask",
287
+ "i will ask",
288
+ "one question,",
289
+ "plain language.",
290
+ "no advice,",
291
+ "no compliments,",
292
+ )
293
+
294
+
295
+ def _strip_reasoning_leak(content: str) -> str:
296
+ """Remove internal monologue that leaked into content for thinking-mode calls.
297
+
298
+ When reasoning_budget is insufficient the model continues "thinking" inside
299
+ the content field before reaching the actual response. This function detects
300
+ that pattern and returns only the final intended output (the last question).
301
+ """
302
+ lower = content.lower()
303
+ if not any(sig in lower for sig in _REASONING_LEAK_SIGNALS):
304
+ return content
305
+
306
+ # Reasoning leaked — extract the last real sentence(s).
307
+ # Split on sentence boundaries and walk backwards to find the last
308
+ # question, which is the intended output.
309
+ import re
310
+ sentences = [s.strip() for s in re.split(r"(?<=[.!?])\s+", content) if s.strip()]
311
+ if not sentences:
312
+ return content
313
+
314
+ # Find the last sentence ending with "?"
315
+ for i in range(len(sentences) - 1, -1, -1):
316
+ if sentences[i].endswith("?"):
317
+ # Include the sentence before it (context) if it's clean prose
318
+ if i > 0 and not any(sig in sentences[i - 1].lower() for sig in _REASONING_LEAK_SIGNALS):
319
+ return f"{sentences[i - 1]} {sentences[i]}"
320
+ return sentences[i]
321
+
322
+ # No question mark found — return the last non-reasoning sentence
323
+ for sent in reversed(sentences):
324
+ if not any(sig in sent.lower() for sig in _REASONING_LEAK_SIGNALS):
325
+ return sent
326
+
327
+ # Couldn't isolate anything clean — raise so the caller serves its fallback
328
+ # message instead of showing garbage to the user.
329
+ logger.warning("_strip_reasoning_leak: could not isolate clean output, raising for caller fallback")
330
+ raise RuntimeError("Reasoning leaked into content and could not be cleaned (mode content fully contaminated)")
331
+
332
+
333
  def _get_config() -> tuple[str, str, str]:
334
  """Return (api_key, base_url, model). Raises RuntimeError if key is absent."""
335
  api_key = os.getenv("NVIDIA_API_KEY", "").strip()
 
422
 
423
  if not content:
424
  if mode in _JSON_MODES and reasoning:
425
+ # JSON mode: try to salvage a JSON block from the reasoning field.
426
  extracted = _extract_json_from_reasoning(reasoning)
427
  if extracted:
428
  logger.info(
 
432
  content = extracted
433
  else:
434
  logger.warning(
435
+ "Nemotron content empty; no JSON in reasoning_content (mode=%s)",
436
  mode,
437
  )
438
+ else:
439
+ # Non-JSON mode (opponent, deal_round, rewrite, …):
440
+ # reasoning_content is the model's compressed internal thinking —
441
+ # it is NEVER safe to display. Let it raise so the caller serves
442
+ # its own clean fallback message.
443
  logger.warning(
444
+ "Nemotron content empty for non-JSON mode=%s; "
445
+ "reasoning_content not usable as output — raising for caller fallback",
446
  mode,
447
  )
 
448
 
449
  if not content:
450
  raise RuntimeError(
451
+ f"Nemotron returned empty content (mode={mode}). "
452
+ "Caller should serve its fallback."
453
  )
454
+
455
+ # For thinking-mode calls (opponent, deal_round, rewrite) strip any internal
456
+ # monologue that leaked into the content field when reasoning_budget runs short.
457
+ if enable_thinking and mode not in _JSON_MODES:
458
+ content = _strip_reasoning_leak(content)
459
+
460
  return content
461
 
462
 
core/retry_handler.py CHANGED
@@ -128,6 +128,19 @@ def start_retry_drill(session: dict) -> dict[str, Any]:
128
  )
129
  difficulty_label = session.get("difficulty_label") or get_label(difficulty_profile)
130
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  retry_id = str(uuid.uuid4())
132
  drill = {
133
  "retry_id": retry_id,
@@ -143,7 +156,10 @@ def start_retry_drill(session: dict) -> dict[str, Any]:
143
  "input_mode": "",
144
  "retry_answer": "",
145
  "result": {},
146
- "dimension_score_before": _dimension_score(scorecard, dimension),
 
 
 
147
  }
148
  session.setdefault("retry_drills", {})[retry_id] = drill
149
 
@@ -380,6 +396,90 @@ def call_nemotron_retry_comparison(
380
  return None
381
 
382
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  def apply_retry_to_scorecard(
384
  session: dict,
385
  drill: dict,
@@ -486,18 +586,26 @@ def evaluate_retry_answer(
486
  if voice_turn_id:
487
  drill["voice_turn_id"] = voice_turn_id
488
 
489
- comparison_result = call_nemotron_retry_comparison(session, drill, answer)
490
- if comparison_result is None:
 
 
 
 
 
491
  comparison_result = build_local_retry_fallback(
492
  drill.get("original_answer", ""),
493
  answer,
494
  drill.get("dimension", "objection_handling"),
495
  drill.get("dimension_score_before", 30),
496
  )
 
 
 
497
 
498
  drill["result"] = comparison_result
499
  comp = comparison_result.get("comparison", {})
500
- updated_scorecard = apply_retry_to_scorecard(session, drill, comp)
501
 
502
  response: dict[str, Any] = {
503
  "session_id": session_id,
@@ -509,14 +617,11 @@ def evaluate_retry_answer(
509
  "original_answer": drill.get("original_answer", ""),
510
  "retry_answer": answer,
511
  "comparison": comp,
 
512
  "next_practice_prompt": comparison_result.get("next_practice_prompt", ""),
 
 
 
 
513
  }
514
- if updated_scorecard:
515
- response["updated_scorecard"] = updated_scorecard
516
- try:
517
- verdict = build_judge_verdict(session, updated_scorecard, local_only=True)
518
- session["judge_verdict"] = verdict
519
- response["judge_verdict"] = verdict
520
- except Exception as exc:
521
- logger.warning("retry_handler: could not refresh judge verdict — %s", exc)
522
  return response
 
128
  )
129
  difficulty_label = session.get("difficulty_label") or get_label(difficulty_profile)
130
 
131
+ # Snapshot the scorecard baseline at drill-creation time so that any later
132
+ # scorecard mutation (or session reload) cannot shift the projection baseline.
133
+ sc_scores = scorecard.get("scores") or {}
134
+ original_overall_score = int(scorecard.get("overall", 0) or 0)
135
+ original_dimension_scores = {
136
+ k: int(v.get("score", 0) or 0)
137
+ for k, v in sc_scores.items()
138
+ if isinstance(v, dict)
139
+ }
140
+ dim_score_before = original_dimension_scores.get(
141
+ dimension, _dimension_score(scorecard, dimension)
142
+ )
143
+
144
  retry_id = str(uuid.uuid4())
145
  drill = {
146
  "retry_id": retry_id,
 
156
  "input_mode": "",
157
  "retry_answer": "",
158
  "result": {},
159
+ "dimension_score_before": dim_score_before,
160
+ # Authoritative baseline — never re-read from session after this point.
161
+ "original_overall_score": original_overall_score,
162
+ "original_dimension_scores": original_dimension_scores,
163
  }
164
  session.setdefault("retry_drills", {})[retry_id] = drill
165
 
 
396
  return None
397
 
398
 
399
+ def compute_retry_projection(
400
+ session: dict,
401
+ drill: dict,
402
+ comparison: dict,
403
+ ) -> dict[str, Any]:
404
+ """Non-destructive training projection — original scorecard stays unchanged.
405
+
406
+ Uses the baseline snapshotted onto the drill at start_retry_drill time so that
407
+ any scorecard mutation between drill-start and drill-submit cannot corrupt the
408
+ displayed baseline (the bug was: practice-nudge stripped by a later resync left
409
+ scorecard["overall"]=28 while the UI showed 31 from the original API response).
410
+ """
411
+ scorecard = session.get("latest_scorecard") or {}
412
+ dim = str(drill.get("dimension", "")).strip()
413
+
414
+ # --- Authoritative baseline: prefer drill snapshot, fall back to live session ---
415
+ original_overall = int(
416
+ drill.get("original_overall_score")
417
+ if drill.get("original_overall_score") is not None
418
+ else (scorecard.get("overall", 0) or 0)
419
+ )
420
+
421
+ # Use snapshotted dimension scores; fall back to live scorecard scores.
422
+ original_dim_scores: dict[str, int] = drill.get("original_dimension_scores") or {}
423
+ if not original_dim_scores:
424
+ scores = scorecard.get("scores") or {}
425
+ original_dim_scores = {
426
+ k: int(v.get("score", 0) or 0)
427
+ for k, v in scores.items() if isinstance(v, dict)
428
+ }
429
+
430
+ # --- Old dimension score for this specific target ---
431
+ old_dim_score = int(
432
+ original_dim_scores.get(
433
+ dim,
434
+ drill.get("dimension_score_before", 0) or 0,
435
+ )
436
+ )
437
+
438
+ # --- New dimension score from Nemotron/fallback comparison ---
439
+ try:
440
+ raw_new = int(comparison.get("estimated_dimension_after", old_dim_score))
441
+ except (TypeError, ValueError):
442
+ raw_new = old_dim_score
443
+
444
+ # Never allow the new score to appear lower than the old score in the projection.
445
+ new_dim_score = max(old_dim_score, raw_new)
446
+ dimension_delta = new_dim_score - old_dim_score
447
+
448
+ if dimension_delta > 0:
449
+ # Replace only the target dimension; all others stay at their original values.
450
+ projected_scores = dict(original_dim_scores)
451
+ projected_scores[dim] = new_dim_score
452
+
453
+ n_dims = len(projected_scores) or 1
454
+ dim_avg_projection = round(sum(projected_scores.values()) / n_dims)
455
+
456
+ # Proportional lift ensures even a single-dim improvement is visible when
457
+ # the raw average is still dragged down by other weak dims.
458
+ proportional_lift = max(1, round(dimension_delta / n_dims))
459
+
460
+ projected_overall = max(
461
+ dim_avg_projection,
462
+ original_overall,
463
+ min(100, original_overall + proportional_lift),
464
+ )
465
+ projected_overall_delta = max(0, projected_overall - original_overall)
466
+ else:
467
+ projected_overall = original_overall
468
+ projected_overall_delta = 0
469
+
470
+ return {
471
+ "target_dimension": dim,
472
+ "old_dimension_score": old_dim_score,
473
+ "new_dimension_score": new_dim_score,
474
+ "dimension_delta": dimension_delta,
475
+ "original_overall_score": original_overall,
476
+ "projected_overall_score": projected_overall,
477
+ "projected_overall_delta": projected_overall_delta,
478
+ "original_scorecard_unchanged": True,
479
+ "projection_method": "replace_target_dimension_only",
480
+ }
481
+
482
+
483
  def apply_retry_to_scorecard(
484
  session: dict,
485
  drill: dict,
 
586
  if voice_turn_id:
587
  drill["voice_turn_id"] = voice_turn_id
588
 
589
+ nemotron_result = call_nemotron_retry_comparison(session, drill, answer)
590
+ if nemotron_result is not None:
591
+ comparison_result = nemotron_result
592
+ retry_score_source = "nemotron"
593
+ model_ok = True
594
+ fallback_reason = ""
595
+ else:
596
  comparison_result = build_local_retry_fallback(
597
  drill.get("original_answer", ""),
598
  answer,
599
  drill.get("dimension", "objection_handling"),
600
  drill.get("dimension_score_before", 30),
601
  )
602
+ retry_score_source = "local_fallback"
603
+ model_ok = False
604
+ fallback_reason = "Nemotron unavailable — local heuristic used"
605
 
606
  drill["result"] = comparison_result
607
  comp = comparison_result.get("comparison", {})
608
+ projection = compute_retry_projection(session, drill, comp)
609
 
610
  response: dict[str, Any] = {
611
  "session_id": session_id,
 
617
  "original_answer": drill.get("original_answer", ""),
618
  "retry_answer": answer,
619
  "comparison": comp,
620
+ "projection": projection,
621
  "next_practice_prompt": comparison_result.get("next_practice_prompt", ""),
622
+ "scorecard_unchanged": True,
623
+ "retry_score_source": retry_score_source,
624
+ "model_ok": model_ok,
625
+ "fallback_reason": fallback_reason,
626
  }
 
 
 
 
 
 
 
 
627
  return response
core/scoring_engine.py CHANGED
@@ -1544,10 +1544,11 @@ def _call_nemotron_scoring(
1544
  difficulty_profile: str,
1545
  difficulty_label: str,
1546
  resolved_mode: str,
1547
- ) -> tuple[dict[str, Any], str, str, str] | None:
1548
  """Call Nemotron for dimension scores only (Call 1).
1549
 
1550
- Returns (scores, best_answer, weakest_answer, why_weak) on success, or None on failure.
 
1551
  """
1552
  messages = _build_scoring_only_prompt(
1553
  session, signals, local_reference, difficulty_profile, difficulty_label
@@ -1558,11 +1559,12 @@ def _call_nemotron_scoring(
1558
  if result.get("ok") and result.get("content"):
1559
  raw_content = result["content"]
1560
  else:
1561
- logger.warning("scoring_engine: Nemotron scoring call not ok — %s", result.get("error"))
1562
- return None
 
1563
  except Exception as exc:
1564
  logger.warning("scoring_engine: Nemotron scoring raised — %s", exc)
1565
- return None
1566
 
1567
  parsed, extraction_used = parse_model_json(raw_content)
1568
  if not isinstance(parsed, dict) or not parsed:
@@ -1578,7 +1580,7 @@ def _call_nemotron_scoring(
1578
  extraction_used,
1579
  sanitize_for_log(raw_content),
1580
  )
1581
- return _normalize_scoring_result(parsed)
1582
 
1583
  # Repair attempt
1584
  logger.warning(
@@ -1597,12 +1599,12 @@ def _call_nemotron_scoring(
1597
  repaired = _normalize_scoring_json(repaired)
1598
  if isinstance(repaired, dict) and repaired and _validate_scoring_json(repaired):
1599
  logger.info("scoring_engine: repaired scoring JSON OK")
1600
- return _normalize_scoring_result(repaired)
1601
  except Exception as exc:
1602
  logger.warning("scoring_engine: scoring repair raised — %s", exc)
1603
 
1604
  logger.warning("scoring_engine: Nemotron scoring failed — will fall back to local scores")
1605
- return None
1606
 
1607
 
1608
  # ---------------------------------------------------------------------------
@@ -1668,6 +1670,7 @@ def generate_claim_based_scorecard(
1668
 
1669
  # Step 3: Nemotron scoring call (Call 1) — skip when no substantive battle answers
1670
  nemotron_scoring_result = None
 
1671
  skip_nemotron_scoring = engagement_info["substantive_answers"] == 0
1672
  if skip_nemotron_scoring:
1673
  logger.info(
@@ -1678,7 +1681,7 @@ def generate_claim_based_scorecard(
1678
  has_startup,
1679
  )
1680
  elif resolved_mode == "premium_nvidia":
1681
- nemotron_scoring_result = _call_nemotron_scoring(
1682
  session, signals, local_reference,
1683
  difficulty_profile, difficulty_label, resolved_mode,
1684
  )
@@ -1776,6 +1779,7 @@ def generate_claim_based_scorecard(
1776
  "coaching_source": coaching_source,
1777
  "difficulty_profile": difficulty_profile,
1778
  "difficulty_label": difficulty_label,
 
1779
  }
1780
  result = _sync_overall_to_dimensions(result)
1781
  result["overall"] = _apply_practice_score_nudge(
@@ -1897,6 +1901,15 @@ def generate_claim_based_scorecard(
1897
  else "Nemotron scoring failed; used local scoring fallback."
1898
  )
1899
  ),
 
 
 
 
 
 
 
 
 
1900
  }
1901
 
1902
 
 
1544
  difficulty_profile: str,
1545
  difficulty_label: str,
1546
  resolved_mode: str,
1547
+ ) -> tuple[tuple[dict[str, Any], str, str, str] | None, str]:
1548
  """Call Nemotron for dimension scores only (Call 1).
1549
 
1550
+ Returns ((scores, best_answer, weakest_answer, why_weak), "") on success,
1551
+ or (None, failure_reason) on failure.
1552
  """
1553
  messages = _build_scoring_only_prompt(
1554
  session, signals, local_reference, difficulty_profile, difficulty_label
 
1559
  if result.get("ok") and result.get("content"):
1560
  raw_content = result["content"]
1561
  else:
1562
+ err = str(result.get("error") or "api_call_failed")
1563
+ logger.warning("scoring_engine: Nemotron scoring call not ok — %s", err)
1564
+ return None, f"api_error:{err[:120]}"
1565
  except Exception as exc:
1566
  logger.warning("scoring_engine: Nemotron scoring raised — %s", exc)
1567
+ return None, f"exception:{str(exc)[:120]}"
1568
 
1569
  parsed, extraction_used = parse_model_json(raw_content)
1570
  if not isinstance(parsed, dict) or not parsed:
 
1580
  extraction_used,
1581
  sanitize_for_log(raw_content),
1582
  )
1583
+ return _normalize_scoring_result(parsed), ""
1584
 
1585
  # Repair attempt
1586
  logger.warning(
 
1599
  repaired = _normalize_scoring_json(repaired)
1600
  if isinstance(repaired, dict) and repaired and _validate_scoring_json(repaired):
1601
  logger.info("scoring_engine: repaired scoring JSON OK")
1602
+ return _normalize_scoring_result(repaired), ""
1603
  except Exception as exc:
1604
  logger.warning("scoring_engine: scoring repair raised — %s", exc)
1605
 
1606
  logger.warning("scoring_engine: Nemotron scoring failed — will fall back to local scores")
1607
+ return None, "json_parse_failed"
1608
 
1609
 
1610
  # ---------------------------------------------------------------------------
 
1670
 
1671
  # Step 3: Nemotron scoring call (Call 1) — skip when no substantive battle answers
1672
  nemotron_scoring_result = None
1673
+ nemotron_failure_reason = ""
1674
  skip_nemotron_scoring = engagement_info["substantive_answers"] == 0
1675
  if skip_nemotron_scoring:
1676
  logger.info(
 
1681
  has_startup,
1682
  )
1683
  elif resolved_mode == "premium_nvidia":
1684
+ nemotron_scoring_result, nemotron_failure_reason = _call_nemotron_scoring(
1685
  session, signals, local_reference,
1686
  difficulty_profile, difficulty_label, resolved_mode,
1687
  )
 
1779
  "coaching_source": coaching_source,
1780
  "difficulty_profile": difficulty_profile,
1781
  "difficulty_label": difficulty_label,
1782
+ "fallback_reason": "",
1783
  }
1784
  result = _sync_overall_to_dimensions(result)
1785
  result["overall"] = _apply_practice_score_nudge(
 
1901
  else "Nemotron scoring failed; used local scoring fallback."
1902
  )
1903
  ),
1904
+ "fallback_reason": (
1905
+ "no_battle_answers"
1906
+ if skip_nemotron_scoring and not has_startup and signals.get("signal_count", 0) == 0
1907
+ else (
1908
+ "startup_context_only"
1909
+ if skip_nemotron_scoring
1910
+ else (nemotron_failure_reason or "nemotron_scoring_failed")
1911
+ )
1912
+ ),
1913
  }
1914
 
1915
 
frontend/index.html CHANGED
@@ -19,6 +19,7 @@
19
  <!-- Landing — Founder Pressure Arena (Pass 1) -->
20
  <section id="screen-landing" class="screen active">
21
  <div class="arena-landing">
 
22
  <div class="arena-scene" aria-hidden="true">
23
  <div class="arena-scene-base"></div>
24
  <div class="hero-center-haze"></div>
@@ -234,26 +235,34 @@
234
  </div>
235
  </section>
236
 
237
- <!-- Start method -->
238
- <section id="screen-start-method" class="screen">
239
- <div class="panel glass">
240
- <div class="panel-header">
241
- <h2>How do you want to start?</h2>
242
- <button id="btn-start-back-landing" class="btn btn-ghost">Back</button>
243
- </div>
244
- <div class="start-method-grid">
245
- <button id="btn-start-text" class="start-method-card selected" type="button">
246
- <span class="start-method-icon"></span>
 
 
 
 
 
 
247
  <h3>Fill Details</h3>
248
- <p>Type your startup context manually.</p>
 
249
  </button>
250
- <button id="btn-start-voice" class="start-method-card" type="button">
251
- <span class="start-method-icon voice-icon">🎙</span>
252
- <h3>Pitch First</h3>
253
- <p>Speak a 60–90 second pitch and let AI extract the details.</p>
 
 
254
  </button>
255
  </div>
256
- <button id="btn-continue-start" class="btn btn-primary btn-wide">Continue</button>
257
  </div>
258
  </section>
259
 
@@ -335,7 +344,7 @@
335
  <textarea
336
  id="quick-pitch-text"
337
  class="quick-pitch-textarea"
338
- rows="9"
339
  placeholder="We're building EventRadar AI for students who miss hackathons because events are scattered across WhatsApp, LinkedIn, and college groups. We collect events in one place and recommend the best ones based on student interests. We tested with 80 students and want mentorship and pilot support."
340
  ></textarea>
341
  <div class="quick-pitch-actions">
@@ -345,15 +354,13 @@
345
  <button id="btn-load-sample-setup" class="btn btn-secondary" type="button">Load Demo Founder</button>
346
  </div>
347
  </div>
348
- <p id="structure-pitch-hint" class="quick-pitch-hint" hidden></p>
349
  </div>
350
 
351
- <!-- AI brief preview -->
352
  <div id="brief-preview-panel" class="panel glass briefing-panel brief-preview-panel" hidden>
353
  <div class="brief-preview-header">
354
  <div class="brief-preview-title-row">
355
  <h3 class="briefing-section-title">AI-Structured Founder Brief</h3>
356
- <span id="brief-preview-confidence" class="brief-confidence-chip"></span>
357
  </div>
358
  <p class="brief-preview-helper">AI extracted this from your pitch. Review and confirm.</p>
359
  <p id="brief-preview-hint" class="brief-preview-hint" hidden></p>
@@ -375,7 +382,7 @@
375
  <p class="brief-read-value is-empty">Not specified</p>
376
  <input type="text" name="target_users" class="brief-read-input" hidden />
377
  </div>
378
- <div class="brief-read-card brief-read-wide" data-field="problem">
379
  <div class="brief-read-head">
380
  <span class="brief-read-label">Problem</span>
381
  <button type="button" class="brief-read-edit" aria-label="Edit Problem">✎</button>
@@ -383,7 +390,7 @@
383
  <p class="brief-read-value is-empty">Not specified</p>
384
  <textarea name="problem" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
385
  </div>
386
- <div class="brief-read-card brief-read-wide" data-field="solution">
387
  <div class="brief-read-head">
388
  <span class="brief-read-label">Solution</span>
389
  <button type="button" class="brief-read-edit" aria-label="Edit Solution">✎</button>
@@ -391,7 +398,7 @@
391
  <p class="brief-read-value is-empty">Not specified</p>
392
  <textarea name="solution" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
393
  </div>
394
- <div class="brief-read-card brief-read-wide" data-field="why_ai">
395
  <div class="brief-read-head">
396
  <span class="brief-read-label">Why AI</span>
397
  <button type="button" class="brief-read-edit" aria-label="Edit Why AI">✎</button>
@@ -430,32 +437,17 @@
430
  </div>
431
  </div>
432
 
433
- <!-- Advanced Briefing (tab onlyno duplicate collapsed section) -->
434
  <div id="panel-advanced-briefing" class="panel glass briefing-panel advanced-briefing-panel" hidden>
435
- <p class="advanced-briefing-helper">Want full control? Edit every field manually.</p>
436
- <form id="startup-form" class="startup-form briefing-form">
437
- <div class="briefing-group">
438
- <label>Name<input name="name" type="text" placeholder="EventRadar AI" /></label>
439
- </div>
440
- <div class="briefing-group briefing-group-problem">
441
- <h4 class="briefing-group-label">Problem + Users</h4>
442
- <label>Problem<textarea name="problem" rows="3" placeholder="What pain are you solving?"></textarea></label>
443
- <label>Target Users<input name="target_users" type="text" placeholder="Who feels this pain most?" /></label>
444
- </div>
445
- <div class="briefing-group briefing-group-solution">
446
- <h4 class="briefing-group-label">Solution + Why AI</h4>
447
- <label>Solution<textarea name="solution" rows="3" placeholder="What do you build?"></textarea></label>
448
- <label>Why AI<textarea name="why_ai" rows="2" placeholder="Why AI instead of rules or manual work?"></textarea></label>
449
- </div>
450
- <div class="briefing-group briefing-group-traction">
451
- <h4 class="briefing-group-label">Traction + Competitors</h4>
452
- <label>Competitors<input name="competitors" type="text" placeholder="Who else solves this?" /></label>
453
- <label>Traction<input name="traction" type="text" placeholder="Users, pilots, revenue, demos…" /></label>
454
- </div>
455
- <div class="briefing-group briefing-group-ask">
456
- <h4 class="briefing-group-label">Ask / Desired Outcome</h4>
457
- <label>Ask<input name="ask" type="text" placeholder="Funding, pilot, mentorship, sponsorship…" /></label>
458
- </div>
459
  </form>
460
  </div>
461
  </div>
@@ -750,8 +742,8 @@
750
  <div class="sc-ring-wrap">
751
  <div class="score-orb sc-ring" aria-label="Overall pitch score">
752
  <strong id="overall-score" class="score-orb-value">0</strong>
753
- <span id="overall-label" class="score-orb-label"></span>
754
  </div>
 
755
  </div>
756
  <div class="sc-hero-meta">
757
  <p id="sc-hero-label" class="sc-hero-kicker">Pitch Battle Result</p>
@@ -761,15 +753,19 @@
761
  <span class="sc-chip sc-chip-weak" id="chip-weakest-dim">↓ Weakest: —</span>
762
  <span class="sc-chip sc-chip-model" id="chip-score-source" hidden>⚡ Nemotron</span>
763
  </div>
764
- <div class="sc-hero-actions">
765
- <button id="btn-path-to-80" class="btn sc-btn-gold" type="button">View Path to 80+</button>
766
- <button id="btn-scorecard-retry" class="btn sc-btn-secondary" type="button">Retry Weakest Question</button>
767
- <button id="btn-reset" class="btn sc-btn-secondary" type="button">New Battle</button>
 
 
 
 
 
 
 
768
  </div>
769
  </div>
770
- <div class="sc-hero-conversation">
771
- <button id="btn-view-conversation" class="btn sc-btn-conversation" type="button">View Conversation</button>
772
- </div>
773
  </div>
774
  </header>
775
 
@@ -814,14 +810,14 @@
814
  <article id="voice-delivery-section" class="sc-voice-inline" hidden>
815
  <div class="sc-tab-divider"></div>
816
  <p class="sc-tab-eyebrow">Voice Delivery</p>
817
- <div id="voice-delivery-content" class="voice-delivery-grid"></div>
818
  </article>
819
  </div>
820
 
821
  <div class="sc-tab-panel" data-panel="answers" role="tabpanel" hidden>
822
  <div id="answers-empty-state" class="sc-answers-empty" hidden>
823
  <p class="sc-empty-title">No battle answers recorded.</p>
824
- <p class="sc-empty-sub">You ended the battle before responding.</p>
825
  <div class="sc-empty-actions">
826
  <button type="button" id="btn-answers-retry" class="btn sc-btn-gold">Retry Weakest Question</button>
827
  <button type="button" id="btn-answers-new-battle" class="btn sc-btn-secondary">New Battle</button>
@@ -859,29 +855,6 @@
859
  </div>
860
  </div>
861
  </div>
862
-
863
- <section id="judge-verdict-hero" class="sc-verdict-section sc-verdict-inline" hidden>
864
- <p class="sc-section-label sc-verdict-heading">Judge Verdict</p>
865
- <div id="judge-verdict-section" class="sc-verdict-card">
866
- <div class="sc-verdict-head">
867
- <span id="verdict-persona-badge" class="sc-verdict-judge"></span>
868
- <span id="verdict-interest-badge" class="sc-verdict-pill"></span>
869
- </div>
870
- <blockquote id="verdict-reaction" class="sc-verdict-quote"></blockquote>
871
- <div class="sc-verdict-meta">
872
- <div class="sc-verdict-meta-item">
873
- <span class="sc-meta-label">Deal Type</span>
874
- <strong id="verdict-deal-type" class="sc-meta-value"></strong>
875
- </div>
876
- <div class="sc-verdict-meta-item sc-verdict-meta-wide">
877
- <span class="sc-meta-label">Why</span>
878
- <strong id="verdict-why" class="sc-meta-value"></strong>
879
- </div>
880
- </div>
881
- <p id="verdict-opening-offer" class="sc-verdict-offer" hidden></p>
882
- <div id="verdict-actions" class="sc-verdict-actions"></div>
883
- </div>
884
- </section>
885
  </div>
886
  </div>
887
 
@@ -1200,6 +1173,46 @@
1200
  </div>
1201
  </div>
1202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1203
  <!-- Coaching Roadmap Overlay -->
1204
  <div id="path80-overlay" class="path80-overlay conversation-modal" hidden aria-modal="true" role="dialog" aria-label="Path to 80+">
1205
  <div class="path80-panel glass results-modal-panel">
@@ -1340,6 +1353,9 @@
1340
  <span id="retry-overall-lift" class="retry-overall-lift"></span>
1341
  <span id="retry-verdict-badge" class="retry-verdict-badge result-verdict-badge"></span>
1342
  </div>
 
 
 
1343
  <p id="retry-next-prompt" class="retry-next-prompt clamp-text"></p>
1344
  <div class="retry-result-actions scorecard-actions">
1345
  <button id="btn-retry-again" class="btn btn-retry-start" type="button">Retry Again</button>
 
19
  <!-- Landing — Founder Pressure Arena (Pass 1) -->
20
  <section id="screen-landing" class="screen active">
21
  <div class="arena-landing">
22
+ <canvas id="pfFallCanvas" aria-hidden="true" style="position:absolute;inset:0;z-index:1;pointer-events:none;opacity:0.45;width:100%;height:100%;"></canvas>
23
  <div class="arena-scene" aria-hidden="true">
24
  <div class="arena-scene-base"></div>
25
  <div class="hero-center-haze"></div>
 
235
  </div>
236
  </section>
237
 
238
+ <!-- Start method — entry path chooser -->
239
+ <section id="screen-start-method" class="screen screen-start-path">
240
+ <div class="start-path-shell">
241
+ <header class="start-path-header">
242
+ <div class="start-path-header-main">
243
+ <p class="results-eyebrow">Pre-Fight Briefing</p>
244
+ <h2 class="start-path-title">Choose your entry mode</h2>
245
+ <p class="start-path-sub">Same arena — two ways in. Both lead to your structured brief.</p>
246
+ </div>
247
+ <button id="btn-start-back-landing" class="btn btn-ghost" type="button">Back</button>
248
+ </header>
249
+
250
+ <div class="start-method-grid start-path-grid">
251
+ <button id="btn-start-text" class="start-method-card start-path-card start-path-card-text" type="button">
252
+ <span class="start-path-badge">Mode 01</span>
253
+ <span class="start-method-icon" aria-hidden="true">✎</span>
254
  <h3>Fill Details</h3>
255
+ <p class="start-path-tagline">Written Briefing</p>
256
+ <p class="start-path-desc">Type or paste your pitch. AI structures it into a founder brief before the judge attacks.</p>
257
  </button>
258
+ <button id="btn-start-voice" class="start-method-card start-path-card start-path-card-voice" type="button">
259
+ <span class="start-path-badge start-path-badge-voice">Mode 02</span>
260
+ <span class="start-method-icon voice-icon" aria-hidden="true">🎙</span>
261
+ <h3>Voice Mode</h3>
262
+ <p class="start-path-tagline">Live Pitch Capture</p>
263
+ <p class="start-path-desc">Speak for 60–90 seconds. AI transcribes, extracts, and structures your startup brief.</p>
264
  </button>
265
  </div>
 
266
  </div>
267
  </section>
268
 
 
344
  <textarea
345
  id="quick-pitch-text"
346
  class="quick-pitch-textarea"
347
+ rows="5"
348
  placeholder="We're building EventRadar AI for students who miss hackathons because events are scattered across WhatsApp, LinkedIn, and college groups. We collect events in one place and recommend the best ones based on student interests. We tested with 80 students and want mentorship and pilot support."
349
  ></textarea>
350
  <div class="quick-pitch-actions">
 
354
  <button id="btn-load-sample-setup" class="btn btn-secondary" type="button">Load Demo Founder</button>
355
  </div>
356
  </div>
 
357
  </div>
358
 
359
+ <!-- AI brief preview — shown after Structure My Pitch (replaces quick pitch panel) -->
360
  <div id="brief-preview-panel" class="panel glass briefing-panel brief-preview-panel" hidden>
361
  <div class="brief-preview-header">
362
  <div class="brief-preview-title-row">
363
  <h3 class="briefing-section-title">AI-Structured Founder Brief</h3>
 
364
  </div>
365
  <p class="brief-preview-helper">AI extracted this from your pitch. Review and confirm.</p>
366
  <p id="brief-preview-hint" class="brief-preview-hint" hidden></p>
 
382
  <p class="brief-read-value is-empty">Not specified</p>
383
  <input type="text" name="target_users" class="brief-read-input" hidden />
384
  </div>
385
+ <div class="brief-read-card brief-read-short" data-field="problem">
386
  <div class="brief-read-head">
387
  <span class="brief-read-label">Problem</span>
388
  <button type="button" class="brief-read-edit" aria-label="Edit Problem">✎</button>
 
390
  <p class="brief-read-value is-empty">Not specified</p>
391
  <textarea name="problem" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
392
  </div>
393
+ <div class="brief-read-card brief-read-short" data-field="solution">
394
  <div class="brief-read-head">
395
  <span class="brief-read-label">Solution</span>
396
  <button type="button" class="brief-read-edit" aria-label="Edit Solution">✎</button>
 
398
  <p class="brief-read-value is-empty">Not specified</p>
399
  <textarea name="solution" class="brief-read-input brief-read-textarea" rows="2" hidden></textarea>
400
  </div>
401
+ <div class="brief-read-card brief-read-short" data-field="why_ai">
402
  <div class="brief-read-head">
403
  <span class="brief-read-label">Why AI</span>
404
  <button type="button" class="brief-read-edit" aria-label="Edit Why AI">✎</button>
 
437
  </div>
438
  </div>
439
 
440
+ <!-- Advanced Briefing (compact gridmatches structured brief layout) -->
441
  <div id="panel-advanced-briefing" class="panel glass briefing-panel advanced-briefing-panel" hidden>
442
+ <form id="startup-form" class="startup-form briefing-form advanced-briefing-form">
443
+ <label class="adv-field adv-field-wide">Name<input name="name" type="text" placeholder="EventRadar AI" /></label>
444
+ <label class="adv-field">Problem<textarea name="problem" rows="2" placeholder="What pain are you solving?"></textarea></label>
445
+ <label class="adv-field">Target Users<input name="target_users" type="text" placeholder="Who feels this pain most?" /></label>
446
+ <label class="adv-field">Solution<textarea name="solution" rows="2" placeholder="What do you build?"></textarea></label>
447
+ <label class="adv-field">Why AI<textarea name="why_ai" rows="2" placeholder="Why AI instead of rules or manual work?"></textarea></label>
448
+ <label class="adv-field">Competitors<input name="competitors" type="text" placeholder="Who else solves this?" /></label>
449
+ <label class="adv-field">Traction<input name="traction" type="text" placeholder="Users, pilots, revenue, demos…" /></label>
450
+ <label class="adv-field adv-field-wide">Ask<input name="ask" type="text" placeholder="Funding, pilot, mentorship, sponsorship…" /></label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  </form>
452
  </div>
453
  </div>
 
742
  <div class="sc-ring-wrap">
743
  <div class="score-orb sc-ring" aria-label="Overall pitch score">
744
  <strong id="overall-score" class="score-orb-value">0</strong>
 
745
  </div>
746
+ <span id="overall-label" class="score-orb-label sc-ring-caption"></span>
747
  </div>
748
  <div class="sc-hero-meta">
749
  <p id="sc-hero-label" class="sc-hero-kicker">Pitch Battle Result</p>
 
753
  <span class="sc-chip sc-chip-weak" id="chip-weakest-dim">↓ Weakest: —</span>
754
  <span class="sc-chip sc-chip-model" id="chip-score-source" hidden>⚡ Nemotron</span>
755
  </div>
756
+ <div class="sc-hero-actions-row">
757
+ <div class="sc-hero-actions sc-hero-actions-primary">
758
+ <button id="btn-path-to-80" class="btn sc-btn-gold" type="button">View Path to 80+</button>
759
+ <button id="btn-scorecard-retry" class="btn sc-btn-secondary" type="button">Retry Weakest Question</button>
760
+ </div>
761
+ <div class="sc-hero-actions sc-hero-actions-secondary">
762
+ <button id="btn-view-deal-scorecard" class="btn score-action-btn score-action-btn-secondary" type="button" hidden>View Combined Readout</button>
763
+ <button id="btn-view-judge-verdict" class="btn score-action-btn score-action-btn-verdict" type="button" hidden>View Judge Verdict</button>
764
+ <button id="btn-view-conversation" class="btn score-action-btn score-action-btn-secondary" type="button">View Conversation</button>
765
+ <button id="btn-reset" class="btn score-action-btn score-action-btn-secondary" type="button">New Battle</button>
766
+ </div>
767
  </div>
768
  </div>
 
 
 
769
  </div>
770
  </header>
771
 
 
810
  <article id="voice-delivery-section" class="sc-voice-inline" hidden>
811
  <div class="sc-tab-divider"></div>
812
  <p class="sc-tab-eyebrow">Voice Delivery</p>
813
+ <div id="voice-delivery-content" class="voice-delivery-wrap"></div>
814
  </article>
815
  </div>
816
 
817
  <div class="sc-tab-panel" data-panel="answers" role="tabpanel" hidden>
818
  <div id="answers-empty-state" class="sc-answers-empty" hidden>
819
  <p class="sc-empty-title">No battle answers recorded.</p>
820
+ <p class="sc-empty-sub">You ended before responding, so the scorecard can only grade the startup brief.</p>
821
  <div class="sc-empty-actions">
822
  <button type="button" id="btn-answers-retry" class="btn sc-btn-gold">Retry Weakest Question</button>
823
  <button type="button" id="btn-answers-new-battle" class="btn sc-btn-secondary">New Battle</button>
 
855
  </div>
856
  </div>
857
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
858
  </div>
859
  </div>
860
 
 
1173
  </div>
1174
  </div>
1175
 
1176
+ <!-- Judge Verdict Modal -->
1177
+ <div id="verdict-overlay" class="verdict-overlay conversation-modal" hidden aria-modal="true" role="dialog" aria-label="Judge Verdict">
1178
+ <div class="verdict-modal-panel glass results-modal-panel">
1179
+ <div class="verdict-modal-header">
1180
+ <div>
1181
+ <p class="results-eyebrow">Post-Battle</p>
1182
+ <h2 class="verdict-modal-title">Judge Verdict</h2>
1183
+ </div>
1184
+ <button id="btn-close-verdict" class="btn btn-ghost btn-sm" type="button" aria-label="Close">✕ Close</button>
1185
+ </div>
1186
+ <div id="judge-verdict-section" class="sc-verdict-card sc-verdict-modal-card">
1187
+ <div class="sc-verdict-head">
1188
+ <span id="verdict-persona-badge" class="sc-verdict-judge"></span>
1189
+ <span id="verdict-interest-badge" class="sc-verdict-pill"></span>
1190
+ </div>
1191
+ <blockquote id="verdict-reaction" class="sc-verdict-quote"></blockquote>
1192
+ <div class="sc-verdict-meta">
1193
+ <div class="sc-verdict-meta-item">
1194
+ <span class="sc-meta-label">Deal Type</span>
1195
+ <strong id="verdict-deal-type" class="sc-meta-value"></strong>
1196
+ </div>
1197
+ <div class="sc-verdict-meta-item sc-verdict-meta-wide">
1198
+ <span class="sc-meta-label">Why</span>
1199
+ <strong id="verdict-why" class="sc-meta-value"></strong>
1200
+ </div>
1201
+ </div>
1202
+ <p id="verdict-opening-offer" class="sc-verdict-offer" hidden></p>
1203
+ <p id="verdict-deal-locked-msg" class="verdict-deal-locked-msg" hidden>Deal phase is not unlocked for this verdict.</p>
1204
+ </div>
1205
+ <div class="verdict-modal-footer">
1206
+ <div id="verdict-actions" class="sc-verdict-actions verdict-modal-actions"></div>
1207
+ <div class="verdict-modal-footer-secondary">
1208
+ <button id="btn-verdict-retry" class="btn sc-btn-secondary" type="button">Retry Weakest Question</button>
1209
+ <button id="btn-verdict-new-battle" class="btn sc-btn-secondary" type="button">New Battle</button>
1210
+ <button id="btn-close-verdict-bottom" class="btn btn-ghost" type="button">Back to Scorecard</button>
1211
+ </div>
1212
+ </div>
1213
+ </div>
1214
+ </div>
1215
+
1216
  <!-- Coaching Roadmap Overlay -->
1217
  <div id="path80-overlay" class="path80-overlay conversation-modal" hidden aria-modal="true" role="dialog" aria-label="Path to 80+">
1218
  <div class="path80-panel glass results-modal-panel">
 
1353
  <span id="retry-overall-lift" class="retry-overall-lift"></span>
1354
  <span id="retry-verdict-badge" class="retry-verdict-badge result-verdict-badge"></span>
1355
  </div>
1356
+ <p id="retry-projection-note" class="retry-projection-note" hidden>
1357
+ Training projection only — your original scorecard stays unchanged.
1358
+ </p>
1359
  <p id="retry-next-prompt" class="retry-next-prompt clamp-text"></p>
1360
  <div class="retry-result-actions scorecard-actions">
1361
  <button id="btn-retry-again" class="btn btn-retry-start" type="button">Retry Again</button>
frontend/script.js CHANGED
@@ -23,9 +23,14 @@ const state = {
23
  dealBattleLog: [],
24
  battleMetaSnapshot: null,
25
  scorecardSnapshot: null,
 
 
26
  briefingMode: "quick",
27
  briefStructured: false,
28
  pitchExtractionConfidence: null,
 
 
 
29
  battleConfidencePct: null,
30
  };
31
 
@@ -62,12 +67,16 @@ const loadingText = document.getElementById("loading-message");
62
  const battleStatus = document.getElementById("battle-status");
63
  const errorBanner = document.getElementById("error-banner");
64
 
 
 
 
 
65
  function showScreen(name) {
66
  Object.entries(screens).forEach(([key, el]) => {
67
  el.classList.toggle("active", key === name);
68
  });
69
  const app = document.getElementById("app");
70
- app?.classList.toggle("app-arena-fullwidth", name === "battle" || name === "deal" || name === "scorecard");
71
  app?.classList.toggle("app-scorecard-fullwidth", name === "scorecard");
72
  if (name === "landing" && landingIntroComplete) {
73
  finalizeLandingIntroStatic();
@@ -120,6 +129,168 @@ function typeText(el, text, speedMs, onDone) {
120
  step();
121
  }
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  function initLandingIntro() {
124
  if (landingIntroRunning || landingIntroComplete) return;
125
  landingIntroRunning = true;
@@ -169,6 +340,22 @@ function isNemotronScorecardSource(src) {
169
  return NEMOTRON_SCORECARD_SOURCES.has(String(src ?? "").trim());
170
  }
171
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  const INTERNAL_ERROR_PATTERNS = [
173
  /nvidia/i,
174
  /max_tokens/i,
@@ -243,6 +430,7 @@ function commitBriefFieldEdit(card) {
243
  }
244
  closeBriefFieldExpanded(card);
245
  syncBriefToStartupForm();
 
246
  }
247
 
248
  function cancelBriefFieldEdit(card) {
@@ -338,7 +526,7 @@ function syncBriefToStartupForm() {
338
  }
339
 
340
  function getStartupPayload() {
341
- if (briefPreviewPanel && !briefPreviewPanel.hidden) {
342
  syncBriefToStartupForm();
343
  }
344
  const data = new FormData(startupForm);
@@ -356,7 +544,7 @@ function fillStartupForm(startup) {
356
  function setBriefingMode(mode) {
357
  state.briefingMode = mode;
358
  const isQuick = mode === "quick";
359
- const previewVisible = state.briefStructured;
360
 
361
  document.getElementById("tab-quick-pitch")?.classList.toggle("active", isQuick);
362
  document.getElementById("tab-advanced-briefing")?.classList.toggle("active", !isQuick);
@@ -364,6 +552,7 @@ function setBriefingMode(mode) {
364
  document.getElementById("tab-advanced-briefing")?.setAttribute("aria-selected", String(!isQuick));
365
  briefingLeftCol?.classList.toggle("mode-quick", isQuick);
366
  briefingLeftCol?.classList.toggle("mode-advanced", !isQuick);
 
367
 
368
  const advancedPanel = document.getElementById("panel-advanced-briefing");
369
  if (advancedPanel) advancedPanel.hidden = isQuick;
@@ -380,7 +569,9 @@ function setBriefingMode(mode) {
380
  const subtitle = document.getElementById("briefing-subtitle");
381
  if (subtitle) {
382
  subtitle.textContent = isQuick
383
- ? "Pitch naturally. We'll structure it before the judge attacks it."
 
 
384
  : "Want full control? Edit every field manually.";
385
  }
386
  }
@@ -388,15 +579,7 @@ function setBriefingMode(mode) {
388
  function showBriefPreview(meta = {}) {
389
  if (quickPitchPanel) quickPitchPanel.hidden = true;
390
  if (briefPreviewPanel) briefPreviewPanel.hidden = false;
391
- const advancedPanel = document.getElementById("panel-advanced-briefing");
392
- if (advancedPanel) advancedPanel.hidden = true;
393
-
394
- const confEl = document.getElementById("brief-preview-confidence");
395
- if (confEl) {
396
- const conf = meta.confidence || "medium";
397
- confEl.textContent = `Confidence: ${String(conf).toUpperCase()}`;
398
- confEl.className = `brief-confidence-chip confidence-${conf}`;
399
- }
400
 
401
  const hintEl = document.getElementById("brief-preview-hint");
402
  if (hintEl) {
@@ -417,7 +600,7 @@ function hideBriefPreview() {
417
  state.briefStructured = false;
418
  if (quickPitchPanel) quickPitchPanel.hidden = false;
419
  if (briefPreviewPanel) briefPreviewPanel.hidden = true;
420
- document.getElementById("structure-pitch-hint")?.setAttribute("hidden", "");
421
  document.getElementById("brief-preview-hint")?.setAttribute("hidden", "");
422
  briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing").forEach((card) => {
423
  commitBriefFieldEdit(card);
@@ -439,6 +622,68 @@ function confidenceFromStartupFields(startup = {}) {
439
  return "low";
440
  }
441
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  function confidenceLevelToPct(level) {
443
  const key = String(level || "medium").toLowerCase();
444
  if (key === "high") return 76;
@@ -449,6 +694,10 @@ function confidenceLevelToPct(level) {
449
  function syncPitchExtractionConfidence(meta = {}, startup = null) {
450
  const level = extractionConfidenceLevel(meta) || confidenceFromStartupFields(startup) || "medium";
451
  state.pitchExtractionConfidence = level;
 
 
 
 
452
  return level;
453
  }
454
 
@@ -456,12 +705,16 @@ function fillBriefPreview(startup, meta = {}) {
456
  state.briefStructured = true;
457
  syncPitchExtractionConfidence(meta, startup);
458
  fillStartupForm(startup);
 
459
  showBriefPreview(meta);
460
  }
461
 
462
  function resetSetupScreen() {
463
  state.briefStructured = false;
464
  state.pitchExtractionConfidence = null;
 
 
 
465
  setBriefingMode("quick");
466
  hideBriefPreview();
467
  const quickText = document.getElementById("quick-pitch-text");
@@ -476,6 +729,15 @@ async function structurePitch() {
476
  showErrorBanner("Type or paste your pitch first.");
477
  return;
478
  }
 
 
 
 
 
 
 
 
 
479
  try {
480
  setGlobalLoading(true, "Structuring your pitch…");
481
  state.startMode = "text";
@@ -484,6 +746,8 @@ async function structurePitch() {
484
  showErrorBanner(data.error || "Could not structure your pitch. Try Advanced Briefing.");
485
  return;
486
  }
 
 
487
  fillBriefPreview(data.startup_context, data);
488
  hideErrorBanner();
489
  } catch (error) {
@@ -643,7 +907,11 @@ function renderConfidenceMeter(confidencePct) {
643
  }
644
 
645
  function refreshBattleConfidenceFromPressure(pressurePct, round = 1) {
646
- const base = confidenceLevelToPct(state.pitchExtractionConfidence || "medium");
 
 
 
 
647
  const pressureDrag = Math.round(Number(pressurePct) * 0.25);
648
  const roundDrag = Math.max(0, Number(round) - 1) * 2;
649
  const ceiling = Math.max(8, base - pressureDrag - roundDrag);
@@ -659,9 +927,12 @@ function adjustBattleConfidenceFromAnswer(quality) {
659
  const deltas = { strong: 8, partial: -2, weak: -12, non_answer: -20 };
660
  const delta = deltas[String(quality || "").toLowerCase()] ?? 0;
661
  if (!delta) return;
 
 
 
662
  state.battleConfidencePct = Math.max(
663
  8,
664
- Math.min(92, (state.battleConfidencePct ?? confidenceLevelToPct(state.pitchExtractionConfidence)) + delta),
665
  );
666
  renderConfidenceMeter(state.battleConfidencePct);
667
  }
@@ -1087,6 +1358,11 @@ export async function loadSample() {
1087
 
1088
  export async function startSession() {
1089
  try {
 
 
 
 
 
1090
  setGlobalLoading(true, "AI judge is preparing the first attack…");
1091
  battleStatus.hidden = true;
1092
  hideBattleReadiness();
@@ -1265,11 +1541,13 @@ export async function resetBattle() {
1265
  state.pendingDealVoiceTurn = null;
1266
  state.startMode = "text";
1267
  state.pitchExtractionConfidence = null;
 
1268
  state.battleConfidencePct = null;
1269
  state.conversationLog = [];
1270
  state.dealConversationLog = [];
1271
  state.battleLog = [];
1272
  state.dealBattleLog = [];
 
1273
  resetLiveTurnTracking();
1274
  chatWindow.innerHTML = "";
1275
  if (dealChatWindow) dealChatWindow.innerHTML = "";
@@ -1518,7 +1796,27 @@ function renderScorecard(data) {
1518
  const { strongest, weakest } = getStrongestWeakest(scores);
1519
  const se = data.score_explanation ?? {};
1520
  const explanation = se;
1521
- const nemotronScored = isNemotronScorecardSource(data.scorecard_source);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1522
 
1523
  const opponent = PERSONA_LABELS[state.persona] ?? data.opponent ?? "AI Judge";
1524
  const mode = DIFFICULTY_LABELS[state.difficultyProfile]
@@ -1553,16 +1851,6 @@ function renderScorecard(data) {
1553
  : "↓ Weakest: —";
1554
  }
1555
 
1556
- const chipSource = document.getElementById("chip-score-source");
1557
- const sourceBadgeEl = document.getElementById("scorecard-source-badge");
1558
- if (chipSource) {
1559
- chipSource.textContent = nemotronScored ? "⚡ Nemotron" : "⚡ Local";
1560
- chipSource.hidden = false;
1561
- }
1562
- if (sourceBadgeEl) {
1563
- sourceBadgeEl.textContent = nemotronScored ? "Powered by NVIDIA Nemotron" : "Local scoring";
1564
- }
1565
-
1566
  const fallbackWarnEl = document.getElementById("scorecard-fallback-warning");
1567
  if (fallbackWarnEl) {
1568
  fallbackWarnEl.textContent = "";
@@ -1659,6 +1947,23 @@ function renderScorecard(data) {
1659
  if (answersEmpty) answersEmpty.hidden = !noAnswers;
1660
  if (answersContent) answersContent.hidden = noAnswers;
1661
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1662
  state.scoreExplanation = data.score_explanation ?? null;
1663
 
1664
  const pathBtn = document.getElementById("btn-path-to-80");
@@ -1683,6 +1988,8 @@ function renderScorecard(data) {
1683
  weakest: weakest ? formatDimLabel(weakest[0]) : null,
1684
  roundsCompleted: state.battleLog?.length ?? 0,
1685
  };
 
 
1686
  }
1687
 
1688
  const DEAL_TYPE_LABELS = {
@@ -1704,13 +2011,36 @@ function verdictNegotiationCta(label, fallback = "Start Negotiation →") {
1704
  .replace(/Deal Round/i, "Negotiation");
1705
  }
1706
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1707
  function renderJudgeVerdict(verdict) {
1708
- const heroWrap = document.getElementById("judge-verdict-hero");
1709
  if (!verdict || !verdict.interest_level) {
1710
- if (heroWrap) heroWrap.hidden = true;
1711
  return;
1712
  }
1713
- if (heroWrap) heroWrap.hidden = false;
1714
 
1715
  const interest = verdict.interest_level || "no_interest";
1716
  const personaLine = `${verdict.persona_name || "Judge"} — ${verdict.persona_type || ""}`.trim();
@@ -1739,32 +2069,26 @@ function renderJudgeVerdict(verdict) {
1739
  }
1740
  }
1741
 
 
 
 
 
 
 
1742
  const actions = document.getElementById("verdict-actions");
1743
  if (!actions) return;
1744
  actions.innerHTML = "";
1745
 
1746
- if (verdict.can_continue_to_deal) {
1747
  const btn = document.createElement("button");
1748
- btn.className = "btn btn-deal-continue";
1749
- btn.textContent = verdictNegotiationCta(verdict.next_step_label);
1750
  btn.type = "button";
1751
- btn.addEventListener("click", startDealPhase);
 
 
 
1752
  actions.appendChild(btn);
1753
- } else if (interest === "too_early") {
1754
- /* Footer sc-cta-row handles retry / setup */
1755
- } else if (interest === "no_interest") {
1756
- /* Footer sc-cta-row handles Path to 80+ and New Battle */
1757
- } else if (interest === "mild_interest" || interest === "strong_interest") {
1758
- if (!verdict.can_continue_to_deal) {
1759
- const btn = document.createElement("button");
1760
- btn.className = "btn btn-deal-continue";
1761
- btn.textContent = verdictNegotiationCta(verdict.next_step_label);
1762
- btn.type = "button";
1763
- btn.addEventListener("click", startDealPhase);
1764
- actions.appendChild(btn);
1765
- }
1766
- } else if (verdict.deal_type === "verdict_only") {
1767
- /* Footer sc-cta-row handles Path to 80+ */
1768
  }
1769
  }
1770
 
@@ -1940,7 +2264,22 @@ export async function endDeal() {
1940
  }
1941
  }
1942
 
 
 
 
 
 
 
 
 
 
 
 
 
1943
  function renderDealScorecard(data) {
 
 
 
1944
  const combined = data.combined_scorecard || {};
1945
  const deal = data.deal_scorecard || {};
1946
 
@@ -2116,16 +2455,27 @@ function renderVoiceDelivery(vd) {
2116
  || (vd.delivery_notes ?? []).find((n) => String(n).trim()) || "";
2117
 
2118
  content.innerHTML = `
2119
- <div class="voice-wave-decor" aria-hidden="true"></div>
2120
- <div class="voice-delivery-summary voice-delivery-grid">
2121
- <div class="voice-delivery-stat"><span>Voice turns</span><strong>${vd.total_voice_turns ?? 0}</strong></div>
2122
- <div class="voice-delivery-stat"><span>Filler words</span><strong>${vd.total_filler_words ?? 0}</strong></div>
2123
- <div class="voice-delivery-stat"><span>Common fillers</span><strong>${escapeHtml(fillers)}</strong></div>
2124
- <div class="voice-delivery-stat"><span>Pace</span><strong>${escapeHtml(vd.average_pace ?? "—")}</strong></div>
2125
- <div class="voice-delivery-stat"><span>Clarity signal</span><strong>${escapeHtml(vd.clarity_signal ?? "")}</strong></div>
2126
- <div class="voice-delivery-stat"><span>Confidence signal</span><strong>${escapeHtml(vd.confidence_signal ?? "—")}</strong></div>
 
 
 
 
 
 
 
 
 
 
 
 
2127
  </div>
2128
- ${overallNote ? `<p class="voice-delivery-overall">${escapeHtml(overallNote)}</p>` : ""}
2129
  `;
2130
 
2131
  const notes = (vd.delivery_notes ?? []).filter((n) => {
@@ -2289,29 +2639,62 @@ function showRetryVoicePreview(data) {
2289
 
2290
  function renderRetryResult(data) {
2291
  const comp = data.comparison ?? {};
 
2292
  document.getElementById("retry-result-old").textContent = comp.old_answer_summary ?? data.original_answer ?? "";
2293
  document.getElementById("retry-result-new").textContent = comp.new_answer_summary ?? data.retry_answer ?? "";
2294
  document.getElementById("retry-what-improved").textContent = comp.what_improved ?? "";
2295
  document.getElementById("retry-still-missing").textContent = comp.still_missing ?? "";
2296
  document.getElementById("retry-specific-tip").textContent = comp.specific_tip ?? "";
2297
 
2298
- const dim = formatDimLabel(data.dimension);
2299
- const before = comp.estimated_dimension_before ?? 0;
2300
- const after = comp.estimated_dimension_after ?? before;
2301
  document.getElementById("retry-dim-estimate").textContent =
2302
  `${formatDimLabel(data.dimension)}: ${before} → ${after}`;
2303
- const lift = data.updated_scorecard?.retry_overall_lift ?? comp.estimated_overall_lift ?? 0;
2304
- document.getElementById("retry-overall-lift").textContent =
2305
- data.updated_scorecard
2306
- ? `Overall score: ${data.updated_scorecard.overall} (+${lift})`
2307
- : `Overall lift: +${lift}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2308
 
2309
  const verdictEl = document.getElementById("retry-verdict-badge");
2310
- const verdict = comp.verdict ?? "needs_more_work";
 
 
2311
  const verdictLabels = {
2312
  improved: "Improved",
2313
  slightly_improved: "Slightly improved",
2314
- needs_more_work: "Needs more work",
2315
  };
2316
  if (verdictEl) {
2317
  verdictEl.textContent = verdictLabels[verdict] ?? verdict;
@@ -2353,14 +2736,6 @@ async function submitRetryAnswer() {
2353
  return;
2354
  }
2355
  renderRetryResult(data);
2356
- if (data.updated_scorecard) {
2357
- renderScorecard(data.updated_scorecard);
2358
- state.scoreExplanation = data.updated_scorecard.score_explanation ?? state.scoreExplanation;
2359
- if (data.judge_verdict) {
2360
- renderJudgeVerdict(data.judge_verdict);
2361
- state.judgeVerdict = data.judge_verdict;
2362
- }
2363
- }
2364
  hideErrorBanner();
2365
  } catch (error) {
2366
  console.error(error);
@@ -2380,49 +2755,54 @@ function openRetryFromPath80() {
2380
  startRetryDrill();
2381
  }
2382
 
2383
- document.getElementById("btn-load-sample").addEventListener("click", loadSample);
2384
- document.getElementById("btn-load-sample-setup")?.addEventListener("click", loadSample);
2385
- document.getElementById("btn-go-setup").addEventListener("click", () => {
2386
- resetSetupScreen();
2387
- showScreen("setup");
2388
  });
2389
- document.getElementById("btn-back-landing").addEventListener("click", () => {
2390
  resetSetupScreen();
2391
  showScreen("landing");
2392
  });
2393
- document.getElementById("tab-quick-pitch")?.addEventListener("click", () => setBriefingMode("quick"));
2394
- document.getElementById("tab-advanced-briefing")?.addEventListener("click", () => setBriefingMode("advanced"));
2395
- document.getElementById("btn-structure-pitch")?.addEventListener("click", structurePitch);
2396
- document.getElementById("btn-record-voice-setup")?.addEventListener("click", () => {
2397
  state.startMode = "voice";
 
2398
  showScreen("voicePitch");
2399
  });
2400
- document.getElementById("btn-looks-good-start")?.addEventListener("click", () => {
2401
  syncBriefToStartupForm();
2402
  startSession();
2403
  });
2404
- document.getElementById("btn-restructure-pitch")?.addEventListener("click", () => {
2405
  hideBriefPreview();
2406
  document.getElementById("quick-pitch-text")?.focus();
2407
  });
2408
- document.getElementById("btn-start-back-landing").addEventListener("click", () => showScreen("landing"));
2409
 
2410
- document.getElementById("btn-start-text").addEventListener("click", () => {
2411
  state.startMode = "text";
2412
- document.querySelectorAll(".start-method-card").forEach((c) => c.classList.remove("selected"));
2413
- document.getElementById("btn-start-text").classList.add("selected");
 
2414
  });
2415
- document.getElementById("btn-start-voice").addEventListener("click", () => {
 
2416
  state.startMode = "voice";
2417
- document.querySelectorAll(".start-method-card").forEach((c) => c.classList.remove("selected"));
2418
- document.getElementById("btn-start-voice").classList.add("selected");
2419
  });
2420
- document.getElementById("btn-continue-start").addEventListener("click", () => {
2421
- if (state.startMode === "voice") showScreen("voicePitch");
2422
- else showScreen("setup");
 
 
 
 
2423
  });
2424
- document.getElementById("btn-voice-pitch-back").addEventListener("click", () => showScreen("setup"));
2425
- document.getElementById("btn-voice-edit-manual").addEventListener("click", () => {
2426
  const form = document.getElementById("voice-extract-form");
2427
  const data = new FormData(form);
2428
  applyVoicePitchToBriefing({
@@ -2433,7 +2813,7 @@ document.getElementById("btn-voice-edit-manual").addEventListener("click", () =>
2433
  setBriefingMode("advanced");
2434
  showScreen("setup");
2435
  });
2436
- document.getElementById("btn-voice-looks-right").addEventListener("click", () => {
2437
  const form = document.getElementById("voice-extract-form");
2438
  const data = new FormData(form);
2439
  applyVoicePitchToBriefing({
@@ -2444,7 +2824,7 @@ document.getElementById("btn-voice-looks-right").addEventListener("click", () =>
2444
  });
2445
  showScreen("setup");
2446
  });
2447
- document.getElementById("btn-voice-turn-send").addEventListener("click", () => {
2448
  const transcript = document.getElementById("voice-turn-transcript")?.value?.trim();
2449
  if (!transcript || !state.pendingVoiceTurn) return;
2450
  sendMessage(transcript, {
@@ -2456,32 +2836,47 @@ document.getElementById("btn-voice-turn-send").addEventListener("click", () => {
2456
  },
2457
  });
2458
  });
2459
- document.getElementById("btn-start-battle").addEventListener("click", startSession);
2460
- document.getElementById("btn-end-battle").addEventListener("click", endBattle);
2461
- document.getElementById("btn-reset").addEventListener("click", resetBattle);
2462
- document.getElementById("btn-back-setup")?.addEventListener("click", () => showScreen("setup"));
2463
- document.getElementById("btn-path-to-80").addEventListener("click", openPath80);
2464
- document.getElementById("btn-close-path80").addEventListener("click", closePath80);
2465
- document.getElementById("btn-close-path80-bottom").addEventListener("click", closePath80);
2466
- document.getElementById("btn-retry-question")?.addEventListener("click", openRetryFromPath80);
2467
- document.getElementById("btn-scorecard-retry")?.addEventListener("click", startRetryDrill);
2468
- document.getElementById("btn-prep-retry")?.addEventListener("click", startRetryDrill);
2469
- document.getElementById("btn-answers-retry")?.addEventListener("click", startRetryDrill);
2470
- document.getElementById("btn-answers-new-battle")?.addEventListener("click", resetBattle);
2471
- document.getElementById("path80-overlay").addEventListener("click", (e) => {
 
 
 
 
 
 
 
 
 
 
 
 
2472
  if (e.target === e.currentTarget) closePath80();
2473
  });
 
 
 
2474
 
2475
- document.getElementById("btn-close-retry")?.addEventListener("click", closeRetryOverlay);
2476
- document.getElementById("btn-submit-retry")?.addEventListener("click", submitRetryAnswer);
2477
- document.getElementById("btn-retry-again")?.addEventListener("click", () => {
2478
  if (state.retryDrill) populateRetryDrill(state.retryDrill);
2479
  });
2480
- document.getElementById("btn-retry-back-scorecard")?.addEventListener("click", () => {
2481
  closeRetryOverlay();
2482
  showScreen("scorecard");
2483
  });
2484
- document.getElementById("btn-retry-new-battle")?.addEventListener("click", () => {
2485
  closeRetryOverlay();
2486
  resetBattle();
2487
  });
@@ -2489,7 +2884,7 @@ document.getElementById("retry-overlay")?.addEventListener("click", (e) => {
2489
  if (e.target === e.currentTarget) closeRetryOverlay();
2490
  });
2491
 
2492
- document.getElementById("btn-view-conversation").addEventListener("click", () => {
2493
  document.getElementById("btn-end-battle").hidden = true;
2494
  document.getElementById("btn-back-scorecard").hidden = false;
2495
  document.getElementById("chat-form").hidden = true;
@@ -2497,7 +2892,7 @@ document.getElementById("btn-view-conversation").addEventListener("click", () =>
2497
  openBattleConversationLog(true);
2498
  });
2499
 
2500
- document.getElementById("btn-back-scorecard").addEventListener("click", () => {
2501
  closeRoundsDrawer("battle-rounds-drawer");
2502
  document.getElementById("btn-end-battle").hidden = false;
2503
  document.getElementById("btn-back-scorecard").hidden = true;
@@ -2540,7 +2935,7 @@ document.querySelectorAll(".difficulty-card").forEach((card) => {
2540
  });
2541
  });
2542
 
2543
- document.getElementById("chat-form").addEventListener("submit", (event) => {
2544
  event.preventDefault();
2545
  sendMessage();
2546
  });
@@ -2652,3 +3047,5 @@ if (document.readyState === "loading") {
2652
  } else {
2653
  boot();
2654
  }
 
 
 
23
  dealBattleLog: [],
24
  battleMetaSnapshot: null,
25
  scorecardSnapshot: null,
26
+ dealScorecardData: null,
27
+ voiceEntrySource: "arena",
28
  briefingMode: "quick",
29
  briefStructured: false,
30
  pitchExtractionConfidence: null,
31
+ pitchExtractionConfidenceScore: null,
32
+ lastStructuredPitchText: null,
33
+ lastStructuredPitchData: null,
34
  battleConfidencePct: null,
35
  };
36
 
 
67
  const battleStatus = document.getElementById("battle-status");
68
  const errorBanner = document.getElementById("error-banner");
69
 
70
+ function bindClick(id, handler) {
71
+ document.getElementById(id)?.addEventListener("click", handler);
72
+ }
73
+
74
  function showScreen(name) {
75
  Object.entries(screens).forEach(([key, el]) => {
76
  el.classList.toggle("active", key === name);
77
  });
78
  const app = document.getElementById("app");
79
+ app?.classList.toggle("app-arena-fullwidth", name === "battle" || name === "deal" || name === "scorecard" || name === "dealScorecard");
80
  app?.classList.toggle("app-scorecard-fullwidth", name === "scorecard");
81
  if (name === "landing" && landingIntroComplete) {
82
  finalizeLandingIntroStatic();
 
129
  step();
130
  }
131
 
132
+ /* ---- Landing currency fall canvas ---- */
133
+
134
+ const PF_FALL_BILL_COUNT = 38;
135
+
136
+ function initPfFallCanvas() {
137
+ const canvas = document.getElementById("pfFallCanvas");
138
+ const landingScreen = document.getElementById("screen-landing");
139
+ const host = canvas?.parentElement;
140
+ if (!canvas || !host || !landingScreen) return;
141
+
142
+ const ctx = canvas.getContext("2d");
143
+ if (!ctx) return;
144
+
145
+ let width = 0;
146
+ let height = 0;
147
+ let bills = [];
148
+ let rafId = 0;
149
+ let running = false;
150
+ const reducedMotion = prefersReducedMotion();
151
+
152
+ const rand = (min, max) => min + Math.random() * (max - min);
153
+
154
+ const roundRectPath = (c, x, y, w, h, r) => {
155
+ const radius = Math.min(r, w / 2, h / 2);
156
+ c.beginPath();
157
+ c.moveTo(x + radius, y);
158
+ c.lineTo(x + w - radius, y);
159
+ c.quadraticCurveTo(x + w, y, x + w, y + radius);
160
+ c.lineTo(x + w, y + h - radius);
161
+ c.quadraticCurveTo(x + w, y + h, x + w - radius, y + h);
162
+ c.lineTo(x + radius, y + h);
163
+ c.quadraticCurveTo(x, y + h, x, y + h - radius);
164
+ c.lineTo(x, y + radius);
165
+ c.quadraticCurveTo(x, y, x + radius, y);
166
+ c.closePath();
167
+ };
168
+
169
+ const createBill = () => {
170
+ const gold = Math.random() < 0.7;
171
+ return {
172
+ x: rand(0, width),
173
+ y: rand(-30, height),
174
+ scale: rand(0.5, 1.2),
175
+ vy: rand(0.25, 0.8),
176
+ vx: rand(-0.2, 0.2),
177
+ vr: rand(-0.025, 0.025),
178
+ alpha: rand(0.12, 0.35),
179
+ rot: rand(0, Math.PI * 2),
180
+ rgb: gold ? [245, 200, 66] : [0, 230, 200],
181
+ strokeAlpha: gold ? 0.35 : 0.25,
182
+ };
183
+ };
184
+
185
+ const drawBill = (bill) => {
186
+ const w = 48 * bill.scale;
187
+ const h = 20 * bill.scale;
188
+ const [r, g, b] = bill.rgb;
189
+ const inset = 3 * bill.scale;
190
+ const radius = 3 * bill.scale;
191
+
192
+ ctx.save();
193
+ ctx.translate(bill.x, bill.y);
194
+ ctx.rotate(bill.rot);
195
+ ctx.globalAlpha = bill.alpha;
196
+
197
+ roundRectPath(ctx, -w / 2, -h / 2, w, h, radius);
198
+ ctx.fillStyle = `rgba(${r},${g},${b},0.04)`;
199
+ ctx.fill();
200
+
201
+ ctx.strokeStyle = `rgba(${r},${g},${b},${bill.strokeAlpha})`;
202
+ ctx.lineWidth = 1;
203
+ ctx.stroke();
204
+
205
+ roundRectPath(ctx, -w / 2 + inset, -h / 2 + inset, w - inset * 2, h - inset * 2, Math.max(1, radius - inset));
206
+ ctx.strokeStyle = `rgba(${r},${g},${b},0.2)`;
207
+ ctx.stroke();
208
+
209
+ ctx.font = `${Math.max(8, 11 * bill.scale)}px monospace`;
210
+ ctx.fillStyle = `rgba(${r},${g},${b},0.4)`;
211
+ ctx.textAlign = "left";
212
+ ctx.textBaseline = "middle";
213
+ ctx.fillText("₹", -w / 2 + inset + 2, 0);
214
+
215
+ ctx.restore();
216
+ };
217
+
218
+ const drawFrame = () => {
219
+ ctx.clearRect(0, 0, width, height);
220
+ for (const bill of bills) {
221
+ drawBill(bill);
222
+ }
223
+ };
224
+
225
+ const tick = () => {
226
+ if (!running) return;
227
+ ctx.clearRect(0, 0, width, height);
228
+ for (const bill of bills) {
229
+ bill.y += bill.vy;
230
+ bill.x += bill.vx;
231
+ bill.rot += bill.vr;
232
+ if (bill.y > height + 30) {
233
+ bill.y = -30;
234
+ bill.x = rand(0, width);
235
+ }
236
+ drawBill(bill);
237
+ }
238
+ rafId = requestAnimationFrame(tick);
239
+ };
240
+
241
+ const resize = () => {
242
+ const rect = host.getBoundingClientRect();
243
+ const dpr = window.devicePixelRatio || 1;
244
+ width = Math.max(1, rect.width);
245
+ height = Math.max(1, rect.height);
246
+ canvas.width = Math.floor(width * dpr);
247
+ canvas.height = Math.floor(height * dpr);
248
+ canvas.style.width = `${width}px`;
249
+ canvas.style.height = `${height}px`;
250
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
251
+ bills = Array.from({ length: PF_FALL_BILL_COUNT }, () => createBill());
252
+ if (reducedMotion || !running) {
253
+ drawFrame();
254
+ }
255
+ };
256
+
257
+ const startAnimation = () => {
258
+ if (reducedMotion || running) return;
259
+ running = true;
260
+ cancelAnimationFrame(rafId);
261
+ rafId = requestAnimationFrame(tick);
262
+ };
263
+
264
+ const stopAnimation = () => {
265
+ running = false;
266
+ cancelAnimationFrame(rafId);
267
+ rafId = 0;
268
+ };
269
+
270
+ const syncVisibility = () => {
271
+ if (landingScreen.classList.contains("active")) {
272
+ resize();
273
+ startAnimation();
274
+ } else {
275
+ stopAnimation();
276
+ }
277
+ };
278
+
279
+ resize();
280
+ window.addEventListener("resize", resize);
281
+
282
+ const observer = new MutationObserver(syncVisibility);
283
+ observer.observe(landingScreen, { attributes: true, attributeFilter: ["class"] });
284
+
285
+ if (landingScreen.classList.contains("active")) {
286
+ if (reducedMotion) {
287
+ drawFrame();
288
+ } else {
289
+ startAnimation();
290
+ }
291
+ }
292
+ }
293
+
294
  function initLandingIntro() {
295
  if (landingIntroRunning || landingIntroComplete) return;
296
  landingIntroRunning = true;
 
340
  return NEMOTRON_SCORECARD_SOURCES.has(String(src ?? "").trim());
341
  }
342
 
343
+ function getScorecardSourceDisplay(data = {}) {
344
+ const src = String(data.scorecard_source ?? "").trim().toLowerCase();
345
+ const provider = String(data.provider ?? "").trim().toLowerCase();
346
+ const modelOk = data.model_ok === true;
347
+
348
+ if (isNemotronScorecardSource(src) || (modelOk && provider === "nvidia")) {
349
+ return {
350
+ show: true,
351
+ chip: "⚡ Nemotron",
352
+ badge: "Powered by NVIDIA Nemotron",
353
+ title: "Scoring source: NVIDIA Nemotron",
354
+ };
355
+ }
356
+ return { show: false };
357
+ }
358
+
359
  const INTERNAL_ERROR_PATTERNS = [
360
  /nvidia/i,
361
  /max_tokens/i,
 
430
  }
431
  closeBriefFieldExpanded(card);
432
  syncBriefToStartupForm();
433
+ recomputeBriefConfidence();
434
  }
435
 
436
  function cancelBriefFieldEdit(card) {
 
526
  }
527
 
528
  function getStartupPayload() {
529
+ if (state.briefStructured) {
530
  syncBriefToStartupForm();
531
  }
532
  const data = new FormData(startupForm);
 
544
  function setBriefingMode(mode) {
545
  state.briefingMode = mode;
546
  const isQuick = mode === "quick";
547
+ const previewVisible = isQuick && state.briefStructured;
548
 
549
  document.getElementById("tab-quick-pitch")?.classList.toggle("active", isQuick);
550
  document.getElementById("tab-advanced-briefing")?.classList.toggle("active", !isQuick);
 
552
  document.getElementById("tab-advanced-briefing")?.setAttribute("aria-selected", String(!isQuick));
553
  briefingLeftCol?.classList.toggle("mode-quick", isQuick);
554
  briefingLeftCol?.classList.toggle("mode-advanced", !isQuick);
555
+ briefingLeftCol?.classList.toggle("mode-structured", previewVisible);
556
 
557
  const advancedPanel = document.getElementById("panel-advanced-briefing");
558
  if (advancedPanel) advancedPanel.hidden = isQuick;
 
569
  const subtitle = document.getElementById("briefing-subtitle");
570
  if (subtitle) {
571
  subtitle.textContent = isQuick
572
+ ? (previewVisible
573
+ ? "Review your structured brief, then pick opponent and start."
574
+ : "Pitch naturally. We'll structure it before the judge attacks it.")
575
  : "Want full control? Edit every field manually.";
576
  }
577
  }
 
579
  function showBriefPreview(meta = {}) {
580
  if (quickPitchPanel) quickPitchPanel.hidden = true;
581
  if (briefPreviewPanel) briefPreviewPanel.hidden = false;
582
+ briefingLeftCol?.classList.add("mode-structured");
 
 
 
 
 
 
 
 
583
 
584
  const hintEl = document.getElementById("brief-preview-hint");
585
  if (hintEl) {
 
600
  state.briefStructured = false;
601
  if (quickPitchPanel) quickPitchPanel.hidden = false;
602
  if (briefPreviewPanel) briefPreviewPanel.hidden = true;
603
+ briefingLeftCol?.classList.remove("mode-structured");
604
  document.getElementById("brief-preview-hint")?.setAttribute("hidden", "");
605
  briefPreviewForm?.querySelectorAll(".brief-read-card.is-editing").forEach((card) => {
606
  commitBriefFieldEdit(card);
 
622
  return "low";
623
  }
624
 
625
+ // JS mirror of Python calculate_structure_confidence — same weights, caps, regexes.
626
+ const _CONF_FIELD_WEIGHTS = { name:10, problem:15, target_users:12, solution:15, why_ai:10, traction:15, competitors:8, ask:10 };
627
+ const _CONF_FIELD_CAPS = [["problem",60],["solution",60],["target_users",70],["traction",74],["competitors",92],["why_ai",90],["ask",85]];
628
+ const _CONF_FILLER = new Set([
629
+ "not specified","n/a","none","unknown","tbd","-","",
630
+ "idk","i don't know","i dont know","not sure","na","no idea",
631
+ "dunno","nothing","?","??","???","yes","no","nope","yep",
632
+ "to be determined","to be decided","will update","coming soon",
633
+ ]);
634
+ // Minimum word count for description fields — single-word noise like "idk" fails this.
635
+ const _CONF_MIN_WORDS = { problem:2, solution:2, why_ai:2, traction:2, target_users:2 };
636
+ const _CONF_USER_SEG_RE = /\b(college students?|university students?|indie developers?|small businesses?|enterprise|founders?|educators?|teachers?|researchers?|professionals?|teams?|parents?|teenagers?|consumers?|startup founders?)\b/i;
637
+ const _CONF_CONCRETE_ASK_RE = /(\$[\d,]+[kKmM]?|\d+[kK]\s*(?:usd|dollars?)?|mentorship|campus pilot|equity partner|co.?founder|sponsorship|strategic partner)/i;
638
+
639
+ function _calcConfidenceFromCtx(ctx, rawText) {
640
+ let score = 0;
641
+ const missing = [];
642
+ for (const [field, weight] of Object.entries(_CONF_FIELD_WEIGHTS)) {
643
+ const val = String(ctx[field] || "").trim();
644
+ const valLower = val.toLowerCase();
645
+ const minWords = _CONF_MIN_WORDS[field] ?? 1;
646
+ const filled = val && !_CONF_FILLER.has(valLower) && val.split(/\s+/).length >= minWords;
647
+ if (filled) score += weight;
648
+ else missing.push(field);
649
+ }
650
+ const text = String(rawText || "").trim();
651
+ let bonus = 0;
652
+ const numHits = (text.match(/\b\d[\d,]*\b/g) || []).length;
653
+ if (numHits >= 3) bonus += 10;
654
+ else if (numHits >= 1) bonus += 5;
655
+ if (_CONF_USER_SEG_RE.test(text)) bonus += 5;
656
+ if (_CONF_CONCRETE_ASK_RE.test(text)) bonus += 5;
657
+ bonus = Math.min(bonus, 20);
658
+ score = Math.min(score + bonus, 100);
659
+ for (const [field, cap] of _CONF_FIELD_CAPS) {
660
+ if (missing.includes(field)) score = Math.min(score, cap);
661
+ }
662
+ score = Math.max(0, Math.min(100, score));
663
+ return { confidence: score >= 75 ? "high" : score >= 45 ? "medium" : "low", confidence_score: score, missing_fields: missing };
664
+ }
665
+
666
+ function recomputeBriefConfidence() {
667
+ if (!briefPreviewForm) return;
668
+ const ctx = {};
669
+ for (const key of BRIEF_FIELD_KEYS) {
670
+ const inp = briefPreviewForm.querySelector(`[name="${key}"]`);
671
+ ctx[key] = (inp?.value || "").trim();
672
+ }
673
+ const result = _calcConfidenceFromCtx(ctx, state.lastStructuredPitchText || "");
674
+ state.pitchExtractionConfidence = result.confidence;
675
+ state.pitchExtractionConfidenceScore = result.confidence_score;
676
+ const hintEl = document.getElementById("brief-preview-hint");
677
+ if (hintEl) {
678
+ if (result.missing_fields.length) {
679
+ hintEl.textContent = `Not in your pitch: ${result.missing_fields.join(", ")}`;
680
+ hintEl.hidden = false;
681
+ } else {
682
+ hintEl.hidden = true;
683
+ }
684
+ }
685
+ }
686
+
687
  function confidenceLevelToPct(level) {
688
  const key = String(level || "medium").toLowerCase();
689
  if (key === "high") return 76;
 
694
  function syncPitchExtractionConfidence(meta = {}, startup = null) {
695
  const level = extractionConfidenceLevel(meta) || confidenceFromStartupFields(startup) || "medium";
696
  state.pitchExtractionConfidence = level;
697
+ // Prefer numeric score from backend (0-100) so battle HUD has a precise baseline.
698
+ state.pitchExtractionConfidenceScore = typeof meta.confidence_score === "number"
699
+ ? meta.confidence_score
700
+ : confidenceLevelToPct(level);
701
  return level;
702
  }
703
 
 
705
  state.briefStructured = true;
706
  syncPitchExtractionConfidence(meta, startup);
707
  fillStartupForm(startup);
708
+ syncBriefToStartupForm();
709
  showBriefPreview(meta);
710
  }
711
 
712
  function resetSetupScreen() {
713
  state.briefStructured = false;
714
  state.pitchExtractionConfidence = null;
715
+ state.pitchExtractionConfidenceScore = null;
716
+ state.lastStructuredPitchText = null;
717
+ state.lastStructuredPitchData = null;
718
  setBriefingMode("quick");
719
  hideBriefPreview();
720
  const quickText = document.getElementById("quick-pitch-text");
 
729
  showErrorBanner("Type or paste your pitch first.");
730
  return;
731
  }
732
+
733
+ // Return cached result when the same pitch text is re-submitted — confidence must not
734
+ // change on repeated clicks for the same input (Part C: stable re-structure).
735
+ if (state.lastStructuredPitchText === pitchText && state.lastStructuredPitchData) {
736
+ fillBriefPreview(state.lastStructuredPitchData.startup_context, state.lastStructuredPitchData);
737
+ hideErrorBanner();
738
+ return;
739
+ }
740
+
741
  try {
742
  setGlobalLoading(true, "Structuring your pitch…");
743
  state.startMode = "text";
 
746
  showErrorBanner(data.error || "Could not structure your pitch. Try Advanced Briefing.");
747
  return;
748
  }
749
+ state.lastStructuredPitchText = pitchText;
750
+ state.lastStructuredPitchData = data;
751
  fillBriefPreview(data.startup_context, data);
752
  hideErrorBanner();
753
  } catch (error) {
 
907
  }
908
 
909
  function refreshBattleConfidenceFromPressure(pressurePct, round = 1) {
910
+ // Use the numeric confidence_score (0-100) from structure-pitch when available so
911
+ // the battle readiness meter reflects actual brief quality, not just high/mid/low buckets.
912
+ const base = state.pitchExtractionConfidenceScore != null
913
+ ? Math.max(25, Math.min(95, state.pitchExtractionConfidenceScore))
914
+ : confidenceLevelToPct(state.pitchExtractionConfidence || "medium");
915
  const pressureDrag = Math.round(Number(pressurePct) * 0.25);
916
  const roundDrag = Math.max(0, Number(round) - 1) * 2;
917
  const ceiling = Math.max(8, base - pressureDrag - roundDrag);
 
927
  const deltas = { strong: 8, partial: -2, weak: -12, non_answer: -20 };
928
  const delta = deltas[String(quality || "").toLowerCase()] ?? 0;
929
  if (!delta) return;
930
+ const fallbackBase = state.pitchExtractionConfidenceScore != null
931
+ ? state.pitchExtractionConfidenceScore
932
+ : confidenceLevelToPct(state.pitchExtractionConfidence);
933
  state.battleConfidencePct = Math.max(
934
  8,
935
+ Math.min(92, (state.battleConfidencePct ?? fallbackBase) + delta),
936
  );
937
  renderConfidenceMeter(state.battleConfidencePct);
938
  }
 
1358
 
1359
  export async function startSession() {
1360
  try {
1361
+ if (state.briefingMode === "quick" && !state.briefStructured) {
1362
+ showErrorBanner("Structure your pitch first, or switch to Advanced Briefing.");
1363
+ return;
1364
+ }
1365
+
1366
  setGlobalLoading(true, "AI judge is preparing the first attack…");
1367
  battleStatus.hidden = true;
1368
  hideBattleReadiness();
 
1541
  state.pendingDealVoiceTurn = null;
1542
  state.startMode = "text";
1543
  state.pitchExtractionConfidence = null;
1544
+ state.pitchExtractionConfidenceScore = null;
1545
  state.battleConfidencePct = null;
1546
  state.conversationLog = [];
1547
  state.dealConversationLog = [];
1548
  state.battleLog = [];
1549
  state.dealBattleLog = [];
1550
+ state.dealScorecardData = null;
1551
  resetLiveTurnTracking();
1552
  chatWindow.innerHTML = "";
1553
  if (dealChatWindow) dealChatWindow.innerHTML = "";
 
1796
  const { strongest, weakest } = getStrongestWeakest(scores);
1797
  const se = data.score_explanation ?? {};
1798
  const explanation = se;
1799
+ const sourceDisplay = getScorecardSourceDisplay(data);
1800
+
1801
+ const chipSource = document.getElementById("chip-score-source");
1802
+ const sourceBadgeEl = document.getElementById("scorecard-source-badge");
1803
+ if (chipSource) {
1804
+ if (sourceDisplay.show) {
1805
+ chipSource.textContent = sourceDisplay.chip;
1806
+ chipSource.title = sourceDisplay.title ?? "";
1807
+ chipSource.hidden = false;
1808
+ } else {
1809
+ chipSource.textContent = "";
1810
+ chipSource.hidden = true;
1811
+ }
1812
+ }
1813
+ if (sourceBadgeEl) {
1814
+ sourceBadgeEl.hidden = true;
1815
+ sourceBadgeEl.textContent = "";
1816
+ }
1817
+ if (!sourceDisplay.show && data.fallback_reason) {
1818
+ console.info("Scorecard used local fallback:", data.fallback_reason, data.model_error || "");
1819
+ }
1820
 
1821
  const opponent = PERSONA_LABELS[state.persona] ?? data.opponent ?? "AI Judge";
1822
  const mode = DIFFICULTY_LABELS[state.difficultyProfile]
 
1851
  : "↓ Weakest: —";
1852
  }
1853
 
 
 
 
 
 
 
 
 
 
 
1854
  const fallbackWarnEl = document.getElementById("scorecard-fallback-warning");
1855
  if (fallbackWarnEl) {
1856
  fallbackWarnEl.textContent = "";
 
1947
  if (answersEmpty) answersEmpty.hidden = !noAnswers;
1948
  if (answersContent) answersContent.hidden = noAnswers;
1949
 
1950
+ if (noAnswers) {
1951
+ if (whyScoredEl && !whyScoredEl.textContent?.trim()) {
1952
+ whyScoredEl.textContent =
1953
+ "You ended before responding, so the scorecard can only grade the startup brief.";
1954
+ }
1955
+ if (whatStoppedEl && !whatStoppedEl.textContent?.trim()) {
1956
+ whatStoppedEl.textContent = "Submit at least one battle answer to unlock answer-level coaching.";
1957
+ }
1958
+ if (nextText && !nextText.textContent?.trim()) {
1959
+ nextText.textContent = "Start a new battle and defend at least one question.";
1960
+ }
1961
+ const prepPanel = document.querySelector('.sc-tab-panel[data-panel="prep"]');
1962
+ if (prepPanel) prepPanel.classList.add("sc-prep-empty");
1963
+ } else {
1964
+ document.querySelector('.sc-tab-panel[data-panel="prep"]')?.classList.remove("sc-prep-empty");
1965
+ }
1966
+
1967
  state.scoreExplanation = data.score_explanation ?? null;
1968
 
1969
  const pathBtn = document.getElementById("btn-path-to-80");
 
1988
  weakest: weakest ? formatDimLabel(weakest[0]) : null,
1989
  roundsCompleted: state.battleLog?.length ?? 0,
1990
  };
1991
+
1992
+ updateScorecardCrossNav();
1993
  }
1994
 
1995
  const DEAL_TYPE_LABELS = {
 
2011
  .replace(/Deal Round/i, "Negotiation");
2012
  }
2013
 
2014
+ function canContinueToDealFromVerdict(verdict) {
2015
+ if (!verdict) return false;
2016
+ if (verdict.can_continue_to_deal) return true;
2017
+ const interest = verdict.interest_level || "";
2018
+ if ((interest === "mild_interest" || interest === "strong_interest") && verdict.deal_type && verdict.deal_type !== "none") {
2019
+ return true;
2020
+ }
2021
+ return false;
2022
+ }
2023
+
2024
+ function openVerdictModal() {
2025
+ const overlay = document.getElementById("verdict-overlay");
2026
+ if (!overlay || !state.judgeVerdict?.interest_level) return;
2027
+ overlay.hidden = false;
2028
+ document.body.style.overflow = "hidden";
2029
+ }
2030
+
2031
+ function closeVerdictModal() {
2032
+ const overlay = document.getElementById("verdict-overlay");
2033
+ if (overlay) overlay.hidden = true;
2034
+ document.body.style.overflow = "";
2035
+ }
2036
+
2037
  function renderJudgeVerdict(verdict) {
2038
+ const verdictBtn = document.getElementById("btn-view-judge-verdict");
2039
  if (!verdict || !verdict.interest_level) {
2040
+ if (verdictBtn) verdictBtn.hidden = true;
2041
  return;
2042
  }
2043
+ if (verdictBtn) verdictBtn.hidden = false;
2044
 
2045
  const interest = verdict.interest_level || "no_interest";
2046
  const personaLine = `${verdict.persona_name || "Judge"} — ${verdict.persona_type || ""}`.trim();
 
2069
  }
2070
  }
2071
 
2072
+ const lockedMsg = document.getElementById("verdict-deal-locked-msg");
2073
+ const dealAvailable = canContinueToDealFromVerdict(verdict);
2074
+ if (lockedMsg) {
2075
+ lockedMsg.hidden = dealAvailable || interest === "verdict_only";
2076
+ }
2077
+
2078
  const actions = document.getElementById("verdict-actions");
2079
  if (!actions) return;
2080
  actions.innerHTML = "";
2081
 
2082
+ if (dealAvailable) {
2083
  const btn = document.createElement("button");
2084
+ btn.className = "btn btn-deal-continue sc-btn-gold";
2085
+ btn.textContent = verdictNegotiationCta(verdict.next_step_label, "Continue to Deal Phase →");
2086
  btn.type = "button";
2087
+ btn.addEventListener("click", () => {
2088
+ closeVerdictModal();
2089
+ startDealPhase();
2090
+ });
2091
  actions.appendChild(btn);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2092
  }
2093
  }
2094
 
 
2264
  }
2265
  }
2266
 
2267
+ function updateScorecardCrossNav() {
2268
+ const btn = document.getElementById("btn-view-deal-scorecard");
2269
+ if (!btn) return;
2270
+ btn.hidden = !state.dealScorecardData;
2271
+ }
2272
+
2273
+ function showDealScorecardScreen() {
2274
+ if (!state.dealScorecardData) return;
2275
+ renderDealScorecard(state.dealScorecardData);
2276
+ showScreen("dealScorecard");
2277
+ }
2278
+
2279
  function renderDealScorecard(data) {
2280
+ state.dealScorecardData = data;
2281
+ updateScorecardCrossNav();
2282
+
2283
  const combined = data.combined_scorecard || {};
2284
  const deal = data.deal_scorecard || {};
2285
 
 
2455
  || (vd.delivery_notes ?? []).find((n) => String(n).trim()) || "";
2456
 
2457
  content.innerHTML = `
2458
+ <div class="voice-delivery-layout">
2459
+ <div class="voice-delivery-metrics" role="list">
2460
+ <div class="voice-delivery-box" role="listitem">
2461
+ <span class="voice-delivery-box-label">Voice turns</span>
2462
+ <strong class="voice-delivery-box-value">${vd.total_voice_turns ?? 0}</strong>
2463
+ </div>
2464
+ <div class="voice-delivery-box" role="listitem">
2465
+ <span class="voice-delivery-box-label">Filler words</span>
2466
+ <strong class="voice-delivery-box-value">${vd.total_filler_words ?? 0}</strong>
2467
+ </div>
2468
+ <div class="voice-delivery-box" role="listitem">
2469
+ <span class="voice-delivery-box-label">Common fillers</span>
2470
+ <strong class="voice-delivery-box-value">${escapeHtml(fillers)}</strong>
2471
+ </div>
2472
+ <div class="voice-delivery-box" role="listitem">
2473
+ <span class="voice-delivery-box-label">Pace</span>
2474
+ <strong class="voice-delivery-box-value">${escapeHtml(vd.average_pace ?? "—")}</strong>
2475
+ </div>
2476
+ </div>
2477
+ ${overallNote ? `<p class="voice-delivery-overall">${escapeHtml(overallNote)}</p>` : ""}
2478
  </div>
 
2479
  `;
2480
 
2481
  const notes = (vd.delivery_notes ?? []).filter((n) => {
 
2639
 
2640
  function renderRetryResult(data) {
2641
  const comp = data.comparison ?? {};
2642
+ const proj = data.projection ?? {};
2643
  document.getElementById("retry-result-old").textContent = comp.old_answer_summary ?? data.original_answer ?? "";
2644
  document.getElementById("retry-result-new").textContent = comp.new_answer_summary ?? data.retry_answer ?? "";
2645
  document.getElementById("retry-what-improved").textContent = comp.what_improved ?? "";
2646
  document.getElementById("retry-still-missing").textContent = comp.still_missing ?? "";
2647
  document.getElementById("retry-specific-tip").textContent = comp.specific_tip ?? "";
2648
 
2649
+ // Prefer normalized projection fields; fall back to legacy comparison fields.
2650
+ const before = proj.old_dimension_score ?? comp.estimated_dimension_before ?? 0;
2651
+ const after = proj.new_dimension_score ?? comp.estimated_dimension_after ?? before;
2652
  document.getElementById("retry-dim-estimate").textContent =
2653
  `${formatDimLabel(data.dimension)}: ${before} → ${after}`;
2654
+
2655
+ // Safety floor: never display a baseline lower than what the scorecard shows.
2656
+ // If the backend returns a projection baseline below the visible scorecard overall,
2657
+ // clamp up to the scorecard snapshot so the user never sees "score went down".
2658
+ const snapshotOverall = state.scorecardSnapshot?.overall ?? null;
2659
+ const rawOriginalOverall = proj.original_overall_score ?? snapshotOverall ?? "—";
2660
+ const originalOverall = (
2661
+ typeof rawOriginalOverall === "number" &&
2662
+ typeof snapshotOverall === "number" &&
2663
+ rawOriginalOverall < snapshotOverall
2664
+ ) ? snapshotOverall : rawOriginalOverall;
2665
+
2666
+ const rawProjected = proj.projected_overall_score ?? originalOverall;
2667
+ const projectedOverall = (
2668
+ typeof rawProjected === "number" && typeof originalOverall === "number"
2669
+ ) ? Math.max(rawProjected, originalOverall) : rawProjected;
2670
+
2671
+ const projectedDelta = (
2672
+ typeof projectedOverall === "number" && typeof originalOverall === "number"
2673
+ ) ? Math.max(0, projectedOverall - originalOverall) : (proj.projected_overall_delta ?? 0);
2674
+
2675
+ const liftEl = document.getElementById("retry-overall-lift");
2676
+ if (liftEl) {
2677
+ if (projectedDelta > 0) {
2678
+ liftEl.textContent = `Projected overall: ${originalOverall} → ${projectedOverall} (+${projectedDelta})`;
2679
+ } else {
2680
+ liftEl.textContent = "No projected lift yet — keep practicing this dimension.";
2681
+ }
2682
+ }
2683
+
2684
+ const noteEl = document.getElementById("retry-projection-note");
2685
+ if (noteEl) {
2686
+ noteEl.textContent = "Training projection only — your original scorecard stays unchanged.";
2687
+ noteEl.hidden = false;
2688
+ }
2689
 
2690
  const verdictEl = document.getElementById("retry-verdict-badge");
2691
+ const verdict = projectedDelta > 0
2692
+ ? (projectedDelta >= 3 ? "improved" : "slightly_improved")
2693
+ : (comp.verdict ?? "needs_more_work");
2694
  const verdictLabels = {
2695
  improved: "Improved",
2696
  slightly_improved: "Slightly improved",
2697
+ needs_more_work: "No lift yet",
2698
  };
2699
  if (verdictEl) {
2700
  verdictEl.textContent = verdictLabels[verdict] ?? verdict;
 
2736
  return;
2737
  }
2738
  renderRetryResult(data);
 
 
 
 
 
 
 
 
2739
  hideErrorBanner();
2740
  } catch (error) {
2741
  console.error(error);
 
2755
  startRetryDrill();
2756
  }
2757
 
2758
+ bindClick("btn-load-sample", loadSample);
2759
+ bindClick("btn-load-sample-setup", loadSample);
2760
+ bindClick("btn-go-setup", () => {
2761
+ showScreen("startMethod");
 
2762
  });
2763
+ bindClick("btn-back-landing", () => {
2764
  resetSetupScreen();
2765
  showScreen("landing");
2766
  });
2767
+ bindClick("tab-quick-pitch", () => setBriefingMode("quick"));
2768
+ bindClick("tab-advanced-briefing", () => setBriefingMode("advanced"));
2769
+ bindClick("btn-structure-pitch", structurePitch);
2770
+ bindClick("btn-record-voice-setup", () => {
2771
  state.startMode = "voice";
2772
+ state.voiceEntrySource = "briefing";
2773
  showScreen("voicePitch");
2774
  });
2775
+ bindClick("btn-looks-good-start", () => {
2776
  syncBriefToStartupForm();
2777
  startSession();
2778
  });
2779
+ bindClick("btn-restructure-pitch", () => {
2780
  hideBriefPreview();
2781
  document.getElementById("quick-pitch-text")?.focus();
2782
  });
2783
+ bindClick("btn-start-back-landing", () => showScreen("landing"));
2784
 
2785
+ bindClick("btn-start-text", () => {
2786
  state.startMode = "text";
2787
+ state.voiceEntrySource = "arena";
2788
+ resetSetupScreen();
2789
+ showScreen("setup");
2790
  });
2791
+
2792
+ bindClick("btn-start-voice", () => {
2793
  state.startMode = "voice";
2794
+ state.voiceEntrySource = "arena";
2795
+ showScreen("voicePitch");
2796
  });
2797
+
2798
+ bindClick("btn-voice-pitch-back", () => {
2799
+ if (state.voiceEntrySource === "briefing") {
2800
+ showScreen("setup");
2801
+ return;
2802
+ }
2803
+ showScreen("startMethod");
2804
  });
2805
+ bindClick("btn-voice-edit-manual", () => {
 
2806
  const form = document.getElementById("voice-extract-form");
2807
  const data = new FormData(form);
2808
  applyVoicePitchToBriefing({
 
2813
  setBriefingMode("advanced");
2814
  showScreen("setup");
2815
  });
2816
+ bindClick("btn-voice-looks-right", () => {
2817
  const form = document.getElementById("voice-extract-form");
2818
  const data = new FormData(form);
2819
  applyVoicePitchToBriefing({
 
2824
  });
2825
  showScreen("setup");
2826
  });
2827
+ bindClick("btn-voice-turn-send", () => {
2828
  const transcript = document.getElementById("voice-turn-transcript")?.value?.trim();
2829
  if (!transcript || !state.pendingVoiceTurn) return;
2830
  sendMessage(transcript, {
 
2836
  },
2837
  });
2838
  });
2839
+ bindClick("btn-start-battle", startSession);
2840
+ bindClick("btn-end-battle", endBattle);
2841
+ bindClick("btn-reset", resetBattle);
2842
+ bindClick("btn-back-setup", () => showScreen("setup"));
2843
+ bindClick("btn-path-to-80", openPath80);
2844
+ bindClick("btn-close-path80", closePath80);
2845
+ bindClick("btn-close-path80-bottom", closePath80);
2846
+ bindClick("btn-retry-question", openRetryFromPath80);
2847
+ bindClick("btn-scorecard-retry", startRetryDrill);
2848
+ bindClick("btn-prep-retry", startRetryDrill);
2849
+ bindClick("btn-answers-retry", startRetryDrill);
2850
+ bindClick("btn-answers-new-battle", resetBattle);
2851
+ bindClick("btn-view-judge-verdict", openVerdictModal);
2852
+ bindClick("btn-view-deal-scorecard", showDealScorecardScreen);
2853
+ bindClick("btn-close-verdict", closeVerdictModal);
2854
+ bindClick("btn-close-verdict-bottom", closeVerdictModal);
2855
+ bindClick("btn-verdict-retry", () => {
2856
+ closeVerdictModal();
2857
+ startRetryDrill();
2858
+ });
2859
+ bindClick("btn-verdict-new-battle", () => {
2860
+ closeVerdictModal();
2861
+ resetBattle();
2862
+ });
2863
+ document.getElementById("path80-overlay")?.addEventListener("click", (e) => {
2864
  if (e.target === e.currentTarget) closePath80();
2865
  });
2866
+ document.getElementById("verdict-overlay")?.addEventListener("click", (e) => {
2867
+ if (e.target === e.currentTarget) closeVerdictModal();
2868
+ });
2869
 
2870
+ bindClick("btn-close-retry", closeRetryOverlay);
2871
+ bindClick("btn-submit-retry", submitRetryAnswer);
2872
+ bindClick("btn-retry-again", () => {
2873
  if (state.retryDrill) populateRetryDrill(state.retryDrill);
2874
  });
2875
+ bindClick("btn-retry-back-scorecard", () => {
2876
  closeRetryOverlay();
2877
  showScreen("scorecard");
2878
  });
2879
+ bindClick("btn-retry-new-battle", () => {
2880
  closeRetryOverlay();
2881
  resetBattle();
2882
  });
 
2884
  if (e.target === e.currentTarget) closeRetryOverlay();
2885
  });
2886
 
2887
+ bindClick("btn-view-conversation", () => {
2888
  document.getElementById("btn-end-battle").hidden = true;
2889
  document.getElementById("btn-back-scorecard").hidden = false;
2890
  document.getElementById("chat-form").hidden = true;
 
2892
  openBattleConversationLog(true);
2893
  });
2894
 
2895
+ bindClick("btn-back-scorecard", () => {
2896
  closeRoundsDrawer("battle-rounds-drawer");
2897
  document.getElementById("btn-end-battle").hidden = false;
2898
  document.getElementById("btn-back-scorecard").hidden = true;
 
2935
  });
2936
  });
2937
 
2938
+ document.getElementById("chat-form")?.addEventListener("submit", (event) => {
2939
  event.preventDefault();
2940
  sendMessage();
2941
  });
 
3047
  } else {
3048
  boot();
3049
  }
3050
+
3051
+ initPfFallCanvas();
frontend/styles.css CHANGED
@@ -26,9 +26,18 @@
26
  box-sizing: border-box;
27
  }
28
 
 
 
 
 
 
 
29
  body {
30
  margin: 0;
31
  min-height: 100vh;
 
 
 
32
  font-family: "Segoe UI", system-ui, sans-serif;
33
  color: var(--text);
34
  background: #09090f;
@@ -47,9 +56,10 @@ body {
47
 
48
  .app {
49
  position: relative;
50
- max-width: 1100px;
 
51
  margin: 0 auto;
52
- padding: 2rem 1rem 3rem;
53
  }
54
 
55
  .app:has(#screen-battle.active),
@@ -123,7 +133,8 @@ h1 {
123
  .arena-landing {
124
  position: relative;
125
  width: 100vw;
126
- min-height: calc(100vh - 2rem);
 
127
  margin-left: calc(50% - 50vw);
128
  margin-right: calc(50% - 50vw);
129
  overflow: hidden;
@@ -736,7 +747,7 @@ h1 {
736
  position: relative;
737
  z-index: 2;
738
  text-align: center;
739
- padding: 2rem 1.25rem 2.5rem;
740
  max-width: 820px;
741
  width: 100%;
742
  }
@@ -766,8 +777,8 @@ h1 {
766
  }
767
 
768
  .arena-title {
769
- margin: 0 0 1.1rem;
770
- font-size: clamp(2.85rem, 8.5vw, 4.5rem);
771
  font-weight: 800;
772
  letter-spacing: 0.03em;
773
  line-height: 1.02;
@@ -780,8 +791,8 @@ h1 {
780
 
781
  /* Pass 2 — hook lines */
782
  .arena-hook {
783
- min-height: 4.5rem;
784
- margin-bottom: 0.9rem;
785
  }
786
 
787
  .arena-hook-line {
@@ -815,7 +826,7 @@ h1 {
815
 
816
  .arena-support {
817
  max-width: 520px;
818
- margin: 0 auto 1.35rem;
819
  font-size: clamp(0.88rem, 2vw, 0.95rem);
820
  line-height: 1.5;
821
  color: var(--text-muted);
@@ -892,7 +903,8 @@ h1 {
892
  flex-wrap: wrap;
893
  gap: 0.4rem;
894
  justify-content: center;
895
- margin-top: 1.25rem;
 
896
  opacity: 0;
897
  transform: translateY(8px);
898
  transition: opacity 0.5s ease 0.12s, transform 0.5s ease 0.12s;
@@ -922,8 +934,9 @@ h1 {
922
  }
923
 
924
  .arena-landing-footer {
925
- margin-top: 1.75rem;
926
- padding-top: 1.1rem;
 
927
  border-top: 1px solid rgba(255, 255, 255, 0.1);
928
  opacity: 0;
929
  transition: opacity 0.5s ease 0.2s;
@@ -1209,14 +1222,14 @@ textarea {
1209
 
1210
  .persona-grid {
1211
  display: grid;
1212
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
1213
- gap: 0.85rem;
1214
- margin-top: 1rem;
1215
  }
1216
 
1217
  .persona-card {
1218
  text-align: left;
1219
- padding: 1rem;
1220
  border-radius: 14px;
1221
  border: 1px solid rgba(255, 255, 255, 0.08);
1222
  background: rgba(0, 0, 0, 0.22);
@@ -1243,7 +1256,7 @@ textarea {
1243
  }
1244
 
1245
  .difficulty-selector {
1246
- margin-top: 1.5rem;
1247
  }
1248
 
1249
  .difficulty-selector h3 {
@@ -1256,13 +1269,13 @@ textarea {
1256
 
1257
  .difficulty-grid {
1258
  display: grid;
1259
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1260
- gap: 0.75rem;
1261
  }
1262
 
1263
  .difficulty-card {
1264
  text-align: left;
1265
- padding: 0.85rem 1rem;
1266
  border-radius: 12px;
1267
  background: rgba(255, 255, 255, 0.04);
1268
  border: 1px solid var(--border);
@@ -1635,8 +1648,8 @@ textarea {
1635
  position: relative;
1636
  z-index: 1;
1637
  width: min(1180px, 100%);
1638
- height: min(92dvh, 820px);
1639
- max-height: calc(100dvh - 24px);
1640
  display: flex;
1641
  flex-direction: column;
1642
  border: 1px solid rgba(78, 205, 196, 0.2);
@@ -2832,14 +2845,14 @@ body.rounds-drawer-open {
2832
  .battle-arena.arena-shell {
2833
  position: relative;
2834
  z-index: 1;
2835
- width: min(1440px, calc(100vw - 48px));
2836
- max-width: 1440px;
2837
  min-width: 0;
2838
  margin: 0 auto;
2839
- min-height: calc(100vh - 64px);
2840
  display: grid;
2841
  grid-template-columns: 280px minmax(0, 1fr);
2842
- gap: 20px;
2843
  align-items: stretch;
2844
  }
2845
 
@@ -2848,21 +2861,37 @@ body.rounds-drawer-open {
2848
  margin: 0 auto;
2849
  }
2850
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2851
  .briefing-shell-wide {
2852
- max-width: 1240px;
 
 
2853
  }
2854
 
2855
  .briefing-mode-tabs {
2856
  display: flex;
2857
- gap: 0.5rem;
2858
- margin-bottom: 1rem;
2859
  flex-wrap: wrap;
2860
  }
2861
 
2862
  .briefing-mode-tab {
2863
  flex: 1;
2864
  min-width: 140px;
2865
- padding: 0.85rem 1.25rem;
2866
  border-radius: 12px;
2867
  border: 1px solid rgba(255, 255, 255, 0.1);
2868
  background: rgba(0, 0, 0, 0.25);
@@ -2887,21 +2916,39 @@ body.rounds-drawer-open {
2887
  }
2888
 
2889
  .briefing-grid-wide {
2890
- grid-template-columns: minmax(0, 1.35fr) minmax(300px, 0.65fr);
2891
- gap: 1.25rem;
2892
  align-items: stretch;
2893
  }
2894
 
2895
  .briefing-left-col {
2896
- display: flex;
2897
- flex-direction: column;
2898
- gap: 0;
2899
  min-width: 0;
2900
  overflow: visible;
2901
  }
2902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2903
  .quick-pitch-panel {
2904
- padding: 1.35rem 1.5rem;
2905
  }
2906
 
2907
  .quick-pitch-label {
@@ -2915,15 +2962,16 @@ body.rounds-drawer-open {
2915
 
2916
  .quick-pitch-textarea {
2917
  width: 100%;
2918
- min-height: 200px;
2919
- padding: 1.1rem 1.2rem;
2920
- border-radius: 14px;
 
2921
  border: 1px solid rgba(255, 255, 255, 0.12);
2922
  background: rgba(0, 0, 0, 0.35);
2923
  color: var(--text);
2924
- font-size: 1rem;
2925
- line-height: 1.55;
2926
- resize: vertical;
2927
  box-shadow: inset 0 2px 12px rgba(0, 0, 0, 0.25);
2928
  }
2929
 
@@ -2942,8 +2990,8 @@ body.rounds-drawer-open {
2942
  .quick-pitch-actions {
2943
  display: flex;
2944
  flex-direction: column;
2945
- gap: 0.65rem;
2946
- margin-top: 1rem;
2947
  }
2948
 
2949
  .quick-pitch-actions .btn-wide {
@@ -2963,15 +3011,37 @@ body.rounds-drawer-open {
2963
  }
2964
 
2965
  .quick-pitch-hint {
2966
- margin: 0.85rem 0 0;
2967
- font-size: 0.85rem;
2968
  color: rgba(244, 211, 94, 0.85);
2969
  }
2970
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2971
  .brief-preview-panel {
2972
- padding: 1.15rem 1.25rem;
2973
  overflow: visible;
2974
  position: relative;
 
 
 
 
2975
  }
2976
 
2977
  .brief-preview-panel.is-editing-active::before {
@@ -2985,7 +3055,7 @@ body.rounds-drawer-open {
2985
  }
2986
 
2987
  .brief-preview-header {
2988
- margin-bottom: 0.75rem;
2989
  }
2990
 
2991
  .brief-preview-title-row {
@@ -3001,53 +3071,27 @@ body.rounds-drawer-open {
3001
  }
3002
 
3003
  .brief-preview-helper {
3004
- margin: 0.4rem 0 0;
3005
  color: var(--muted);
3006
- font-size: 0.85rem;
3007
  }
3008
 
3009
  .brief-preview-hint {
3010
- margin: 0.5rem 0 0;
3011
- font-size: 0.82rem;
3012
  color: rgba(244, 211, 94, 0.85);
3013
  }
3014
 
3015
- .brief-confidence-chip {
3016
- display: inline-block;
3017
- padding: 0.25rem 0.65rem;
3018
- border-radius: 999px;
3019
- font-size: 0.72rem;
3020
- font-weight: 700;
3021
- letter-spacing: 0.06em;
3022
- text-transform: uppercase;
3023
- border: 1px solid rgba(255, 255, 255, 0.12);
3024
- color: var(--muted);
3025
- }
3026
-
3027
- .brief-confidence-chip.confidence-high {
3028
- border-color: rgba(94, 244, 160, 0.45);
3029
- color: #7dffb8;
3030
- }
3031
-
3032
- .brief-confidence-chip.confidence-medium {
3033
- border-color: rgba(244, 211, 94, 0.45);
3034
- color: var(--gold);
3035
- }
3036
-
3037
- .brief-confidence-chip.confidence-low {
3038
- border-color: rgba(255, 140, 120, 0.45);
3039
- color: #ffb09a;
3040
- }
3041
-
3042
  .brief-preview-grid {
3043
  display: grid;
3044
  grid-template-columns: repeat(2, minmax(0, 1fr));
3045
- gap: 0.55rem;
 
3046
  }
3047
 
3048
  .brief-read-card {
3049
- padding: 0.65rem 0.8rem;
3050
- border-radius: 12px;
3051
  border: 1px solid rgba(255, 255, 255, 0.08);
3052
  background: rgba(0, 0, 0, 0.22);
3053
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s, transform 0.2s;
@@ -3207,8 +3251,9 @@ body.rounds-drawer-open {
3207
  .brief-preview-actions {
3208
  display: flex;
3209
  flex-direction: column;
3210
- gap: 0.55rem;
3211
- margin-top: 0.85rem;
 
3212
  }
3213
 
3214
  .brief-preview-actions .btn-wide {
@@ -3216,13 +3261,116 @@ body.rounds-drawer-open {
3216
  }
3217
 
3218
  .advanced-briefing-panel {
3219
- padding: 1.25rem 1.35rem;
 
 
 
 
3220
  }
3221
 
3222
- .advanced-briefing-helper {
3223
- margin: 0 0 1rem;
3224
- color: var(--muted);
3225
- font-size: 0.88rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3226
  }
3227
 
3228
  .briefing-opponent-note {
@@ -3238,15 +3386,17 @@ body.rounds-drawer-open {
3238
  flex-direction: column;
3239
  align-self: stretch;
3240
  min-height: 100%;
 
3241
  gap: 0;
 
3242
  }
3243
 
3244
  .briefing-opponent-footer {
3245
  margin-top: auto;
3246
- padding-top: 1.5rem;
3247
  display: flex;
3248
  flex-direction: column;
3249
- gap: 0.75rem;
3250
  }
3251
 
3252
  .briefing-opponent-panel .btn-arena-start {
@@ -3262,7 +3412,7 @@ body.rounds-drawer-open {
3262
  justify-content: space-between;
3263
  align-items: flex-start;
3264
  gap: 1rem;
3265
- margin-bottom: 1rem;
3266
  }
3267
 
3268
  .briefing-title {
@@ -4697,8 +4847,8 @@ body.rounds-drawer-open {
4697
 
4698
  .voice-delivery-summary {
4699
  display: grid;
4700
- grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
4701
- gap: 0.55rem;
4702
  }
4703
 
4704
  .voice-wave-decor {
@@ -5760,41 +5910,160 @@ body.rounds-drawer-open {
5760
  .outcome-strong_win { background: rgba(74, 222, 128, 0.18); color: #86efac; }
5761
  .outcome-weak_concession { background: rgba(248, 113, 113, 0.18); color: #fca5a5; }
5762
 
5763
- /* Voice mode — Phase 7 */
5764
- .start-method-grid {
5765
- display: grid;
5766
- grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
5767
- gap: 0.85rem;
5768
- margin: 1rem 0 1.25rem;
5769
  }
5770
 
5771
- .start-method-card {
5772
- text-align: left;
5773
- padding: 1.1rem;
5774
- border-radius: 12px;
5775
- border: 1px solid rgba(255, 255, 255, 0.08);
5776
- background: rgba(0, 0, 0, 0.2);
5777
- color: inherit;
5778
- cursor: pointer;
5779
- transition: border-color 0.2s, box-shadow 0.2s;
5780
  }
5781
 
5782
- .start-method-card.selected {
5783
- border-color: rgba(125, 211, 252, 0.45);
5784
- box-shadow: 0 0 20px rgba(125, 211, 252, 0.12);
 
 
 
5785
  }
5786
 
5787
- .start-method-card h3 {
5788
  margin: 0.35rem 0 0.25rem;
5789
- font-size: 1rem;
5790
  }
5791
 
5792
- .start-method-card p {
5793
  margin: 0;
5794
  color: var(--muted);
5795
- font-size: 0.85rem;
5796
- line-height: 1.4;
5797
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5798
 
5799
  .start-method-icon {
5800
  font-size: 1.4rem;
@@ -5993,41 +6262,108 @@ body.rounds-drawer-open {
5993
  margin-top: 0.5rem;
5994
  }
5995
 
5996
- .voice-delivery-grid {
 
 
 
 
 
 
 
 
 
 
5997
  display: grid;
5998
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
5999
- gap: 0.65rem;
6000
  }
6001
 
6002
- .voice-delivery-stat {
6003
- padding: 0.65rem 0.75rem;
6004
- border-radius: 8px;
6005
- background: rgba(125, 211, 252, 0.06);
6006
- border: 1px solid rgba(125, 211, 252, 0.15);
 
6007
  }
6008
 
6009
- .voice-delivery-stat span {
 
 
 
 
 
 
 
 
6010
  display: block;
6011
- font-size: 0.72rem;
6012
- color: var(--muted);
 
6013
  text-transform: uppercase;
6014
- letter-spacing: 0.03em;
6015
  }
6016
 
6017
- .voice-delivery-stat strong {
6018
  display: block;
6019
- margin-top: 0.25rem;
6020
- font-size: 0.9rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6021
  }
6022
 
6023
  .voice-delivery-notes {
6024
- grid-column: 1 / -1;
6025
- margin: 0.5rem 0 0;
6026
- padding-left: 1.1rem;
6027
- font-size: 0.88rem;
 
 
6028
  color: var(--muted);
6029
  }
6030
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6031
  .voice-overall-feedback {
6032
  grid-column: 1 / -1;
6033
  margin: 0.5rem 0 0;
@@ -7219,17 +7555,17 @@ body.rounds-drawer-open {
7219
  ================================ */
7220
 
7221
  .sc-shell {
7222
- width: 100%;
7223
- max-width: none;
7224
- margin: 0;
7225
- padding: 24px 32px;
7226
  }
7227
 
7228
  /* ---- Hero ---- */
7229
  .sc-hero {
7230
- padding: 20px 24px;
7231
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
7232
- margin: 0 -8px 0;
7233
  }
7234
 
7235
  .sc-hero-row {
@@ -7238,21 +7574,41 @@ body.rounds-drawer-open {
7238
  align-items: flex-start;
7239
  }
7240
 
7241
- .sc-hero-conversation {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7242
  margin-left: auto;
7243
- flex-shrink: 0;
7244
- align-self: center;
7245
  }
7246
 
7247
  .sc-ring-wrap {
7248
  flex-shrink: 0;
 
 
 
 
 
7249
  }
7250
 
7251
  .sc-ring {
7252
- width: 80px;
7253
- height: 80px;
7254
- min-width: 80px;
7255
- min-height: 80px;
7256
  border-radius: 50%;
7257
  border: 3px solid #f5c842;
7258
  background: rgba(245, 200, 66, 0.05);
@@ -7261,22 +7617,28 @@ body.rounds-drawer-open {
7261
  align-items: center;
7262
  justify-content: center;
7263
  box-shadow: none;
 
7264
  }
7265
 
7266
  .sc-ring .score-orb-value {
7267
- font-size: 28px;
7268
  font-weight: 800;
7269
  line-height: 1;
7270
  color: #f5c842;
7271
  }
7272
 
7273
- .sc-ring .score-orb-label {
7274
- margin-top: 2px;
7275
- font-size: 9px;
 
 
 
7276
  font-weight: 800;
7277
- letter-spacing: 0.1em;
 
7278
  text-transform: uppercase;
7279
- color: rgba(245, 200, 66, 0.85);
 
7280
  }
7281
 
7282
  .sc-hero-meta {
@@ -7307,25 +7669,105 @@ body.rounds-drawer-open {
7307
  display: flex;
7308
  flex-wrap: wrap;
7309
  gap: 8px;
7310
- margin-top: 12px;
7311
  }
7312
 
7313
- .sc-btn-conversation {
7314
- padding: 14px 22px;
7315
- font-size: 14px;
7316
- font-weight: 800;
 
7317
  letter-spacing: 0.04em;
7318
  text-transform: uppercase;
7319
- border-radius: 12px;
7320
- border: 2px solid rgba(94, 211, 244, 0.75);
7321
- background: rgba(94, 211, 244, 0.12);
 
 
 
 
 
 
 
 
7322
  color: #5ed3f4;
7323
- box-shadow:
7324
- 0 0 24px rgba(94, 211, 244, 0.22),
7325
- inset 0 0 16px rgba(94, 211, 244, 0.08);
7326
- cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7327
  white-space: nowrap;
7328
- transition: border-color 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7329
  }
7330
 
7331
  .sc-btn-conversation:hover {
@@ -7378,15 +7820,19 @@ body.rounds-drawer-open {
7378
  /* ---- Body grid ---- */
7379
  .sc-body-grid {
7380
  display: grid;
7381
- grid-template-columns: minmax(320px, 380px) 1fr;
7382
- min-height: 400px;
7383
  width: 100%;
 
 
7384
  }
7385
 
7386
  .sc-left-col {
7387
- padding: 18px 20px;
7388
  border-right: 1px solid rgba(255, 255, 255, 0.06);
7389
  min-width: 0;
 
 
 
7390
  }
7391
 
7392
  .sc-section-label {
@@ -7458,8 +7904,8 @@ body.rounds-drawer-open {
7458
  .sc-dim-list .dimension-row-scorecard {
7459
  display: flex;
7460
  flex-direction: column;
7461
- gap: 10px;
7462
- padding: 12px 14px;
7463
  border: 1px solid rgba(255, 255, 255, 0.08);
7464
  border-radius: 12px;
7465
  background: rgba(255, 255, 255, 0.03);
@@ -7636,7 +8082,6 @@ body.rounds-drawer-open {
7636
  min-width: 0;
7637
  display: flex;
7638
  flex-direction: column;
7639
- min-height: 100%;
7640
  }
7641
 
7642
  .sc-tabs {
@@ -7671,7 +8116,7 @@ body.rounds-drawer-open {
7671
 
7672
  .sc-tab-panel {
7673
  display: none;
7674
- padding: 18px 22px;
7675
  overflow-y: auto;
7676
  max-height: none;
7677
  }
@@ -7722,6 +8167,37 @@ body.rounds-drawer-open {
7722
 
7723
  .sc-voice-inline {
7724
  margin-top: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7725
  }
7726
 
7727
  .sc-answers-stack {
@@ -7857,24 +8333,82 @@ blockquote.sc-answer-text {
7857
  color: rgba(255, 255, 255, 0.65);
7858
  }
7859
 
7860
- /* ---- Verdict (inline in right column) ---- */
7861
- .sc-verdict-section {
7862
- padding: 16px 24px 20px;
7863
- border-top: 1px solid rgba(255, 255, 255, 0.06);
7864
- margin: 0 -8px;
 
 
 
 
 
 
7865
  }
7866
 
7867
- .sc-verdict-inline {
7868
- margin-top: auto;
7869
- margin-left: 0;
7870
- margin-right: 0;
7871
- padding: 20px 22px 22px;
7872
- border-top: 1px solid rgba(255, 255, 255, 0.08);
7873
- background: rgba(0, 0, 0, 0.15);
7874
  }
7875
 
7876
- .sc-verdict-heading {
7877
- margin-bottom: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7878
  }
7879
 
7880
  .sc-verdict-card {
@@ -8029,39 +8563,183 @@ blockquote.sc-answer-text {
8029
  -webkit-line-clamp: unset;
8030
  }
8031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8032
  @media (max-width: 960px) {
8033
  .sc-shell {
8034
- padding: 20px 16px;
8035
  }
8036
 
8037
  .sc-hero-row {
8038
  flex-wrap: wrap;
8039
  }
8040
 
8041
- .sc-hero-conversation {
8042
- margin-left: 0;
8043
- width: 100%;
8044
- align-self: stretch;
8045
  }
8046
 
8047
- .sc-btn-conversation {
 
8048
  width: 100%;
 
 
 
 
 
 
8049
  text-align: center;
8050
  }
8051
 
8052
- .sc-body-grid {
8053
  grid-template-columns: 1fr;
8054
  }
8055
 
8056
- .sc-left-col {
8057
- border-right: none;
8058
- border-bottom: 1px solid rgba(255, 255, 255, 0.06);
8059
  }
 
8060
 
8061
- .sc-verdict-meta {
8062
- grid-template-columns: 1fr;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8063
  }
8064
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8065
  }
8066
 
8067
  @media (max-width: 720px) {
 
26
  box-sizing: border-box;
27
  }
28
 
29
+ html {
30
+ width: 100%;
31
+ max-width: 100%;
32
+ overflow-x: hidden;
33
+ }
34
+
35
  body {
36
  margin: 0;
37
  min-height: 100vh;
38
+ width: 100%;
39
+ max-width: 100%;
40
+ overflow-x: hidden;
41
  font-family: "Segoe UI", system-ui, sans-serif;
42
  color: var(--text);
43
  background: #09090f;
 
56
 
57
  .app {
58
  position: relative;
59
+ width: min(1280px, calc(100vw - 32px));
60
+ max-width: 100%;
61
  margin: 0 auto;
62
+ padding: clamp(1rem, 2vw, 1.5rem) clamp(0.75rem, 2vw, 1rem) clamp(1.25rem, 3vw, 2rem);
63
  }
64
 
65
  .app:has(#screen-battle.active),
 
133
  .arena-landing {
134
  position: relative;
135
  width: 100vw;
136
+ min-height: auto;
137
+ padding: clamp(0.5rem, 1.5vh, 1rem) 0;
138
  margin-left: calc(50% - 50vw);
139
  margin-right: calc(50% - 50vw);
140
  overflow: hidden;
 
747
  position: relative;
748
  z-index: 2;
749
  text-align: center;
750
+ padding: clamp(1rem, 3vw, 1.75rem) clamp(0.85rem, 2.5vw, 1.25rem) clamp(1rem, 2.5vw, 1.5rem);
751
  max-width: 820px;
752
  width: 100%;
753
  }
 
777
  }
778
 
779
  .arena-title {
780
+ margin: 0 0 clamp(0.6rem, 2vw, 1rem);
781
+ font-size: clamp(2rem, 6vw, 3.5rem);
782
  font-weight: 800;
783
  letter-spacing: 0.03em;
784
  line-height: 1.02;
 
791
 
792
  /* Pass 2 — hook lines */
793
  .arena-hook {
794
+ min-height: clamp(3rem, 8vw, 4rem);
795
+ margin-bottom: clamp(0.5rem, 1.5vw, 0.75rem);
796
  }
797
 
798
  .arena-hook-line {
 
826
 
827
  .arena-support {
828
  max-width: 520px;
829
+ margin: 0 auto clamp(0.75rem, 2vw, 1rem);
830
  font-size: clamp(0.88rem, 2vw, 0.95rem);
831
  line-height: 1.5;
832
  color: var(--text-muted);
 
903
  flex-wrap: wrap;
904
  gap: 0.4rem;
905
  justify-content: center;
906
+ margin-top: clamp(0.65rem, 2vw, 1rem);
907
+ padding-bottom: 0.35rem;
908
  opacity: 0;
909
  transform: translateY(8px);
910
  transition: opacity 0.5s ease 0.12s, transform 0.5s ease 0.12s;
 
934
  }
935
 
936
  .arena-landing-footer {
937
+ margin-top: clamp(0.85rem, 2vw, 1.25rem);
938
+ padding-top: clamp(0.65rem, 1.5vw, 0.9rem);
939
+ padding-bottom: 0.35rem;
940
  border-top: 1px solid rgba(255, 255, 255, 0.1);
941
  opacity: 0;
942
  transition: opacity 0.5s ease 0.2s;
 
1222
 
1223
  .persona-grid {
1224
  display: grid;
1225
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1226
+ gap: 0.6rem;
1227
+ margin-top: 0.65rem;
1228
  }
1229
 
1230
  .persona-card {
1231
  text-align: left;
1232
+ padding: 0.75rem 0.85rem;
1233
  border-radius: 14px;
1234
  border: 1px solid rgba(255, 255, 255, 0.08);
1235
  background: rgba(0, 0, 0, 0.22);
 
1256
  }
1257
 
1258
  .difficulty-selector {
1259
+ margin-top: 1rem;
1260
  }
1261
 
1262
  .difficulty-selector h3 {
 
1269
 
1270
  .difficulty-grid {
1271
  display: grid;
1272
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
1273
+ gap: 0.55rem;
1274
  }
1275
 
1276
  .difficulty-card {
1277
  text-align: left;
1278
+ padding: 0.65rem 0.8rem;
1279
  border-radius: 12px;
1280
  background: rgba(255, 255, 255, 0.04);
1281
  border: 1px solid var(--border);
 
1648
  position: relative;
1649
  z-index: 1;
1650
  width: min(1180px, 100%);
1651
+ height: min(80vh, 720px);
1652
+ max-height: min(80vh, calc(100dvh - 24px));
1653
  display: flex;
1654
  flex-direction: column;
1655
  border: 1px solid rgba(78, 205, 196, 0.2);
 
2845
  .battle-arena.arena-shell {
2846
  position: relative;
2847
  z-index: 1;
2848
+ width: min(1360px, calc(100vw - 32px));
2849
+ max-width: 1360px;
2850
  min-width: 0;
2851
  margin: 0 auto;
2852
+ min-height: 0;
2853
  display: grid;
2854
  grid-template-columns: 280px minmax(0, 1fr);
2855
+ gap: 16px;
2856
  align-items: stretch;
2857
  }
2858
 
 
2861
  margin: 0 auto;
2862
  }
2863
 
2864
+ .app:has(#screen-scorecard.active),
2865
+ .app.app-scorecard-fullwidth {
2866
+ max-width: none !important;
2867
+ width: 100% !important;
2868
+ padding: clamp(0.5rem, 1.2vw, 0.85rem) !important;
2869
+ }
2870
+
2871
+ #screen-setup.active .briefing-shell-wide {
2872
+ width: min(1180px, 92vw);
2873
+ max-width: 100%;
2874
+ margin: 0 auto;
2875
+ padding-bottom: 0.5rem;
2876
+ }
2877
+
2878
  .briefing-shell-wide {
2879
+ max-width: min(1180px, 92vw);
2880
+ width: 100%;
2881
+ margin: 0 auto;
2882
  }
2883
 
2884
  .briefing-mode-tabs {
2885
  display: flex;
2886
+ gap: 0.45rem;
2887
+ margin-bottom: 0.75rem;
2888
  flex-wrap: wrap;
2889
  }
2890
 
2891
  .briefing-mode-tab {
2892
  flex: 1;
2893
  min-width: 140px;
2894
+ padding: 0.65rem 1rem;
2895
  border-radius: 12px;
2896
  border: 1px solid rgba(255, 255, 255, 0.1);
2897
  background: rgba(0, 0, 0, 0.25);
 
2916
  }
2917
 
2918
  .briefing-grid-wide {
2919
+ grid-template-columns: minmax(0, 1.12fr) minmax(280px, 0.88fr);
2920
+ gap: 0.85rem;
2921
  align-items: stretch;
2922
  }
2923
 
2924
  .briefing-left-col {
2925
+ display: grid;
2926
+ grid-template-columns: minmax(0, 1fr);
2927
+ align-items: stretch;
2928
  min-width: 0;
2929
  overflow: visible;
2930
  }
2931
 
2932
+ .briefing-left-col > .panel {
2933
+ min-height: 0;
2934
+ }
2935
+
2936
+ .briefing-left-col.mode-advanced .quick-pitch-panel,
2937
+ .briefing-left-col.mode-advanced .brief-preview-panel {
2938
+ display: none !important;
2939
+ }
2940
+
2941
+ .briefing-left-col.mode-structured .quick-pitch-panel,
2942
+ .briefing-left-col:not(.mode-structured) .brief-preview-panel {
2943
+ display: none !important;
2944
+ }
2945
+
2946
+ [hidden] {
2947
+ display: none !important;
2948
+ }
2949
+
2950
  .quick-pitch-panel {
2951
+ padding: clamp(0.75rem, 1.8vw, 0.95rem) clamp(0.85rem, 1.8vw, 1rem);
2952
  }
2953
 
2954
  .quick-pitch-label {
 
2962
 
2963
  .quick-pitch-textarea {
2964
  width: 100%;
2965
+ min-height: clamp(96px, 14vh, 128px);
2966
+ max-height: clamp(120px, 18vh, 150px);
2967
+ padding: 0.7rem 0.85rem;
2968
+ border-radius: 12px;
2969
  border: 1px solid rgba(255, 255, 255, 0.12);
2970
  background: rgba(0, 0, 0, 0.35);
2971
  color: var(--text);
2972
+ font-size: 0.92rem;
2973
+ line-height: 1.45;
2974
+ resize: none;
2975
  box-shadow: inset 0 2px 12px rgba(0, 0, 0, 0.25);
2976
  }
2977
 
 
2990
  .quick-pitch-actions {
2991
  display: flex;
2992
  flex-direction: column;
2993
+ gap: 0.55rem;
2994
+ margin-top: 0.75rem;
2995
  }
2996
 
2997
  .quick-pitch-actions .btn-wide {
 
3011
  }
3012
 
3013
  .quick-pitch-hint {
3014
+ margin: 0.55rem 0 0;
3015
+ font-size: 0.8rem;
3016
  color: rgba(244, 211, 94, 0.85);
3017
  }
3018
 
3019
+ .quick-pitch-success {
3020
+ margin: 0.65rem 0 0;
3021
+ padding: 0.5rem 0.7rem;
3022
+ border-radius: 10px;
3023
+ border: 1px solid rgba(244, 211, 94, 0.35);
3024
+ background: rgba(244, 211, 94, 0.08);
3025
+ color: var(--gold);
3026
+ font-size: 0.82rem;
3027
+ line-height: 1.4;
3028
+ }
3029
+
3030
+ .quick-pitch-reedit {
3031
+ margin-top: 0.35rem;
3032
+ align-self: flex-start;
3033
+ padding: 0.35rem 0.65rem;
3034
+ font-size: 0.78rem;
3035
+ }
3036
+
3037
  .brief-preview-panel {
3038
+ padding: clamp(0.7rem, 1.6vw, 0.85rem) clamp(0.8rem, 1.8vw, 0.95rem);
3039
  overflow: visible;
3040
  position: relative;
3041
+ display: flex;
3042
+ flex-direction: column;
3043
+ height: 100%;
3044
+ min-height: 100%;
3045
  }
3046
 
3047
  .brief-preview-panel.is-editing-active::before {
 
3055
  }
3056
 
3057
  .brief-preview-header {
3058
+ margin-bottom: 0.5rem;
3059
  }
3060
 
3061
  .brief-preview-title-row {
 
3071
  }
3072
 
3073
  .brief-preview-helper {
3074
+ margin: 0.25rem 0 0;
3075
  color: var(--muted);
3076
+ font-size: 0.78rem;
3077
  }
3078
 
3079
  .brief-preview-hint {
3080
+ margin: 0.35rem 0 0;
3081
+ font-size: 0.78rem;
3082
  color: rgba(244, 211, 94, 0.85);
3083
  }
3084
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3085
  .brief-preview-grid {
3086
  display: grid;
3087
  grid-template-columns: repeat(2, minmax(0, 1fr));
3088
+ gap: 0.45rem;
3089
+ flex: 1 1 auto;
3090
  }
3091
 
3092
  .brief-read-card {
3093
+ padding: 0.45rem 0.55rem;
3094
+ border-radius: 10px;
3095
  border: 1px solid rgba(255, 255, 255, 0.08);
3096
  background: rgba(0, 0, 0, 0.22);
3097
  transition: border-color 0.2s, background 0.2s, box-shadow 0.2s, transform 0.2s;
 
3251
  .brief-preview-actions {
3252
  display: flex;
3253
  flex-direction: column;
3254
+ gap: 0.45rem;
3255
+ margin-top: auto;
3256
+ padding-top: 0.55rem;
3257
  }
3258
 
3259
  .brief-preview-actions .btn-wide {
 
3261
  }
3262
 
3263
  .advanced-briefing-panel {
3264
+ padding: clamp(0.7rem, 1.6vw, 0.85rem) clamp(0.8rem, 1.8vw, 0.95rem);
3265
+ display: flex;
3266
+ flex-direction: column;
3267
+ height: 100%;
3268
+ min-height: 100%;
3269
  }
3270
 
3271
+ .advanced-briefing-form {
3272
+ display: grid;
3273
+ grid-template-columns: repeat(2, minmax(0, 1fr));
3274
+ gap: 0.45rem;
3275
+ margin-top: 0;
3276
+ flex: 1 1 auto;
3277
+ align-content: start;
3278
+ }
3279
+
3280
+ .advanced-briefing-form .adv-field-wide {
3281
+ grid-column: 1 / -1;
3282
+ }
3283
+
3284
+ .advanced-briefing-form label {
3285
+ display: grid;
3286
+ gap: 0.25rem;
3287
+ margin: 0;
3288
+ font-size: 0.68rem;
3289
+ font-weight: 700;
3290
+ letter-spacing: 0.1em;
3291
+ text-transform: uppercase;
3292
+ color: var(--accent-cyan);
3293
+ }
3294
+
3295
+ .advanced-briefing-form input,
3296
+ .advanced-briefing-form textarea {
3297
+ padding: 0.45rem 0.55rem;
3298
+ font-size: 0.82rem;
3299
+ line-height: 1.35;
3300
+ border-radius: 8px;
3301
+ }
3302
+
3303
+ .advanced-briefing-form textarea {
3304
+ min-height: 52px;
3305
+ max-height: 68px;
3306
+ resize: none;
3307
+ overflow-y: auto;
3308
+ }
3309
+
3310
+ .briefing-left-col.mode-advanced .advanced-briefing-panel {
3311
+ min-height: 100%;
3312
+ }
3313
+
3314
+ .briefing-opponent-panel .briefing-section-title,
3315
+ .briefing-opponent-panel .difficulty-selector h3 {
3316
+ margin: 0 0 0.4rem;
3317
+ font-size: 0.82rem;
3318
+ letter-spacing: 0.08em;
3319
+ }
3320
+
3321
+ .briefing-opponent-panel .persona-grid {
3322
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3323
+ gap: 0.45rem;
3324
+ margin-top: 0.35rem;
3325
+ }
3326
+
3327
+ .briefing-opponent-panel .difficulty-grid {
3328
+ grid-template-columns: repeat(3, minmax(0, 1fr));
3329
+ gap: 0.45rem;
3330
+ }
3331
+
3332
+ .briefing-opponent-panel .persona-card {
3333
+ padding: 0.5rem 0.55rem;
3334
+ border-radius: 10px;
3335
+ }
3336
+
3337
+ .briefing-opponent-panel .persona-card .persona-icon {
3338
+ font-size: 1rem;
3339
+ line-height: 1;
3340
+ }
3341
+
3342
+ .briefing-opponent-panel .persona-card h3 {
3343
+ margin: 0.25rem 0 0.15rem;
3344
+ font-size: 0.8rem;
3345
+ }
3346
+
3347
+ .briefing-opponent-panel .persona-card p {
3348
+ font-size: 0.68rem;
3349
+ line-height: 1.25;
3350
+ }
3351
+
3352
+ .briefing-opponent-panel .difficulty-selector {
3353
+ margin-top: 0.65rem;
3354
+ }
3355
+
3356
+ .briefing-opponent-panel .difficulty-card {
3357
+ padding: 0.5rem 0.55rem;
3358
+ border-radius: 10px;
3359
+ }
3360
+
3361
+ .briefing-opponent-panel .difficulty-card .difficulty-icon {
3362
+ font-size: 1rem;
3363
+ margin-bottom: 0.15rem;
3364
+ }
3365
+
3366
+ .briefing-opponent-panel .difficulty-card h4 {
3367
+ margin: 0 0 0.1rem;
3368
+ font-size: 0.78rem;
3369
+ }
3370
+
3371
+ .briefing-opponent-panel .difficulty-card p {
3372
+ font-size: 0.66rem;
3373
+ line-height: 1.2;
3374
  }
3375
 
3376
  .briefing-opponent-note {
 
3386
  flex-direction: column;
3387
  align-self: stretch;
3388
  min-height: 100%;
3389
+ height: 100%;
3390
  gap: 0;
3391
+ padding: clamp(0.7rem, 1.6vw, 0.85rem) clamp(0.8rem, 1.8vw, 0.95rem);
3392
  }
3393
 
3394
  .briefing-opponent-footer {
3395
  margin-top: auto;
3396
+ padding-top: 1rem;
3397
  display: flex;
3398
  flex-direction: column;
3399
+ gap: 0.55rem;
3400
  }
3401
 
3402
  .briefing-opponent-panel .btn-arena-start {
 
3412
  justify-content: space-between;
3413
  align-items: flex-start;
3414
  gap: 1rem;
3415
+ margin-bottom: 0.75rem;
3416
  }
3417
 
3418
  .briefing-title {
 
4847
 
4848
  .voice-delivery-summary {
4849
  display: grid;
4850
+ grid-template-columns: repeat(4, minmax(0, 1fr));
4851
+ gap: 0.5rem;
4852
  }
4853
 
4854
  .voice-wave-decor {
 
5910
  .outcome-strong_win { background: rgba(74, 222, 128, 0.18); color: #86efac; }
5911
  .outcome-weak_concession { background: rgba(248, 113, 113, 0.18); color: #fca5a5; }
5912
 
5913
+ /* Voice mode — Phase 7 + entry path chooser */
5914
+ .screen-start-path {
5915
+ width: 100%;
 
 
 
5916
  }
5917
 
5918
+ .start-path-shell {
5919
+ width: min(920px, 92vw);
5920
+ max-width: 100%;
5921
+ margin: 0 auto;
5922
+ padding: clamp(0.35rem, 1.5vw, 0.75rem) 0;
 
 
 
 
5923
  }
5924
 
5925
+ .start-path-header {
5926
+ display: flex;
5927
+ justify-content: space-between;
5928
+ align-items: flex-start;
5929
+ gap: 1rem;
5930
+ margin-bottom: clamp(0.85rem, 2vw, 1.15rem);
5931
  }
5932
 
5933
+ .start-path-title {
5934
  margin: 0.35rem 0 0.25rem;
5935
+ font-size: clamp(1.45rem, 3vw, 1.85rem);
5936
  }
5937
 
5938
+ .start-path-sub {
5939
  margin: 0;
5940
  color: var(--muted);
5941
+ font-size: 0.88rem;
5942
+ line-height: 1.45;
5943
+ }
5944
+
5945
+ .start-path-grid {
5946
+ grid-template-columns: repeat(2, minmax(0, 1fr));
5947
+ gap: clamp(0.75rem, 2vw, 1rem);
5948
+ margin: 0;
5949
+ }
5950
+
5951
+ .start-path-card {
5952
+ padding: clamp(1rem, 2.5vw, 1.25rem) clamp(0.95rem, 2vw, 1.15rem);
5953
+ border-radius: 16px;
5954
+ border: 1px solid rgba(255, 255, 255, 0.1);
5955
+ background: linear-gradient(165deg, rgba(18, 18, 24, 0.95), rgba(8, 8, 12, 0.92));
5956
+ text-align: left;
5957
+ transition: transform 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
5958
+ }
5959
+
5960
+ .start-path-card-text:hover,
5961
+ .start-path-card-text:focus-visible {
5962
+ border-color: rgba(244, 211, 94, 0.55);
5963
+ box-shadow: 0 0 32px rgba(244, 211, 94, 0.14);
5964
+ transform: translateY(-2px);
5965
+ }
5966
+
5967
+ .start-path-card-voice:hover,
5968
+ .start-path-card-voice:focus-visible {
5969
+ border-color: rgba(78, 205, 196, 0.55);
5970
+ box-shadow: 0 0 32px rgba(78, 205, 196, 0.14);
5971
+ transform: translateY(-2px);
5972
+ }
5973
+
5974
+ .start-path-badge {
5975
+ display: inline-block;
5976
+ margin-bottom: 0.55rem;
5977
+ font-size: 0.62rem;
5978
+ font-weight: 800;
5979
+ letter-spacing: 0.16em;
5980
+ text-transform: uppercase;
5981
+ color: var(--gold);
5982
+ }
5983
+
5984
+ .start-path-badge-voice {
5985
+ color: #7efff0;
5986
+ }
5987
+
5988
+ .start-path-card h3 {
5989
+ margin: 0.45rem 0 0.15rem;
5990
+ font-size: clamp(1.1rem, 2.2vw, 1.3rem);
5991
+ }
5992
+
5993
+ .start-path-tagline {
5994
+ margin: 0 0 0.45rem;
5995
+ font-size: 0.68rem;
5996
+ font-weight: 700;
5997
+ letter-spacing: 0.12em;
5998
+ text-transform: uppercase;
5999
+ color: var(--gold);
6000
+ }
6001
+
6002
+ .start-path-card-voice .start-path-tagline {
6003
+ color: #7efff0;
6004
+ }
6005
+
6006
+ .start-path-desc {
6007
+ margin: 0;
6008
+ font-size: 0.84rem;
6009
+ line-height: 1.45;
6010
+ color: var(--muted);
6011
+ }
6012
+
6013
+ @media (max-width: 640px) {
6014
+ .start-path-grid {
6015
+ grid-template-columns: 1fr;
6016
+ }
6017
+ }
6018
+
6019
+ @media (max-height: 760px) {
6020
+ .start-path-header {
6021
+ margin-bottom: 0.65rem;
6022
+ }
6023
+
6024
+ .start-path-card {
6025
+ padding: 0.85rem 0.9rem;
6026
+ }
6027
+
6028
+ .start-path-card h3 {
6029
+ font-size: 1.05rem;
6030
+ }
6031
+ }
6032
+
6033
+ .start-method-grid {
6034
+ display: grid;
6035
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
6036
+ gap: 0.85rem;
6037
+ margin: 1rem 0 1.25rem;
6038
+ }
6039
+
6040
+ .start-method-card {
6041
+ text-align: left;
6042
+ padding: 1.1rem;
6043
+ border-radius: 12px;
6044
+ border: 1px solid rgba(255, 255, 255, 0.08);
6045
+ background: rgba(0, 0, 0, 0.2);
6046
+ color: inherit;
6047
+ cursor: pointer;
6048
+ transition: border-color 0.2s, box-shadow 0.2s;
6049
+ }
6050
+
6051
+ .start-method-card.selected {
6052
+ border-color: rgba(125, 211, 252, 0.45);
6053
+ box-shadow: 0 0 20px rgba(125, 211, 252, 0.12);
6054
+ }
6055
+
6056
+ .start-method-card h3 {
6057
+ margin: 0.35rem 0 0.25rem;
6058
+ font-size: 1rem;
6059
+ }
6060
+
6061
+ .start-method-card p {
6062
+ margin: 0;
6063
+ color: var(--muted);
6064
+ font-size: 0.85rem;
6065
+ line-height: 1.4;
6066
+ }
6067
 
6068
  .start-method-icon {
6069
  font-size: 1.4rem;
 
6262
  margin-top: 0.5rem;
6263
  }
6264
 
6265
+ .voice-delivery-wrap {
6266
+ width: 100%;
6267
+ }
6268
+
6269
+ .voice-delivery-layout {
6270
+ display: flex;
6271
+ flex-direction: column;
6272
+ gap: 0.55rem;
6273
+ }
6274
+
6275
+ .voice-delivery-metrics {
6276
  display: grid;
6277
+ grid-template-columns: repeat(4, minmax(0, 1fr));
6278
+ gap: 0.5rem;
6279
  }
6280
 
6281
+ .voice-delivery-box,
6282
+ .voice-delivery-insight {
6283
+ padding: 0.55rem 0.65rem;
6284
+ border-radius: 10px;
6285
+ border: 1px solid rgba(78, 205, 196, 0.22);
6286
+ background: rgba(0, 0, 0, 0.28);
6287
  }
6288
 
6289
+ .voice-delivery-box {
6290
+ display: flex;
6291
+ flex-direction: column;
6292
+ justify-content: space-between;
6293
+ gap: 0.35rem;
6294
+ min-height: 58px;
6295
+ }
6296
+
6297
+ .voice-delivery-box-label {
6298
  display: block;
6299
+ font-size: 0.62rem;
6300
+ font-weight: 700;
6301
+ letter-spacing: 0.1em;
6302
  text-transform: uppercase;
6303
+ color: rgba(125, 211, 252, 0.85);
6304
  }
6305
 
6306
+ .voice-delivery-box-value {
6307
  display: block;
6308
+ font-size: 0.88rem;
6309
+ font-weight: 700;
6310
+ line-height: 1.3;
6311
+ color: var(--text);
6312
+ word-break: break-word;
6313
+ }
6314
+
6315
+ .voice-delivery-insight {
6316
+ display: grid;
6317
+ grid-template-columns: minmax(72px, 100px) minmax(0, 1fr);
6318
+ gap: 0.65rem;
6319
+ align-items: start;
6320
+ }
6321
+
6322
+ .voice-delivery-insight-text {
6323
+ margin: 0;
6324
+ font-size: 0.84rem;
6325
+ line-height: 1.45;
6326
+ color: rgba(255, 255, 255, 0.9);
6327
+ }
6328
+
6329
+ .voice-delivery-overall {
6330
+ margin: 0;
6331
+ padding: 0.55rem 0.65rem;
6332
+ border-radius: 10px;
6333
+ border: 1px solid rgba(255, 255, 255, 0.08);
6334
+ background: rgba(255, 255, 255, 0.03);
6335
+ font-size: 0.84rem;
6336
+ line-height: 1.45;
6337
+ color: rgba(255, 255, 255, 0.82);
6338
  }
6339
 
6340
  .voice-delivery-notes {
6341
+ margin: 0.25rem 0 0;
6342
+ padding: 0.55rem 0.65rem 0.55rem 1.1rem;
6343
+ border-radius: 10px;
6344
+ border: 1px solid rgba(255, 255, 255, 0.06);
6345
+ background: rgba(0, 0, 0, 0.18);
6346
+ font-size: 0.82rem;
6347
  color: var(--muted);
6348
  }
6349
 
6350
+ @media (max-width: 900px) {
6351
+ .voice-delivery-metrics {
6352
+ grid-template-columns: repeat(2, minmax(0, 1fr));
6353
+ }
6354
+ }
6355
+
6356
+ @media (max-width: 520px) {
6357
+ .voice-delivery-metrics {
6358
+ grid-template-columns: 1fr 1fr;
6359
+ }
6360
+
6361
+ .voice-delivery-insight {
6362
+ grid-template-columns: 1fr;
6363
+ gap: 0.35rem;
6364
+ }
6365
+ }
6366
+
6367
  .voice-overall-feedback {
6368
  grid-column: 1 / -1;
6369
  margin: 0.5rem 0 0;
 
7555
  ================================ */
7556
 
7557
  .sc-shell {
7558
+ width: min(1320px, 90vw);
7559
+ max-width: 100%;
7560
+ margin: 0 auto;
7561
+ padding: clamp(10px, 1.8vw, 16px) clamp(12px, 2vw, 20px);
7562
  }
7563
 
7564
  /* ---- Hero ---- */
7565
  .sc-hero {
7566
+ padding: clamp(12px, 2vw, 16px) clamp(14px, 2vw, 20px);
7567
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
7568
+ margin: 0;
7569
  }
7570
 
7571
  .sc-hero-row {
 
7574
  align-items: flex-start;
7575
  }
7576
 
7577
+ .sc-hero-actions-row {
7578
+ display: flex;
7579
+ flex-wrap: wrap;
7580
+ align-items: center;
7581
+ justify-content: space-between;
7582
+ gap: 10px;
7583
+ margin-top: 12px;
7584
+ }
7585
+
7586
+ .sc-hero-actions-primary,
7587
+ .sc-hero-actions-secondary {
7588
+ display: flex;
7589
+ flex-wrap: wrap;
7590
+ gap: 8px;
7591
+ align-items: center;
7592
+ }
7593
+
7594
+ .sc-hero-actions-secondary {
7595
  margin-left: auto;
 
 
7596
  }
7597
 
7598
  .sc-ring-wrap {
7599
  flex-shrink: 0;
7600
+ display: flex;
7601
+ flex-direction: column;
7602
+ align-items: center;
7603
+ gap: 5px;
7604
+ min-width: 88px;
7605
  }
7606
 
7607
  .sc-ring {
7608
+ width: 84px;
7609
+ height: 84px;
7610
+ min-width: 84px;
7611
+ min-height: 84px;
7612
  border-radius: 50%;
7613
  border: 3px solid #f5c842;
7614
  background: rgba(245, 200, 66, 0.05);
 
7617
  align-items: center;
7618
  justify-content: center;
7619
  box-shadow: none;
7620
+ overflow: visible;
7621
  }
7622
 
7623
  .sc-ring .score-orb-value {
7624
+ font-size: 30px;
7625
  font-weight: 800;
7626
  line-height: 1;
7627
  color: #f5c842;
7628
  }
7629
 
7630
+ .sc-ring-caption {
7631
+ display: block;
7632
+ max-width: 92px;
7633
+ margin: 0;
7634
+ text-align: center;
7635
+ font-size: 8px;
7636
  font-weight: 800;
7637
+ letter-spacing: 0.08em;
7638
+ line-height: 1.25;
7639
  text-transform: uppercase;
7640
+ color: rgba(245, 200, 66, 0.9);
7641
+ word-break: break-word;
7642
  }
7643
 
7644
  .sc-hero-meta {
 
7669
  display: flex;
7670
  flex-wrap: wrap;
7671
  gap: 8px;
 
7672
  }
7673
 
7674
+ .score-action-btn {
7675
+ min-height: 42px;
7676
+ padding: 10px 16px;
7677
+ font-size: 12px;
7678
+ font-weight: 700;
7679
  letter-spacing: 0.04em;
7680
  text-transform: uppercase;
7681
+ border-radius: 10px;
7682
+ white-space: nowrap;
7683
+ line-height: 1.2;
7684
+ display: inline-flex;
7685
+ align-items: center;
7686
+ justify-content: center;
7687
+ }
7688
+
7689
+ .score-action-btn-secondary {
7690
+ border: 2px solid rgba(94, 211, 244, 0.45);
7691
+ background: rgba(94, 211, 244, 0.08);
7692
  color: #5ed3f4;
7693
+ }
7694
+
7695
+ @media (min-width: 900px) {
7696
+ .briefing-opponent-panel .persona-grid,
7697
+ .briefing-opponent-panel .difficulty-grid {
7698
+ grid-template-columns: repeat(3, minmax(0, 1fr));
7699
+ }
7700
+ }
7701
+
7702
+ @media (max-width: 900px) {
7703
+ .advanced-briefing-form {
7704
+ grid-template-columns: 1fr;
7705
+ }
7706
+
7707
+ .advanced-briefing-form .adv-field-wide {
7708
+ grid-column: auto;
7709
+ }
7710
+
7711
+ .briefing-opponent-panel .persona-grid,
7712
+ .briefing-opponent-panel .difficulty-grid {
7713
+ grid-template-columns: 1fr;
7714
+ }
7715
+ }
7716
+
7717
+ .score-action-btn-verdict {
7718
+ border: 2px solid rgba(0, 230, 200, 0.45);
7719
+ background: rgba(0, 230, 200, 0.08);
7720
+ color: #00e6c8;
7721
+ }
7722
+
7723
+ .score-action-btn-verdict:hover {
7724
+ border-color: #00e6c8;
7725
+ background: rgba(0, 230, 200, 0.16);
7726
+ }
7727
+
7728
+ .sc-btn-gold,
7729
+ .sc-btn-secondary {
7730
+ min-height: 42px;
7731
+ padding: 10px 16px;
7732
+ font-size: 12px;
7733
+ }
7734
+
7735
+ .sc-btn-verdict,
7736
+ .sc-btn-conversation {
7737
+ min-height: 42px;
7738
+ padding: 10px 16px;
7739
+ font-size: 12px;
7740
+ font-weight: 700;
7741
+ letter-spacing: 0.04em;
7742
+ text-transform: uppercase;
7743
+ border-radius: 10px;
7744
  white-space: nowrap;
7745
+ }
7746
+
7747
+ .sc-btn-verdict {
7748
+ border: 2px solid rgba(0, 230, 200, 0.45);
7749
+ background: rgba(0, 230, 200, 0.08);
7750
+ color: #00e6c8;
7751
+ }
7752
+
7753
+ .sc-btn-conversation {
7754
+ border: 2px solid rgba(94, 211, 244, 0.45);
7755
+ background: rgba(94, 211, 244, 0.08);
7756
+ color: #5ed3f4;
7757
+ box-shadow: none;
7758
+ }
7759
+
7760
+ .retry-projection-note {
7761
+ margin: 0.65rem 0 0;
7762
+ font-size: 12px;
7763
+ font-weight: 600;
7764
+ color: rgba(255, 255, 255, 0.55);
7765
+ text-align: center;
7766
+ }
7767
+
7768
+ .retry-drill-panel {
7769
+ max-height: min(82vh, 760px);
7770
+ overflow-y: auto;
7771
  }
7772
 
7773
  .sc-btn-conversation:hover {
 
7820
  /* ---- Body grid ---- */
7821
  .sc-body-grid {
7822
  display: grid;
7823
+ grid-template-columns: minmax(260px, 320px) minmax(0, 1fr);
 
7824
  width: 100%;
7825
+ align-items: start;
7826
+ gap: 0;
7827
  }
7828
 
7829
  .sc-left-col {
7830
+ padding: clamp(10px, 1.8vw, 14px) clamp(12px, 2vw, 16px);
7831
  border-right: 1px solid rgba(255, 255, 255, 0.06);
7832
  min-width: 0;
7833
+ max-height: min(72vh, 680px);
7834
+ overflow-y: auto;
7835
+ overscroll-behavior: contain;
7836
  }
7837
 
7838
  .sc-section-label {
 
7904
  .sc-dim-list .dimension-row-scorecard {
7905
  display: flex;
7906
  flex-direction: column;
7907
+ gap: 8px;
7908
+ padding: 10px 12px;
7909
  border: 1px solid rgba(255, 255, 255, 0.08);
7910
  border-radius: 12px;
7911
  background: rgba(255, 255, 255, 0.03);
 
8082
  min-width: 0;
8083
  display: flex;
8084
  flex-direction: column;
 
8085
  }
8086
 
8087
  .sc-tabs {
 
8116
 
8117
  .sc-tab-panel {
8118
  display: none;
8119
+ padding: clamp(12px, 2vw, 16px) clamp(14px, 2vw, 18px);
8120
  overflow-y: auto;
8121
  max-height: none;
8122
  }
 
8167
 
8168
  .sc-voice-inline {
8169
  margin-top: 4px;
8170
+ width: 100%;
8171
+ }
8172
+
8173
+ .sc-voice-inline .voice-delivery-wrap,
8174
+ .sc-voice-inline .voice-delivery-layout {
8175
+ width: 100%;
8176
+ }
8177
+
8178
+ .sc-voice-inline .voice-delivery-metrics {
8179
+ display: flex;
8180
+ flex-direction: row;
8181
+ flex-wrap: nowrap;
8182
+ gap: 0.45rem;
8183
+ width: 100%;
8184
+ }
8185
+
8186
+ .sc-voice-inline .voice-delivery-box {
8187
+ flex: 1 1 0;
8188
+ min-width: 0;
8189
+ }
8190
+
8191
+ @media (max-width: 720px) {
8192
+ .sc-voice-inline .voice-delivery-metrics {
8193
+ display: grid;
8194
+ grid-template-columns: repeat(2, minmax(0, 1fr));
8195
+ flex-wrap: wrap;
8196
+ }
8197
+
8198
+ .sc-voice-inline .voice-delivery-box {
8199
+ flex: unset;
8200
+ }
8201
  }
8202
 
8203
  .sc-answers-stack {
 
8333
  color: rgba(255, 255, 255, 0.65);
8334
  }
8335
 
8336
+ /* ---- Judge Verdict Modal ---- */
8337
+ .verdict-overlay {
8338
+ position: fixed;
8339
+ inset: 0;
8340
+ background: rgba(0, 0, 0, 0.72);
8341
+ z-index: 55;
8342
+ overflow-y: auto;
8343
+ display: flex;
8344
+ align-items: center;
8345
+ justify-content: center;
8346
+ padding: 1rem;
8347
  }
8348
 
8349
+ .verdict-overlay[hidden] {
8350
+ display: none;
 
 
 
 
 
8351
  }
8352
 
8353
+ .verdict-modal-panel {
8354
+ width: 100%;
8355
+ max-width: 640px;
8356
+ padding: clamp(0.9rem, 2vw, 1.15rem);
8357
+ margin: auto;
8358
+ max-height: min(80vh, 720px);
8359
+ overflow-y: auto;
8360
+ }
8361
+
8362
+ .verdict-modal-header {
8363
+ display: flex;
8364
+ justify-content: space-between;
8365
+ align-items: flex-start;
8366
+ gap: 1rem;
8367
+ margin-bottom: 0.75rem;
8368
+ }
8369
+
8370
+ .verdict-modal-title {
8371
+ margin: 0;
8372
+ font-size: 1.2rem;
8373
+ }
8374
+
8375
+ .sc-verdict-modal-card {
8376
+ padding: 14px 16px;
8377
+ border-radius: 10px;
8378
+ background: rgba(255, 255, 255, 0.03);
8379
+ border: 1px solid rgba(255, 255, 255, 0.08);
8380
+ border-top: 2px solid rgba(0, 230, 200, 0.35);
8381
+ overflow: visible;
8382
+ }
8383
+
8384
+ .verdict-deal-locked-msg {
8385
+ margin: 10px 0 0;
8386
+ padding: 10px 12px;
8387
+ border-radius: 8px;
8388
+ font-size: 13px;
8389
+ font-weight: 600;
8390
+ color: rgba(255, 255, 255, 0.65);
8391
+ background: rgba(255, 80, 80, 0.08);
8392
+ border: 1px solid rgba(255, 80, 80, 0.2);
8393
+ }
8394
+
8395
+ .verdict-modal-footer {
8396
+ margin-top: 1rem;
8397
+ display: flex;
8398
+ flex-direction: column;
8399
+ gap: 0.75rem;
8400
+ }
8401
+
8402
+ .verdict-modal-actions {
8403
+ display: flex;
8404
+ flex-wrap: wrap;
8405
+ gap: 0.5rem;
8406
+ }
8407
+
8408
+ .verdict-modal-footer-secondary {
8409
+ display: flex;
8410
+ flex-wrap: wrap;
8411
+ gap: 0.5rem;
8412
  }
8413
 
8414
  .sc-verdict-card {
 
8563
  -webkit-line-clamp: unset;
8564
  }
8565
 
8566
+ .sc-prep-empty .sc-prep-retry-box {
8567
+ margin-top: 0.75rem;
8568
+ }
8569
+
8570
+ .sc-answers-empty {
8571
+ padding: 1rem;
8572
+ border-radius: 10px;
8573
+ background: rgba(255, 255, 255, 0.03);
8574
+ border: 1px solid rgba(255, 255, 255, 0.08);
8575
+ }
8576
+
8577
+ .sc-empty-title {
8578
+ margin: 0 0 6px;
8579
+ font-size: 15px;
8580
+ font-weight: 700;
8581
+ color: rgba(255, 255, 255, 0.9);
8582
+ }
8583
+
8584
+ .sc-empty-sub {
8585
+ margin: 0 0 12px;
8586
+ font-size: 13px;
8587
+ color: rgba(255, 255, 255, 0.6);
8588
+ line-height: 1.5;
8589
+ }
8590
+
8591
+ /* ---- HF Spaces / responsive polish ---- */
8592
+ @media (max-width: 1100px) {
8593
+ .briefing-grid,
8594
+ .briefing-grid-wide {
8595
+ grid-template-columns: 1fr;
8596
+ }
8597
+
8598
+ .sc-body-grid {
8599
+ grid-template-columns: 1fr;
8600
+ }
8601
+
8602
+ .sc-left-col {
8603
+ border-right: none;
8604
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
8605
+ }
8606
+
8607
+ .sc-hero-actions-secondary {
8608
+ margin-left: 0;
8609
+ width: 100%;
8610
+ }
8611
+ }
8612
+
8613
  @media (max-width: 960px) {
8614
  .sc-shell {
8615
+ padding: 14px 12px;
8616
  }
8617
 
8618
  .sc-hero-row {
8619
  flex-wrap: wrap;
8620
  }
8621
 
8622
+ .sc-hero-actions-row {
8623
+ flex-direction: column;
8624
+ align-items: stretch;
 
8625
  }
8626
 
8627
+ .sc-hero-actions-primary,
8628
+ .sc-hero-actions-secondary {
8629
  width: 100%;
8630
+ }
8631
+
8632
+ .sc-btn-conversation,
8633
+ .sc-btn-verdict {
8634
+ flex: 1;
8635
+ min-width: 140px;
8636
  text-align: center;
8637
  }
8638
 
8639
+ .sc-verdict-meta {
8640
  grid-template-columns: 1fr;
8641
  }
8642
 
8643
+ .verdict-modal-panel {
8644
+ max-width: 100%;
 
8645
  }
8646
+ }
8647
 
8648
+ @media (max-width: 768px) {
8649
+ .arena-title {
8650
+ font-size: clamp(1.75rem, 8vw, 2.5rem);
8651
+ }
8652
+
8653
+ .founder-silhouette,
8654
+ .judge-silhouette {
8655
+ transform: scale(0.72);
8656
+ opacity: 0.55;
8657
+ }
8658
+
8659
+ .sc-tabs {
8660
+ overflow-x: auto;
8661
+ flex-wrap: nowrap;
8662
+ -webkit-overflow-scrolling: touch;
8663
+ }
8664
+
8665
+ .sc-tab {
8666
+ flex-shrink: 0;
8667
+ }
8668
+
8669
+ .sc-dim-list .dimension-name {
8670
+ font-size: 13px;
8671
+ }
8672
+ }
8673
+
8674
+ @media (max-height: 760px) {
8675
+ .arena-landing {
8676
+ min-height: auto;
8677
+ padding: 0.5rem 0;
8678
  }
8679
 
8680
+ .arena-landing-content {
8681
+ padding-top: 0.75rem;
8682
+ padding-bottom: 0.75rem;
8683
+ }
8684
+
8685
+ .founder-silhouette,
8686
+ .judge-silhouette {
8687
+ transform: scale(0.68);
8688
+ opacity: 0.5;
8689
+ }
8690
+
8691
+ .briefing-header {
8692
+ margin-bottom: 0.45rem;
8693
+ }
8694
+
8695
+ .briefing-title {
8696
+ font-size: clamp(1.25rem, 2.5vw, 1.55rem);
8697
+ }
8698
+
8699
+ .briefing-subtitle {
8700
+ font-size: 0.82rem;
8701
+ }
8702
+
8703
+ .quick-pitch-textarea {
8704
+ min-height: 88px;
8705
+ max-height: 110px;
8706
+ }
8707
+
8708
+ .advanced-briefing-form textarea {
8709
+ min-height: 44px;
8710
+ max-height: 58px;
8711
+ }
8712
+
8713
+ .persona-grid,
8714
+ .difficulty-grid {
8715
+ gap: 0.45rem;
8716
+ }
8717
+
8718
+ .briefing-opponent-panel .persona-card,
8719
+ .briefing-opponent-panel .difficulty-card {
8720
+ padding: 0.45rem 0.5rem;
8721
+ }
8722
+
8723
+ .persona-card,
8724
+ .difficulty-card {
8725
+ padding: 0.55rem 0.65rem;
8726
+ }
8727
+
8728
+ .sc-hero {
8729
+ padding-top: 10px;
8730
+ padding-bottom: 10px;
8731
+ }
8732
+
8733
+ .sc-ring {
8734
+ width: 68px;
8735
+ height: 68px;
8736
+ min-width: 68px;
8737
+ min-height: 68px;
8738
+ }
8739
+
8740
+ .sc-ring .score-orb-value {
8741
+ font-size: 22px;
8742
+ }
8743
  }
8744
 
8745
  @media (max-width: 720px) {