gowtham0992 Codex commited on
Commit
eb0a695
·
1 Parent(s): aa9a872

Refine Jawbreaker safety-card UI

Browse files

Co-authored-by: Codex <codex@openai.com>

Files changed (3) hide show
  1. app.py +53 -17
  2. jawbreaker/render.py +42 -8
  3. style.css +397 -221
app.py CHANGED
@@ -13,7 +13,7 @@ from jawbreaker.analyzers import (
13
  prediction_to_analysis,
14
  validate_prediction,
15
  )
16
- from jawbreaker.render import render_analysis_html, render_memory_html
17
  from jawbreaker.schema import ScamAnalysis
18
 
19
  try:
@@ -28,6 +28,32 @@ EXAMPLES = [
28
  "Chase fraud alert: Did you attempt a $249.00 purchase at TARGET? Reply YES or NO.",
29
  ]
30
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  def app_theme() -> gr.Theme:
33
  return gr.themes.Soft(
@@ -200,6 +226,9 @@ def analyze_message(
200
  "message": message,
201
  "entry": memory_entry_from_analysis(message, analysis),
202
  }
 
 
 
203
  return render_analysis_html(message, analysis), analysis.trusted_person_message, render_memory_html(analysis, memory), memory, last_scan
204
 
205
 
@@ -245,6 +274,13 @@ def remember_current(message: str, memory: list[dict] | None, last_scan: dict |
245
  return render_save_status("Saved this scam pattern for this session."), render_current_memory(memory), memory
246
 
247
 
 
 
 
 
 
 
 
248
  def build_app() -> gr.Blocks:
249
  with gr.Blocks(title="Jawbreaker") as demo:
250
  memory_state = gr.State([])
@@ -253,12 +289,9 @@ def build_app() -> gr.Blocks:
253
  gr.HTML(
254
  """
255
  <section class="hero">
256
- <div>
257
- <p class="eyebrow">Backyard AI</p>
258
- <h1>Jawbreaker</h1>
259
- <p class="subtitle">Scam defense for someone you love.</p>
260
- </div>
261
- <div class="hero-badge">Small-model shield</div>
262
  </section>
263
  """
264
  )
@@ -279,9 +312,7 @@ def build_app() -> gr.Blocks:
279
  lines=10,
280
  max_lines=16,
281
  )
282
- with gr.Row():
283
- analyze = gr.Button("Check message", variant="primary")
284
- remember = gr.Button("Remember pattern")
285
  gr.Examples(examples=EXAMPLES, inputs=message, label="Try a sample")
286
 
287
  with gr.Column(scale=7):
@@ -303,19 +334,24 @@ def build_app() -> gr.Blocks:
303
  memory = gr.HTML("<div class='memory-card muted'>No scam memory saved yet.</div>")
304
  save_status = gr.HTML("<div class='save-status'></div>")
305
 
306
- analyze.click(
 
 
 
 
 
307
  fn=analyze_message,
308
  inputs=[message, memory_state],
309
  outputs=[result, trusted_message, memory, memory_state, last_scan_state],
310
  )
311
- remember.click(
312
- fn=remember_current,
313
- inputs=[message, memory_state, last_scan_state],
314
- outputs=[save_status, memory, memory_state],
315
- )
316
 
317
  return demo
318
 
319
 
320
  if __name__ == "__main__":
321
- build_app().launch(theme=app_theme(), css=app_css())
 
 
 
 
 
 
13
  prediction_to_analysis,
14
  validate_prediction,
15
  )
16
+ from jawbreaker.render import render_analysis_html, render_memory_html, render_scanning_html
17
  from jawbreaker.schema import ScamAnalysis
18
 
19
  try:
 
28
  "Chase fraud alert: Did you attempt a $249.00 purchase at TARGET? Reply YES or NO.",
29
  ]
30
 
31
+ FORCE_LIGHT_JS = """() => {
32
+ const forceLight = () => document.body.classList.remove('dark');
33
+ forceLight();
34
+ new MutationObserver(forceLight).observe(document.body, {
35
+ attributes: true,
36
+ attributeFilter: ['class'],
37
+ });
38
+ }"""
39
+
40
+ FORCE_LIGHT_HEAD = """
41
+ <script>
42
+ (() => {
43
+ const forceLight = () => {
44
+ if (document.body) document.body.classList.remove("dark");
45
+ };
46
+ window.addEventListener("DOMContentLoaded", () => {
47
+ forceLight();
48
+ new MutationObserver(forceLight).observe(document.body, {
49
+ attributes: true,
50
+ attributeFilter: ["class"],
51
+ });
52
+ });
53
+ })();
54
+ </script>
55
+ """
56
+
57
 
58
  def app_theme() -> gr.Theme:
59
  return gr.themes.Soft(
 
226
  "message": message,
227
  "entry": memory_entry_from_analysis(message, analysis),
228
  }
229
+ entry = last_scan["entry"]
230
+ if not memory or memory[-1].get("text") != entry.get("text"):
231
+ memory.append(entry)
232
  return render_analysis_html(message, analysis), analysis.trusted_person_message, render_memory_html(analysis, memory), memory, last_scan
233
 
234
 
 
274
  return render_save_status("Saved this scam pattern for this session."), render_current_memory(memory), memory
275
 
276
 
277
+ def start_scan(message: str) -> tuple[str, str]:
278
+ if not message.strip():
279
+ analysis = ScamAnalysis.from_heuristics(message, [])
280
+ return render_analysis_html(message, analysis), ""
281
+ return render_scanning_html(), ""
282
+
283
+
284
  def build_app() -> gr.Blocks:
285
  with gr.Blocks(title="Jawbreaker") as demo:
286
  memory_state = gr.State([])
 
289
  gr.HTML(
290
  """
291
  <section class="hero">
292
+ <p class="eyebrow">Backyard AI</p>
293
+ <h1>Jawbreaker</h1>
294
+ <p class="subtitle">Scam defense for someone you love.</p>
 
 
 
295
  </section>
296
  """
297
  )
 
312
  lines=10,
313
  max_lines=16,
314
  )
315
+ analyze = gr.Button("Check this message", variant="primary", elem_classes=["check-btn"])
 
 
316
  gr.Examples(examples=EXAMPLES, inputs=message, label="Try a sample")
317
 
318
  with gr.Column(scale=7):
 
334
  memory = gr.HTML("<div class='memory-card muted'>No scam memory saved yet.</div>")
335
  save_status = gr.HTML("<div class='save-status'></div>")
336
 
337
+ scan_event = analyze.click(
338
+ fn=start_scan,
339
+ inputs=message,
340
+ outputs=[result, trusted_message],
341
+ )
342
+ scan_event.then(
343
  fn=analyze_message,
344
  inputs=[message, memory_state],
345
  outputs=[result, trusted_message, memory, memory_state, last_scan_state],
346
  )
 
 
 
 
 
347
 
348
  return demo
349
 
350
 
351
  if __name__ == "__main__":
352
+ build_app().launch(
353
+ theme=app_theme(),
354
+ css=app_css(),
355
+ js=FORCE_LIGHT_JS,
356
+ head=FORCE_LIGHT_HEAD,
357
+ )
jawbreaker/render.py CHANGED
@@ -5,6 +5,21 @@ from html import escape
5
  from jawbreaker.schema import ScamAnalysis
6
 
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  def render_analysis_html(message: str, analysis: ScamAnalysis) -> str:
9
  if not message.strip():
10
  return """
@@ -19,36 +34,55 @@ def render_analysis_html(message: str, analysis: ScamAnalysis) -> str:
19
  dna_html = "".join(
20
  f"""
21
  <div class="dna-item">
22
- <div class="dna-label">{escape(label)}</div>
23
  <div class="dna-value">{escape(value)}</div>
24
  </div>
25
  """
26
  for label, value in analysis.scam_dna.items()
27
  )
28
  memory_html = f"<p><strong>Memory:</strong> {escape(analysis.similar_memory)}</p>" if analysis.similar_memory else ""
 
 
29
 
30
  return f"""
31
  <section class="verdict-card risk-{escape(analysis.risk_level)}">
32
- <div class="verdict-top">
33
- <span class="risk-pill">{escape(analysis.risk_level.replace("_", " "))}</span>
34
- <span class="scan-stamp">checked</span>
 
 
 
35
  </div>
36
- <p class="summary">{escape(analysis.summary)}</p>
37
  <div class="action-card">
38
- <strong>Safest next step</strong>
39
  <p>{escape(analysis.safest_action)}</p>
40
  </div>
41
- <h3>Scam DNA</h3>
 
42
  <div class="dna-grid">{dna_html}</div>
43
  <h3>Warning signs</h3>
44
  <div class="tactics">{tactic_html or "<span class='tactic'>none found</span>"}</div>
45
  {memory_html}
46
- <h3>Ask someone you trust</h3>
47
  <p class="trusted-inline">{escape(analysis.trusted_person_message)}</p>
48
  </section>
49
  """
50
 
51
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  def render_memory_html(analysis: ScamAnalysis, memory: list[dict]) -> str:
53
  if not memory:
54
  return "<div class='memory-card muted'><strong>Session memory</strong><p>No scam memory saved yet.</p></div>"
 
5
  from jawbreaker.schema import ScamAnalysis
6
 
7
 
8
+ DNA_LABELS = {
9
+ "Impersonates": "Who they pretend to be",
10
+ "Pressure": "How they pressure you",
11
+ "Ask": "What they want",
12
+ "Risk": "What could happen",
13
+ }
14
+
15
+ VERDICT_COPY = {
16
+ "dangerous": ("This looks dangerous", "danger"),
17
+ "suspicious": ("This looks suspicious", "warning"),
18
+ "needs_check": ("Check this before you act", "check"),
19
+ "safe": ("This looks safe", "safe"),
20
+ }
21
+
22
+
23
  def render_analysis_html(message: str, analysis: ScamAnalysis) -> str:
24
  if not message.strip():
25
  return """
 
34
  dna_html = "".join(
35
  f"""
36
  <div class="dna-item">
37
+ <div class="dna-label">{escape(DNA_LABELS.get(label, label))}</div>
38
  <div class="dna-value">{escape(value)}</div>
39
  </div>
40
  """
41
  for label, value in analysis.scam_dna.items()
42
  )
43
  memory_html = f"<p><strong>Memory:</strong> {escape(analysis.similar_memory)}</p>" if analysis.similar_memory else ""
44
+ verdict_title, icon_class = VERDICT_COPY[analysis.risk_level]
45
+ verdict_subtitle = analysis.summary.replace("This looks dangerous: likely ", "Likely ").rstrip(".")
46
 
47
  return f"""
48
  <section class="verdict-card risk-{escape(analysis.risk_level)}">
49
+ <div class="verdict-header">
50
+ <span class="verdict-icon verdict-icon-{escape(icon_class)}" aria-hidden="true"></span>
51
+ <div>
52
+ <h2 class="verdict-title">{escape(verdict_title)}</h2>
53
+ <p class="verdict-subtitle">{escape(verdict_subtitle)}.</p>
54
+ </div>
55
  </div>
 
56
  <div class="action-card">
57
+ <strong>What to do</strong>
58
  <p>{escape(analysis.safest_action)}</p>
59
  </div>
60
+ <h3>How this scam works</h3>
61
+ <p class="section-kicker">Scam DNA</p>
62
  <div class="dna-grid">{dna_html}</div>
63
  <h3>Warning signs</h3>
64
  <div class="tactics">{tactic_html or "<span class='tactic'>none found</span>"}</div>
65
  {memory_html}
66
+ <h3>Send this to someone you trust</h3>
67
  <p class="trusted-inline">{escape(analysis.trusted_person_message)}</p>
68
  </section>
69
  """
70
 
71
 
72
+ def render_scanning_html() -> str:
73
+ return """
74
+ <section class="scanning-state">
75
+ <div class="shield-pulse" aria-hidden="true"></div>
76
+ <h2>Scanning message...</h2>
77
+ <div class="scan-steps">
78
+ <div class="scan-step done">Reading message</div>
79
+ <div class="scan-step active">Checking for scam patterns</div>
80
+ <div class="scan-step pending">Building safety report</div>
81
+ </div>
82
+ </section>
83
+ """
84
+
85
+
86
  def render_memory_html(analysis: ScamAnalysis, memory: list[dict]) -> str:
87
  if not memory:
88
  return "<div class='memory-card muted'><strong>Session memory</strong><p>No scam memory saved yet.</p></div>"
style.css CHANGED
@@ -1,385 +1,522 @@
1
  :root {
2
- --paper: #fffaf0;
3
- --paper-strong: #fffdf7;
4
- --ink: #243040;
5
- --muted: #657080;
6
- --cranberry: #b42335;
7
- --cranberry-deep: #7f1d2d;
8
- --sage: #47745d;
9
- --sage-soft: #e8f3ec;
10
- --marigold: #c17a17;
11
- --blue: #2e5f83;
12
- --line: #dfd3bd;
13
- --shadow: 0 18px 45px rgba(62, 48, 30, 0.14);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
 
16
  html,
17
  body,
18
  body.dark {
19
  background:
20
- linear-gradient(90deg, rgba(180, 35, 53, 0.045) 1px, transparent 1px),
21
- linear-gradient(0deg, rgba(71, 116, 93, 0.04) 1px, transparent 1px),
22
- #f8efe0 !important;
23
- background-size: 34px 34px;
24
- color: var(--ink) !important;
25
  }
26
 
27
  .gradio-container {
28
- max-width: 1180px !important;
 
29
  margin: 0 auto !important;
30
- padding: 18px 18px 28px !important;
31
- font-family: ui-rounded, "Avenir Next", "Segoe UI", system-ui, sans-serif !important;
32
  background:
33
- linear-gradient(90deg, rgba(180, 35, 53, 0.045) 1px, transparent 1px),
34
- linear-gradient(0deg, rgba(71, 116, 93, 0.04) 1px, transparent 1px),
35
- #f8efe0 !important;
36
- background-size: 34px 34px !important;
37
- color: var(--ink) !important;
38
- min-height: 100vh !important;
39
- box-shadow: 0 0 0 100vmax #f8efe0;
40
  }
41
 
42
  .gradio-container main,
43
  .gradio-container .wrap,
44
- .gradio-container .contain {
45
- background: transparent !important;
46
- color: var(--ink) !important;
47
- }
48
-
49
  .gradio-container .block,
50
  .gradio-container .form {
51
  background: transparent !important;
52
  border-color: transparent !important;
53
- color: var(--ink) !important;
54
  }
55
 
56
  .gradio-container label,
57
  .gradio-container label span,
58
  .gradio-container .label-wrap,
59
  .gradio-container .label-wrap span {
60
- color: var(--ink) !important;
61
  }
62
 
63
- .hero {
64
- display: flex;
65
- align-items: end;
66
- justify-content: space-between;
67
- gap: 24px;
68
- padding: 28px 4px 20px;
69
- border-bottom: 2px solid var(--line);
70
- margin-bottom: 18px;
71
- position: relative;
72
  }
73
 
74
- .hero::after {
75
- content: "";
76
- position: absolute;
77
- left: 0;
78
- right: 0;
79
- bottom: -7px;
80
- height: 3px;
81
- background: repeating-linear-gradient(90deg, var(--cranberry) 0 18px, var(--sage) 18px 36px, var(--blue) 36px 54px);
82
- opacity: 0.55;
83
  }
84
 
85
  .eyebrow {
86
  margin: 0 0 8px;
87
- color: var(--cranberry-deep);
88
- font-size: 12px;
89
- font-weight: 900;
 
 
 
 
90
  text-transform: uppercase;
91
  }
92
 
93
  .hero h1 {
94
  margin: 0;
95
- color: var(--ink);
96
- font-size: 50px;
97
- line-height: 1;
98
  letter-spacing: 0;
 
99
  }
100
 
101
  .subtitle {
102
- margin: 10px 0 0;
103
- color: #3f4a5a;
104
- font-size: 20px;
105
- }
106
-
107
- .hero-badge {
108
- border: 1px solid #efb4bb;
109
- background: #fff5f6;
110
- color: var(--cranberry-deep);
111
- padding: 10px 12px;
112
- border-radius: 8px;
113
- font-weight: 900;
114
- white-space: nowrap;
115
- box-shadow: 0 4px 0 rgba(127, 29, 45, 0.12);
116
  }
117
 
118
  .main-grid {
119
- align-items: stretch;
120
- gap: 18px !important;
121
  }
122
 
123
  .scan-panel,
124
  .main-grid > .column:last-child {
125
- border: 1px solid var(--line);
126
- background: rgba(255, 253, 247, 0.94);
127
- border-radius: 8px;
128
- padding: 16px;
129
- box-shadow: var(--shadow);
 
 
 
 
 
 
 
130
  }
131
 
132
  .panel-kicker {
133
- display: flex;
134
- align-items: center;
135
- gap: 9px;
136
- margin: 0 0 10px;
137
- color: var(--cranberry-deep);
138
  font-size: 13px;
139
- font-weight: 900;
140
  text-transform: uppercase;
141
  }
142
 
143
- .panel-kicker span:not(.kicker-pin) {
144
- color: var(--cranberry-deep) !important;
145
  }
146
 
147
  .kicker-pin {
148
- width: 11px;
149
- height: 11px;
150
- border-radius: 50%;
151
- background: var(--cranberry);
152
- box-shadow: 0 0 0 4px #f9d4d8;
153
  }
154
 
155
  .scan-panel textarea {
156
- min-height: 260px !important;
157
- border-color: #cdbfaa !important;
158
- background: #fffdf7 !important;
159
- color: var(--ink) !important;
 
 
160
  font-size: 18px !important;
161
- line-height: 1.5 !important;
162
  }
163
 
164
- .scan-panel textarea:focus {
165
- border-color: var(--blue) !important;
166
- box-shadow: 0 0 0 3px rgba(46, 95, 131, 0.16) !important;
 
167
  }
168
 
169
- .gradio-container label span.svelte-jdcl7l {
 
 
 
 
 
170
  color: #ffffff !important;
 
 
 
171
  }
172
 
173
- button.primary {
174
- background: var(--cranberry) !important;
175
- border-color: var(--cranberry) !important;
176
- color: white !important;
177
- font-weight: 900 !important;
178
  }
179
 
180
- button.primary:hover {
181
- background: var(--cranberry-deep) !important;
 
 
 
182
  }
183
 
184
  .verdict-card,
185
  .empty-state,
 
186
  .memory-card {
187
- border: 1px solid var(--line);
188
- border-radius: 8px;
189
- padding: 18px;
190
- background: var(--paper-strong);
 
191
  }
192
 
193
  .empty-state {
194
  min-height: 260px;
195
  display: flex;
196
- flex-direction: column;
197
- justify-content: center;
198
  align-items: flex-start;
199
- background:
200
- linear-gradient(135deg, rgba(71, 116, 93, 0.1), transparent 55%),
201
- var(--paper-strong);
202
  }
203
 
204
  .empty-state h2,
205
- .verdict-card h2 {
206
- color: var(--ink);
207
- margin-top: 0;
 
 
208
  }
209
 
210
  .empty-state p {
211
- color: #435163;
 
 
212
  }
213
 
214
- .shield-mark {
215
- width: 46px;
216
- height: 54px;
217
- margin-bottom: 14px;
218
- background: var(--sage);
219
- clip-path: polygon(50% 0, 92% 14%, 84% 72%, 50% 100%, 16% 72%, 8% 14%);
220
- box-shadow: inset 0 -10px 0 rgba(0, 0, 0, 0.12);
 
 
 
221
  }
222
 
223
- .verdict-card {
224
- animation: card-in 180ms ease-out;
 
 
 
 
 
 
225
  }
226
 
227
- .verdict-top {
 
228
  display: flex;
229
  align-items: center;
230
- justify-content: space-between;
231
- gap: 12px;
232
- margin-bottom: 14px;
 
 
 
 
 
 
233
  }
234
 
235
- .scan-stamp {
236
- border: 1px dashed currentColor;
237
- border-radius: 8px;
238
- color: var(--muted);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  font-size: 12px;
240
  font-weight: 900;
241
- padding: 6px 8px;
242
- text-transform: uppercase;
243
- transform: rotate(-2deg);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
 
246
  .risk-dangerous {
247
- border-color: #e9a6ad;
248
- background: #fff4f5;
249
  }
250
 
251
  .risk-suspicious {
252
- border-color: #ecc783;
253
- background: #fff8e8;
254
  }
255
 
256
  .risk-needs_check {
257
- border-color: #d8c77c;
258
- background: #fffbe7;
259
  }
260
 
261
  .risk-safe {
262
- border-color: #9fc7ab;
263
- background: var(--sage-soft);
264
  }
265
 
266
- .risk-pill {
267
- display: inline-block;
268
- padding: 8px 11px;
269
- border-radius: 8px;
270
- background: var(--ink);
271
- color: #ffffff;
272
- font-size: 13px;
 
 
 
 
 
 
 
 
 
273
  font-weight: 900;
274
- text-transform: uppercase;
275
  }
276
 
277
- .risk-dangerous .risk-pill {
278
- background: var(--cranberry);
 
279
  }
280
 
281
- .risk-suspicious .risk-pill {
282
- background: var(--marigold);
283
  }
284
 
285
- .risk-needs_check .risk-pill {
286
- background: var(--blue);
 
287
  }
288
 
289
- .risk-safe .risk-pill {
290
- background: var(--sage);
291
  }
292
 
293
- .summary {
294
- color: var(--ink);
295
- font-size: 21px;
296
- line-height: 1.35;
297
- margin: 0 0 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
298
  }
299
 
300
  .action-card {
301
- margin: 16px 0;
302
- border-left: 5px solid var(--ink);
303
- background: rgba(255, 255, 255, 0.62);
304
- padding: 14px 14px 12px;
305
- border-radius: 6px;
306
  }
307
 
308
  .action-card strong,
309
  .action-card p,
310
  .verdict-card p {
311
- color: var(--ink);
312
  }
313
 
314
- .risk-dangerous .action-card {
315
- border-left-color: var(--cranberry);
 
316
  }
317
 
318
- .risk-safe .action-card {
319
- border-left-color: var(--sage);
 
 
 
320
  }
321
 
322
- .verdict-card h3 {
323
- margin: 18px 0 8px;
324
- color: #314052;
325
- font-size: 15px;
 
 
326
  }
327
 
328
  .dna-grid {
329
  display: grid;
330
  grid-template-columns: repeat(2, minmax(0, 1fr));
331
- gap: 10px;
332
- margin-top: 12px;
 
333
  }
334
 
335
  .dna-item {
336
- border: 1px solid #e3d7c5;
337
- background: rgba(255, 255, 255, 0.65);
338
- border-radius: 8px;
 
339
  padding: 12px;
340
  }
341
 
 
 
 
 
 
 
 
 
342
  .dna-label {
343
- color: var(--muted);
344
- font-size: 12px;
345
- font-weight: 900;
 
346
  text-transform: uppercase;
347
  }
348
 
349
  .dna-value {
350
- margin-top: 6px;
351
- color: var(--ink);
352
- font-size: 16px;
353
  font-weight: 800;
 
354
  }
355
 
356
  .tactics {
357
  display: flex;
358
  flex-wrap: wrap;
359
  gap: 8px;
360
- margin-top: 12px;
361
  }
362
 
363
  .tactic {
364
- border: 1px solid #c9bca9;
365
  border-radius: 999px;
366
- padding: 7px 10px;
367
- background: #ffffff;
368
- color: #273447;
369
  font-size: 13px;
370
- font-weight: 800;
 
 
 
 
 
 
 
 
 
 
 
 
 
371
  }
372
 
373
  .trusted-inline {
374
  margin-bottom: 0;
 
 
 
 
 
375
  }
376
 
377
  .trusted-output textarea {
378
- background: #fffdf7 !important;
379
- border-color: #d8cbb6 !important;
380
- color: var(--ink) !important;
 
381
  font-size: 15px !important;
382
- line-height: 1.45 !important;
383
  }
384
 
385
  .trusted-output label span.svelte-jdcl7l {
@@ -388,9 +525,9 @@ button.primary:hover {
388
  }
389
 
390
  .save-status {
391
- min-height: 20px;
392
- margin-top: 14px;
393
- color: var(--blue);
394
  font-size: 14px;
395
  font-weight: 800;
396
  }
@@ -400,7 +537,7 @@ button.primary:hover {
400
  }
401
 
402
  .memory-card strong {
403
- color: var(--blue);
404
  }
405
 
406
  .memory-card ul {
@@ -409,11 +546,12 @@ button.primary:hover {
409
  }
410
 
411
  .memory-card li {
412
- margin: 6px 0;
 
413
  }
414
 
415
  .muted {
416
- color: var(--muted);
417
  }
418
 
419
  @keyframes card-in {
@@ -427,35 +565,73 @@ button.primary:hover {
427
  }
428
  }
429
 
 
 
 
 
 
 
 
 
 
 
 
 
430
  @media (max-width: 760px) {
431
  .gradio-container {
432
- padding: 12px !important;
433
  }
434
 
435
  .hero {
436
- align-items: start;
437
- flex-direction: column;
438
  }
439
 
440
  .hero h1 {
441
- font-size: 40px;
442
  }
443
 
444
- .hero-badge {
445
- white-space: normal;
446
  }
447
 
448
  .scan-panel,
449
  .main-grid > .column:last-child {
450
- padding: 12px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  }
452
 
453
  .dna-grid {
454
  grid-template-columns: 1fr;
455
  }
456
 
457
- .verdict-top {
458
- align-items: flex-start;
459
- flex-direction: column;
 
 
 
 
 
 
460
  }
461
  }
 
1
  :root {
2
+ --body-background-fill: #fafaf7 !important;
3
+ --background-fill-primary: #ffffff !important;
4
+ --background-fill-secondary: #fafaf7 !important;
5
+ --jb-bg: #fafaf7;
6
+ --jb-warm: #fff8ea;
7
+ --jb-surface: #ffffff;
8
+ --jb-border: rgba(29, 29, 31, 0.09);
9
+ --jb-shadow: 0 1px 3px rgba(0, 0, 0, 0.04), 0 14px 34px rgba(62, 45, 27, 0.08);
10
+ --jb-radius: 14px;
11
+ --jb-text: #1d1d1f;
12
+ --jb-secondary: #6f6f76;
13
+ --jb-dangerous-bg: #fff5f5;
14
+ --jb-dangerous: #dc2626;
15
+ --jb-dangerous-icon: #fee2e2;
16
+ --jb-suspicious-bg: #fffbeb;
17
+ --jb-suspicious: #d97706;
18
+ --jb-suspicious-icon: #fef3c7;
19
+ --jb-needscheck-bg: #fff7ed;
20
+ --jb-needscheck: #ea580c;
21
+ --jb-needscheck-icon: #ffedd5;
22
+ --jb-safe-bg: #f0fdf4;
23
+ --jb-safe: #16a34a;
24
+ --jb-safe-icon: #dcfce7;
25
+ --jb-rose: #c7515a;
26
+ --jb-rose-hover: #a43f48;
27
+ --jb-blue: #2563eb;
28
+ color-scheme: light !important;
29
+ }
30
+
31
+ .dark {
32
+ --body-background-fill: #fafaf7 !important;
33
+ --background-fill-primary: #ffffff !important;
34
+ --background-fill-secondary: #fafaf7 !important;
35
+ color-scheme: light !important;
36
  }
37
 
38
  html,
39
  body,
40
  body.dark {
41
  background:
42
+ radial-gradient(circle at 50% 0%, rgba(255, 248, 234, 0.9), rgba(250, 250, 247, 0.98) 42%),
43
+ var(--jb-bg) !important;
44
+ color: var(--jb-text) !important;
 
 
45
  }
46
 
47
  .gradio-container {
48
+ max-width: 1080px !important;
49
+ min-height: 100vh !important;
50
  margin: 0 auto !important;
51
+ padding: 42px 24px 30px !important;
 
52
  background:
53
+ radial-gradient(circle at 50% 0%, rgba(255, 248, 234, 0.9), rgba(250, 250, 247, 0.98) 42%),
54
+ var(--jb-bg) !important;
55
+ box-shadow: 0 0 0 100vmax var(--jb-bg);
56
+ color: var(--jb-text) !important;
57
+ font-family: Inter, ui-rounded, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif !important;
 
 
58
  }
59
 
60
  .gradio-container main,
61
  .gradio-container .wrap,
62
+ .gradio-container .contain,
 
 
 
 
63
  .gradio-container .block,
64
  .gradio-container .form {
65
  background: transparent !important;
66
  border-color: transparent !important;
67
+ color: var(--jb-text) !important;
68
  }
69
 
70
  .gradio-container label,
71
  .gradio-container label span,
72
  .gradio-container .label-wrap,
73
  .gradio-container .label-wrap span {
74
+ color: var(--jb-text) !important;
75
  }
76
 
77
+ .gradio-container label span.svelte-jdcl7l {
78
+ color: #ffffff !important;
 
 
 
 
 
 
 
79
  }
80
 
81
+ .hero {
82
+ display: flex;
83
+ align-items: center;
84
+ flex-direction: column;
85
+ margin: 0 auto 34px;
86
+ text-align: center;
 
 
 
87
  }
88
 
89
  .eyebrow {
90
  margin: 0 0 8px;
91
+ border-radius: 999px;
92
+ background: #f8d8dc;
93
+ color: #9f2f38;
94
+ padding: 5px 10px;
95
+ font-size: 13px;
96
+ font-weight: 850;
97
+ line-height: 1;
98
  text-transform: uppercase;
99
  }
100
 
101
  .hero h1 {
102
  margin: 0;
103
+ color: var(--jb-text);
104
+ font-size: 60px;
105
+ font-weight: 850;
106
  letter-spacing: 0;
107
+ line-height: 0.98;
108
  }
109
 
110
  .subtitle {
111
+ max-width: 560px;
112
+ margin: 12px 0 0;
113
+ color: #5f554f;
114
+ font-size: 21px;
115
+ line-height: 1.35;
 
 
 
 
 
 
 
 
 
116
  }
117
 
118
  .main-grid {
119
+ align-items: start;
120
+ gap: 28px !important;
121
  }
122
 
123
  .scan-panel,
124
  .main-grid > .column:last-child {
125
+ border: 1px solid var(--jb-border);
126
+ border-radius: var(--jb-radius);
127
+ background: rgba(255, 255, 255, 0.82);
128
+ box-shadow: var(--jb-shadow);
129
+ padding: 18px;
130
+ }
131
+
132
+ .main-grid > .column:last-child {
133
+ background: transparent;
134
+ border-color: transparent;
135
+ box-shadow: none;
136
+ padding: 0;
137
  }
138
 
139
  .panel-kicker {
140
+ margin: 2px 0 12px;
141
+ color: #6b5144;
 
 
 
142
  font-size: 13px;
143
+ font-weight: 850;
144
  text-transform: uppercase;
145
  }
146
 
147
+ .panel-kicker span {
148
+ color: #6b5144 !important;
149
  }
150
 
151
  .kicker-pin {
152
+ display: none;
 
 
 
 
153
  }
154
 
155
  .scan-panel textarea {
156
+ min-height: 305px !important;
157
+ border: 1px solid rgba(80, 65, 48, 0.24) !important;
158
+ border-radius: 10px !important;
159
+ background: #ffffff !important;
160
+ color: var(--jb-text) !important;
161
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.03) !important;
162
  font-size: 18px !important;
163
+ line-height: 1.42 !important;
164
  }
165
 
166
+ .scan-panel textarea:focus,
167
+ .trusted-output textarea:focus {
168
+ border-color: var(--jb-rose) !important;
169
+ box-shadow: 0 0 0 3px rgba(199, 81, 90, 0.15) !important;
170
  }
171
 
172
+ .check-btn,
173
+ button.primary {
174
+ min-height: 48px !important;
175
+ border: 0 !important;
176
+ border-radius: 999px !important;
177
+ background: linear-gradient(135deg, #db6c70, var(--jb-rose)) !important;
178
  color: #ffffff !important;
179
+ box-shadow: 0 8px 20px rgba(199, 81, 90, 0.22) !important;
180
+ font-size: 16px !important;
181
+ font-weight: 800 !important;
182
  }
183
 
184
+ .check-btn:hover,
185
+ button.primary:hover {
186
+ background: linear-gradient(135deg, #cf5b62, var(--jb-rose-hover)) !important;
 
 
187
  }
188
 
189
+ .scan-panel button:not(.primary) {
190
+ border-color: rgba(29, 29, 31, 0.12) !important;
191
+ border-radius: 10px !important;
192
+ background: rgba(255, 255, 255, 0.72) !important;
193
+ color: var(--jb-text) !important;
194
  }
195
 
196
  .verdict-card,
197
  .empty-state,
198
+ .scanning-state,
199
  .memory-card {
200
+ border: 1px solid var(--jb-border);
201
+ border-radius: var(--jb-radius);
202
+ background: var(--jb-surface);
203
+ box-shadow: var(--jb-shadow);
204
+ padding: 22px;
205
  }
206
 
207
  .empty-state {
208
  min-height: 260px;
209
  display: flex;
 
 
210
  align-items: flex-start;
211
+ justify-content: center;
212
+ flex-direction: column;
 
213
  }
214
 
215
  .empty-state h2,
216
+ .scanning-state h2 {
217
+ margin: 0 0 8px;
218
+ color: var(--jb-text);
219
+ font-size: 24px;
220
+ line-height: 1.2;
221
  }
222
 
223
  .empty-state p {
224
+ margin: 0;
225
+ color: var(--jb-secondary);
226
+ font-size: 15px;
227
  }
228
 
229
+ .shield-mark,
230
+ .shield-pulse {
231
+ width: 74px;
232
+ height: 74px;
233
+ margin: 0 auto 18px;
234
+ border-radius: 50%;
235
+ background:
236
+ linear-gradient(135deg, rgba(199, 81, 90, 0.95), rgba(199, 81, 90, 0.66)),
237
+ #f8d8dc;
238
+ position: relative;
239
  }
240
 
241
+ .shield-mark::before,
242
+ .shield-pulse::before {
243
+ content: "";
244
+ position: absolute;
245
+ inset: 20px 24px 17px;
246
+ border: 4px solid #ffffff;
247
+ border-radius: 3px 3px 12px 12px;
248
+ clip-path: polygon(50% 0, 100% 18%, 86% 75%, 50% 100%, 14% 75%, 0 18%);
249
  }
250
 
251
+ .scanning-state {
252
+ min-height: 360px;
253
  display: flex;
254
  align-items: center;
255
+ justify-content: center;
256
+ flex-direction: column;
257
+ background: linear-gradient(180deg, #fffefd, var(--jb-warm));
258
+ text-align: center;
259
+ }
260
+
261
+ .shield-pulse {
262
+ animation: pulse 1.5s ease-in-out infinite;
263
+ box-shadow: 0 0 0 18px rgba(199, 81, 90, 0.08), 0 0 0 36px rgba(199, 81, 90, 0.04);
264
  }
265
 
266
+ .scan-steps {
267
+ width: min(100%, 330px);
268
+ margin-top: 18px;
269
+ border: 1px solid rgba(80, 65, 48, 0.14);
270
+ border-radius: 12px;
271
+ background: rgba(255, 255, 255, 0.64);
272
+ padding: 12px;
273
+ text-align: left;
274
+ }
275
+
276
+ .scan-step {
277
+ display: flex;
278
+ align-items: center;
279
+ gap: 10px;
280
+ padding: 9px 10px;
281
+ color: #4b423d;
282
+ font-size: 15px;
283
+ }
284
+
285
+ .scan-step::before {
286
+ width: 18px;
287
+ height: 18px;
288
+ border-radius: 50%;
289
+ display: inline-grid;
290
+ place-items: center;
291
+ flex: 0 0 18px;
292
+ color: #ffffff;
293
  font-size: 12px;
294
  font-weight: 900;
295
+ }
296
+
297
+ .scan-step.done::before {
298
+ content: "✓";
299
+ background: var(--jb-safe);
300
+ }
301
+
302
+ .scan-step.active::before {
303
+ content: "";
304
+ background: var(--jb-rose);
305
+ box-shadow: 0 0 12px rgba(199, 81, 90, 0.55);
306
+ }
307
+
308
+ .scan-step.pending::before {
309
+ content: "";
310
+ background: #c8c5be;
311
+ }
312
+
313
+ .verdict-card {
314
+ animation: card-in 180ms ease-out;
315
  }
316
 
317
  .risk-dangerous {
318
+ background: var(--jb-dangerous-bg);
 
319
  }
320
 
321
  .risk-suspicious {
322
+ background: var(--jb-suspicious-bg);
 
323
  }
324
 
325
  .risk-needs_check {
326
+ background: var(--jb-needscheck-bg);
 
327
  }
328
 
329
  .risk-safe {
330
+ background: var(--jb-safe-bg);
 
331
  }
332
 
333
+ .verdict-header {
334
+ display: grid;
335
+ grid-template-columns: auto 1fr;
336
+ align-items: center;
337
+ gap: 16px;
338
+ margin-bottom: 18px;
339
+ }
340
+
341
+ .verdict-icon {
342
+ width: 58px;
343
+ height: 58px;
344
+ border-radius: 50%;
345
+ display: inline-grid;
346
+ place-items: center;
347
+ flex: 0 0 58px;
348
+ font-size: 28px;
349
  font-weight: 900;
 
350
  }
351
 
352
+ .verdict-icon-danger {
353
+ background: var(--jb-dangerous-icon);
354
+ color: var(--jb-dangerous);
355
  }
356
 
357
+ .verdict-icon-danger::before {
358
+ content: "x";
359
  }
360
 
361
+ .verdict-icon-warning {
362
+ background: var(--jb-suspicious-icon);
363
+ color: var(--jb-suspicious);
364
  }
365
 
366
+ .verdict-icon-warning::before {
367
+ content: "!";
368
  }
369
 
370
+ .verdict-icon-check {
371
+ background: var(--jb-needscheck-icon);
372
+ color: var(--jb-needscheck);
373
+ }
374
+
375
+ .verdict-icon-check::before {
376
+ content: "?";
377
+ }
378
+
379
+ .verdict-icon-safe {
380
+ background: var(--jb-safe-icon);
381
+ color: var(--jb-safe);
382
+ }
383
+
384
+ .verdict-icon-safe::before {
385
+ content: "✓";
386
+ }
387
+
388
+ .verdict-title {
389
+ margin: 0;
390
+ color: var(--jb-text);
391
+ font-size: 28px;
392
+ line-height: 1.08;
393
+ }
394
+
395
+ .verdict-subtitle {
396
+ margin: 4px 0 0;
397
+ color: var(--jb-secondary);
398
+ font-size: 16px;
399
  }
400
 
401
  .action-card {
402
+ margin: 18px 0;
403
+ border-left: 4px solid var(--jb-safe);
404
+ border-radius: 12px;
405
+ background: #eaf6ed;
406
+ padding: 15px 18px;
407
  }
408
 
409
  .action-card strong,
410
  .action-card p,
411
  .verdict-card p {
412
+ color: var(--jb-text);
413
  }
414
 
415
+ .action-card p {
416
+ margin: 5px 0 0;
417
+ line-height: 1.45;
418
  }
419
 
420
+ .verdict-card h3 {
421
+ margin: 20px 0 8px;
422
+ color: var(--jb-text);
423
+ font-size: 17px;
424
+ line-height: 1.2;
425
  }
426
 
427
+ .section-kicker {
428
+ margin: 0 0 10px !important;
429
+ color: var(--jb-secondary) !important;
430
+ font-size: 12px !important;
431
+ font-weight: 850;
432
+ text-transform: uppercase;
433
  }
434
 
435
  .dna-grid {
436
  display: grid;
437
  grid-template-columns: repeat(2, minmax(0, 1fr));
438
+ border: 1px solid rgba(29, 29, 31, 0.09);
439
+ border-radius: 12px;
440
+ overflow: hidden;
441
  }
442
 
443
  .dna-item {
444
+ min-height: 90px;
445
+ border-right: 1px solid rgba(29, 29, 31, 0.08);
446
+ border-bottom: 1px solid rgba(29, 29, 31, 0.08);
447
+ background: rgba(255, 255, 255, 0.62);
448
  padding: 12px;
449
  }
450
 
451
+ .dna-item:nth-child(2n) {
452
+ border-right: 0;
453
+ }
454
+
455
+ .dna-item:nth-last-child(-n + 2) {
456
+ border-bottom: 0;
457
+ }
458
+
459
  .dna-label {
460
+ color: var(--jb-secondary);
461
+ font-size: 10px;
462
+ font-weight: 850;
463
+ line-height: 1.2;
464
  text-transform: uppercase;
465
  }
466
 
467
  .dna-value {
468
+ margin-top: 7px;
469
+ color: var(--jb-text);
470
+ font-size: 17px;
471
  font-weight: 800;
472
+ line-height: 1.22;
473
  }
474
 
475
  .tactics {
476
  display: flex;
477
  flex-wrap: wrap;
478
  gap: 8px;
 
479
  }
480
 
481
  .tactic {
482
+ border: 1px solid #e5e7eb;
483
  border-radius: 999px;
484
+ background: #f3f4f6;
485
+ color: #6b7280;
486
+ padding: 6px 10px;
487
  font-size: 13px;
488
+ font-weight: 700;
489
+ }
490
+
491
+ .risk-dangerous .tactic {
492
+ border-color: #fecaca;
493
+ background: #fee2e2;
494
+ color: #991b1b;
495
+ }
496
+
497
+ .risk-suspicious .tactic,
498
+ .risk-needs_check .tactic {
499
+ border-color: #fed7aa;
500
+ background: #ffedd5;
501
+ color: #9a3412;
502
  }
503
 
504
  .trusted-inline {
505
  margin-bottom: 0;
506
+ line-height: 1.45;
507
+ }
508
+
509
+ .trusted-output {
510
+ margin-top: 14px;
511
  }
512
 
513
  .trusted-output textarea {
514
+ border-color: rgba(29, 29, 31, 0.1) !important;
515
+ border-radius: 12px !important;
516
+ background: linear-gradient(135deg, #fffdf8, #fff5df) !important;
517
+ color: var(--jb-text) !important;
518
  font-size: 15px !important;
519
+ line-height: 1.42 !important;
520
  }
521
 
522
  .trusted-output label span.svelte-jdcl7l {
 
525
  }
526
 
527
  .save-status {
528
+ min-height: 18px;
529
+ margin-top: 10px;
530
+ color: var(--jb-blue);
531
  font-size: 14px;
532
  font-weight: 800;
533
  }
 
537
  }
538
 
539
  .memory-card strong {
540
+ color: var(--jb-text);
541
  }
542
 
543
  .memory-card ul {
 
546
  }
547
 
548
  .memory-card li {
549
+ margin: 7px 0;
550
+ color: var(--jb-text);
551
  }
552
 
553
  .muted {
554
+ color: var(--jb-secondary);
555
  }
556
 
557
  @keyframes card-in {
 
565
  }
566
  }
567
 
568
+ @keyframes pulse {
569
+ 0%,
570
+ 100% {
571
+ opacity: 1;
572
+ transform: scale(1);
573
+ }
574
+ 50% {
575
+ opacity: 0.84;
576
+ transform: scale(1.07);
577
+ }
578
+ }
579
+
580
  @media (max-width: 760px) {
581
  .gradio-container {
582
+ padding: 28px 14px 24px !important;
583
  }
584
 
585
  .hero {
586
+ margin-bottom: 26px;
 
587
  }
588
 
589
  .hero h1 {
590
+ font-size: 44px;
591
  }
592
 
593
+ .subtitle {
594
+ font-size: 19px;
595
  }
596
 
597
  .scan-panel,
598
  .main-grid > .column:last-child {
599
+ padding: 14px;
600
+ }
601
+
602
+ .main-grid > .column:last-child {
603
+ padding: 0;
604
+ }
605
+
606
+ .scan-panel textarea {
607
+ min-height: 270px !important;
608
+ }
609
+
610
+ .verdict-header {
611
+ grid-template-columns: 1fr;
612
+ text-align: center;
613
+ }
614
+
615
+ .verdict-icon {
616
+ margin: 0 auto;
617
+ }
618
+
619
+ .verdict-title {
620
+ font-size: 27px;
621
  }
622
 
623
  .dna-grid {
624
  grid-template-columns: 1fr;
625
  }
626
 
627
+ .dna-item,
628
+ .dna-item:nth-child(2n),
629
+ .dna-item:nth-last-child(-n + 2) {
630
+ border-right: 0;
631
+ border-bottom: 1px solid rgba(29, 29, 31, 0.08);
632
+ }
633
+
634
+ .dna-item:last-child {
635
+ border-bottom: 0;
636
  }
637
  }