cjc0013 commited on
Commit
148cf0c
·
verified ·
1 Parent(s): 17a4606

Simplify public disclosure reader UI

Browse files
Files changed (3) hide show
  1. README.md +4 -5
  2. app.py +539 -131
  3. space_bundle_manifest.json +5 -5
README.md CHANGED
@@ -1,6 +1,5 @@
1
  ---
2
- title: Senator Financial Disclosure Explorer
3
- emoji: 📊
4
  colorFrom: green
5
  colorTo: yellow
6
  sdk: gradio
@@ -10,8 +9,8 @@ pinned: false
10
  license: cc-by-4.0
11
  ---
12
 
13
- # Senator Financial Disclosure Explorer
14
 
15
- Public-interest explorer for official Senate financial disclosures: reported wealth ranges, disclosed transactions, filing delays, and cautious market-context checks.
16
 
17
- This Space does not claim intent, illegality, causality, insider trading, realized private returns, or release-grade above-market alpha.
 
1
  ---
2
+ title: Senate Disclosure Reader
 
3
  colorFrom: green
4
  colorTo: yellow
5
  sdk: gradio
 
9
  license: cc-by-4.0
10
  ---
11
 
12
+ # Senate Disclosure Reader
13
 
14
+ Plain-English reader for official Senate financial disclosures: search filers, reported wealth ranges, transaction rows, filing dates, and source receipts.
15
 
16
+ This Space does not claim intent, illegality, causality, insider trading, realized private returns, or market-beating performance.
app.py CHANGED
@@ -82,8 +82,8 @@ def window_label(value: str) -> str:
82
 
83
  def claim_label(value: str) -> str:
84
  labels = {
85
- "blocked_market_data_policy": "Not a public alpha claim: stronger market data required",
86
- "blocked": "Not a public alpha claim",
87
  }
88
  return labels.get(str(value), str(value).replace("_", " "))
89
 
@@ -101,6 +101,7 @@ RETURN_HANDOFF = pd.read_csv(DATA / "reporter_handoff" / "return_test_summary_ha
101
  TRADES = read_jsonl_gz("trades.jsonl.gz")
102
  WEALTH = read_jsonl_gz("net_worth_intervals.jsonl.gz")
103
  FILINGS = read_jsonl_gz("filings.jsonl.gz")
 
104
 
105
  TRADES["transaction_year"] = pd.to_datetime(TRADES["transaction_date"], errors="coerce").dt.year
106
  TRADES["filing_lag_days_num"] = pd.to_numeric(TRADES.get("filing_lag_days"), errors="coerce")
@@ -111,26 +112,143 @@ for col in ["net_worth_min", "net_worth_midpoint", "net_worth_max", "uncertainty
111
  WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
112
 
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  def hero_html() -> str:
115
  return """
116
- <section class="hero-panel" style="background:#ffffff;border:1px solid #cbd5e1;border-radius:12px;padding:22px 24px;margin:12px 0 16px;box-shadow:0 16px 40px rgba(15,23,42,.08);">
117
- <div style="display:flex;gap:22px;align-items:flex-start;justify-content:space-between;flex-wrap:wrap;">
118
- <div style="min-width:280px;flex:1 1 620px;">
119
- <div style="color:#0f766e;font-size:.88rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;margin-bottom:8px;">Official Senate financial disclosure data</div>
120
- <h1 style="color:#0f172a;font-size:2.35rem;line-height:1.08;font-weight:850;margin:0 0 10px;">Senator Financial Disclosure Explorer</h1>
121
- <p style="color:#334155;font-size:1.08rem;line-height:1.6;max-width:850px;margin:0;">
122
- This turns public Senate disclosure forms into searchable visuals: reported wealth ranges, disclosed transactions,
123
- filing delays, and cautious market-context checks. It is built to show what the records say, where the uncertainty is,
124
- and what cannot be concluded from the disclosures alone.
125
- </p>
126
- </div>
127
- <div style="background:#f8fafc;border:1px solid #d7dee8;border-radius:10px;padding:14px 16px;min-width:250px;flex:0 1 310px;">
128
- <div style="color:#64748b;font-size:.84rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;">Current evidence status</div>
129
- <div style="color:#0f172a;font-size:1.35rem;font-weight:850;line-height:1.2;margin-top:5px;">Disclosure explorer: ready</div>
130
- <div style="color:#8a4b00;font-size:1rem;font-weight:800;margin-top:10px;">Market-beating proof: not established</div>
131
- <p style="color:#475569;font-size:.95rem;line-height:1.45;margin:8px 0 0;">The market checks are exploratory because the public bundle does not use a licensed total-return source with delisting handling.</p>
132
- </div>
133
- </div>
134
  </section>
135
  """
136
 
@@ -138,87 +256,87 @@ def hero_html() -> str:
138
  def plain_english_cards_html() -> str:
139
  cards = [
140
  (
141
- "What this is",
142
- "A public-records browser for Senate financial disclosures: filings, reported asset ranges, liabilities, income rows, and disclosed transactions.",
143
  "#e8f7f3",
144
  "#0f766e",
145
  ),
146
  (
147
- "Why wealth is shown as ranges",
148
- "Disclosures use broad dollar brackets. The app shows low, midpoint, and high estimates so the uncertainty stays visible.",
149
  "#eef4ff",
150
  "#3155b7",
151
  ),
152
  (
153
- "What the market charts mean",
154
- "They compare disclosed timing to simple benchmarks under stated assumptions. They do not prove motive, private profit, illegality, or insider trading.",
155
  "#fff7e8",
156
  "#8a4b00",
157
  ),
158
  ]
159
  cells = "".join(
160
  f"""
161
- <div style="background:{bg};border:1px solid rgba(15,23,42,.12);border-radius:10px;padding:15px 16px;min-height:142px;">
162
- <div style="color:{accent};font-size:1.02rem;font-weight:850;margin-bottom:8px;">{title}</div>
163
- <div style="color:#243447;font-size:1rem;line-height:1.5;">{body}</div>
164
  </div>
165
  """
166
  for title, body, bg, accent in cards
167
  )
168
- return f"<div class='plain-cards' style='display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:14px 0 18px;'>{cells}</div>"
169
 
170
 
171
  def metric_html() -> str:
172
  counts = MANIFEST.get("counts", {})
173
  dataset = counts.get("dataset", {})
174
- returns = counts.get("return_tests", {})
175
  items = [
176
- ("Disclosed transaction rows", fmt_int(dataset.get("trades", "")), "transactions listed in Senate reports"),
177
- ("Financial disclosure filings", fmt_int(dataset.get("filings", "")), "annual reports, PTRs, and amendments"),
178
- ("Reported asset rows", fmt_int(dataset.get("annual_assets", "")), "asset bracket rows parsed from filings"),
179
- ("Wealth range estimates", fmt_int(dataset.get("net_worth_intervals", "")), "low/mid/high disclosure estimates"),
180
- ("Market-context event rows", fmt_int(returns.get("return_test_events", "")), "exploratory benchmark comparisons"),
181
- ("Public alpha claim", "Not established", "blocked by market-data standard"),
182
  ]
183
  cells = "".join(
184
- f"""
185
- <div class='metric' style='border:1px solid #cbd5e1;border-radius:10px;padding:13px 14px;background:#ffffff;box-shadow:0 6px 18px rgba(15,23,42,.05);'>
186
- <div class='metric-value' style='font-size:1.38rem;font-weight:850;color:#0f766e;line-height:1.15;overflow-wrap:anywhere;'>{value}</div>
187
- <div class='metric-label' style='font-size:.92rem;color:#0f172a;font-weight:800;margin-top:5px;line-height:1.25;'>{label}</div>
188
- <div style='font-size:.82rem;color:#475569;font-weight:600;margin-top:4px;line-height:1.25;'>{note}</div>
189
- </div>
190
- """
191
- for label, value, note in items
192
  )
193
- return f"<div class='metrics' style='display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px;margin:14px 0 18px;'>{cells}</div>"
194
 
195
 
196
  def boundary_html() -> str:
197
  return """
198
- <div class='boundary' style='border-left:5px solid #8a4b00;background:#fff7e8;color:#3f2a00;padding:14px 16px;border-radius:8px;margin:12px 0 20px;font-size:1rem;line-height:1.55;'>
199
- <strong style='color:#2f1d00;font-weight:850;'>Important boundary:</strong>
200
- This is a public disclosure explorer, not an accusation tool. It does not claim intent, illegality, causality,
201
- insider trading, realized private returns, or proven above-market performance.
202
  </div>
203
  """
204
 
205
 
206
  def overview_intro_html() -> str:
207
  return """
208
- <div class="section-intro" style="background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:16px 18px;margin:8px 0 14px;">
209
- <div style="color:#0f172a;font-size:1.18rem;font-weight:850;margin-bottom:6px;">Start with the records, then the timing</div>
210
- <div style="color:#334155;font-size:1rem;line-height:1.55;">
211
- The first charts show filing behavior and what assets appear most often in disclosed transactions.
212
- The return chart is deliberately lower on the page because it is context, not a verdict.
 
 
213
  </div>
214
  </div>
215
  """
216
 
217
 
 
 
 
 
 
 
 
 
 
218
  def market_note_html() -> str:
219
  return """
220
  <div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;">
221
- <div style="color:#0f172a;font-size:1.08rem;font-weight:850;margin-bottom:5px;">How to read the market-context chart</div>
222
  <div style="color:#334155;font-size:1rem;line-height:1.55;">
223
  Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption.
224
  Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory.
@@ -235,20 +353,19 @@ def return_chart():
235
  name=str(date_basis).capitalize(),
236
  x=frame["window_label"],
237
  y=frame["equal_weight_alpha_pct"],
238
- customdata=frame[["claim_status_label", "n_events", "q_value"]],
239
  hovertemplate=(
240
  "%{x}<br>"
241
  "Benchmark difference: %{y:.3f}%<br>"
242
  "Rows: %{customdata[1]}<br>"
243
- "%{customdata[0]}<br>"
244
- "q-value: %{customdata[2]}<extra></extra>"
245
  ),
246
  )
247
  )
248
  fig.add_hline(y=0, line_width=1, line_color="#555")
249
  fig.update_layout(
250
- title="Exploratory market check: disclosed transactions vs benchmark",
251
- yaxis_title="Benchmark difference (%)",
252
  xaxis_title="Market window after date",
253
  barmode="group",
254
  margin=dict(l=64, r=20, t=70, b=62),
@@ -297,9 +414,9 @@ def top_ticker_chart():
297
  counts.columns = ["ticker", "rows"]
298
  fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
299
  fig.update_layout(
300
- title="Ticker candidates that appear most often",
301
- xaxis_title="Rows",
302
- yaxis_title="Ticker candidate",
303
  margin=dict(l=80, r=20, t=60, b=50),
304
  coloraxis_showscale=False,
305
  )
@@ -314,7 +431,18 @@ def wealth_senators() -> list[str]:
314
  def wealth_chart(senator_id: str):
315
  frame = WEALTH[WEALTH["senator_id"] == senator_id].sort_values("report_year_num")
316
  if frame.empty:
317
- return style_figure(go.Figure())
 
 
 
 
 
 
 
 
 
 
 
318
  fig = go.Figure()
319
  fig.add_trace(
320
  go.Scatter(
@@ -349,7 +477,7 @@ def wealth_chart(senator_id: str):
349
  )
350
  )
351
  fig.update_layout(
352
- title=f"Reported wealth range over time: {senator_id}",
353
  xaxis_title="Report year",
354
  yaxis_title="Estimated range from disclosure brackets ($)",
355
  margin=dict(l=70, r=20, t=60, b=50),
@@ -381,18 +509,84 @@ def wealth_table(senator_id: str) -> pd.DataFrame:
381
  )
382
 
383
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
385
  frame = TRADES.copy()
386
  if query:
387
  q = query.lower().strip()
 
388
  frame = frame[
389
  frame["asset_name"].fillna("").str.lower().str.contains(q, regex=False)
390
  | frame["ticker_candidate"].fillna("").str.lower().str.contains(q, regex=False)
391
  | frame["senator_id"].fillna("").str.lower().str.contains(q, regex=False)
 
392
  ]
393
  frame = frame[frame["filing_lag_days_num"].fillna(-1) >= min_lag]
 
 
394
  cols = [
395
- "senator_id",
396
  "transaction_date",
397
  "filing_date",
398
  "filing_lag_days",
@@ -404,16 +598,16 @@ def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
404
  "amount_max",
405
  "what_this_does_not_prove",
406
  ]
407
- return frame[cols].head(int(max_rows)).rename(
408
  columns={
409
- "senator_id": "Senator/Filer ID",
410
  "transaction_date": "Reported transaction date",
411
  "filing_date": "Public filing date",
412
  "filing_lag_days": "Filing lag days",
413
  "transaction_type": "Disclosure transaction type",
414
  "owner": "Reported owner",
415
  "asset_name": "Asset name from filing",
416
- "ticker_candidate": "Ticker candidate",
417
  "amount_min": "Amount low",
418
  "amount_max": "Amount high",
419
  "what_this_does_not_prove": "What this row does not prove",
@@ -421,6 +615,12 @@ def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
421
  )
422
 
423
 
 
 
 
 
 
 
424
  def return_handoff_display() -> pd.DataFrame:
425
  frame = RETURN_HANDOFF.copy()
426
  if "date_basis" in frame:
@@ -439,7 +639,6 @@ def return_handoff_display() -> pd.DataFrame:
439
  "midpoint_weight_alpha_pct",
440
  "bootstrap_ci_low_pct",
441
  "bootstrap_ci_high_pct",
442
- "q_value",
443
  "claim_status",
444
  "plain_english_status",
445
  ]
@@ -448,15 +647,14 @@ def return_handoff_display() -> pd.DataFrame:
448
  columns={
449
  "date_basis": "Measured from",
450
  "window": "Window",
451
- "events": "Transaction-event rows",
452
- "senators": "Senators/filer IDs",
453
- "tickers": "Ticker candidates",
454
  "equal_weight_alpha_pct": "Equal-weight benchmark difference (%)",
455
  "midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)",
456
  "bootstrap_ci_low_pct": "Bootstrap low (%)",
457
  "bootstrap_ci_high_pct": "Bootstrap high (%)",
458
- "q_value": "FDR-adjusted q-value",
459
- "claim_status": "Claim status",
460
  "plain_english_status": "Plain English status",
461
  }
462
  )
@@ -480,6 +678,23 @@ def file_inventory() -> pd.DataFrame:
480
  return pd.DataFrame(rows)
481
 
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  CSS = """
484
  :root {
485
  --accent: #1b6b63;
@@ -489,6 +704,7 @@ CSS = """
489
  --line: #cbd5e1;
490
  --soft: #f8fafc;
491
  --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
 
492
  }
493
  html,
494
  body,
@@ -499,6 +715,7 @@ body,
499
  .gradio-container .contain {
500
  background: #f6f8fb !important;
501
  color: var(--ink) !important;
 
502
  }
503
  .dark .gradio-container,
504
  .dark .gradio-container .main,
@@ -506,6 +723,7 @@ body,
506
  .dark .gradio-container .contain {
507
  background: #f6f8fb !important;
508
  color: var(--ink) !important;
 
509
  }
510
  .gradio-container,
511
  .gradio-container * {
@@ -516,13 +734,177 @@ body,
516
  letter-spacing: 0;
517
  }
518
  .gradio-container {
519
- max-width: 1180px !important;
520
  margin: auto;
521
  color: var(--ink);
522
  font-size: 16px;
523
  line-height: 1.48;
524
  padding: 0 18px 28px;
525
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  .gradio-container h1,
527
  .gradio-container .prose h1,
528
  .gradio-container .markdown h1 {
@@ -545,7 +927,7 @@ body,
545
  .gradio-container button[role="tab"] {
546
  color: #0f172a !important;
547
  font-size: 1rem !important;
548
- font-weight: 650 !important;
549
  }
550
  .gradio-container button[role="tab"][aria-selected="true"] {
551
  color: var(--accent) !important;
@@ -561,19 +943,37 @@ body,
561
  font-size: 1rem !important;
562
  background: #ffffff !important;
563
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
564
  .gradio-container .block,
565
  .gradio-container .form,
566
  .gradio-container .panel,
567
  .gradio-container .plot-container,
568
  .gradio-container .table-wrap,
569
- .gradio-container .dataframe {
 
 
570
  background: #ffffff !important;
571
  color: #111827 !important;
572
  }
573
  .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
574
  .metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
575
  .metric-value { font-size: 1.45rem; font-weight: 800; color: var(--accent); line-height: 1.18; overflow-wrap: anywhere; }
576
- .metric-label { font-size: .92rem; color: var(--muted); font-weight: 650; margin-top: 5px; }
577
  .boundary {
578
  border-left: 5px solid var(--warn);
579
  background: #fff7e8;
@@ -596,7 +996,7 @@ body,
596
  .gradio-container th {
597
  background: #eef4f8 !important;
598
  color: #0f172a !important;
599
- font-weight: 750 !important;
600
  }
601
  .gradio-container td {
602
  background: #ffffff !important;
@@ -606,16 +1006,16 @@ body,
606
  border-color: var(--line) !important;
607
  }
608
  @media (max-width: 900px) {
609
- .metrics, .plain-cards { grid-template-columns: repeat(2, minmax(0, 1fr)) !important; }
610
- .hero-panel h1 { font-size: 1.8rem !important; }
611
  }
612
  @media (max-width: 620px) {
613
- .metrics, .plain-cards { grid-template-columns: 1fr !important; }
614
  }
615
  """
616
 
617
 
618
- with gr.Blocks(title="Senator Financial Disclosure Explorer") as demo:
619
  gr.HTML(hero_html())
620
  gr.HTML(plain_english_cards_html())
621
  gr.HTML(metric_html())
@@ -624,57 +1024,65 @@ with gr.Blocks(title="Senator Financial Disclosure Explorer") as demo:
624
  with gr.Tabs():
625
  with gr.Tab("Start Here"):
626
  gr.HTML(overview_intro_html())
627
- with gr.Row():
628
- gr.Plot(value=filing_delay_chart(), label="Filing delay: how late were public reports?")
629
- gr.Plot(value=top_ticker_chart(), label="Ticker candidates: what appears in filings?")
630
  gr.HTML(
631
- "<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;color:#334155;font-size:1rem;line-height:1.55;'>"
632
- "<strong style='color:#0f172a;'>Where to go next:</strong> use <strong>Wealth Ranges</strong> to inspect bracket-based wealth estimates, "
633
- "<strong>Disclosed Transactions</strong> to search filings, <strong>Market Checks</strong> for cautious benchmark context, and <strong>Receipts</strong> for the audit trail."
634
  "</div>"
635
  )
636
 
637
- with gr.Tab("Wealth Ranges"):
638
- choices = wealth_senators()
639
- gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>Read this as a range, not a precise fortune.</strong> Senate forms use value brackets, so the ribbon shows low-to-high estimates from reported assets and liabilities.</div>")
640
- selector = gr.Dropdown(choices=choices, value=choices[0] if choices else None, label="Choose a senator/filer ID")
641
- wealth_plot = gr.Plot(value=wealth_chart(choices[0]) if choices else style_figure(go.Figure()), label="Reported wealth range")
642
- wealth_rows = gr.Dataframe(value=wealth_table(choices[0]) if choices else pd.DataFrame(), label="Range rows from disclosure brackets", interactive=False, wrap=True)
643
- selector.change(wealth_chart, inputs=selector, outputs=wealth_plot)
644
- selector.change(wealth_table, inputs=selector, outputs=wealth_rows)
645
-
646
- with gr.Tab("Disclosed Transactions"):
647
- gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>These are disclosed transaction rows.</strong> A row can be a purchase, sale, exchange, or another reported transaction type. The table does not know execution price, exact trade size, or private realized return.</div>")
648
- with gr.Row():
649
- search_box = gr.Textbox(label="Search asset, ticker, or filer ID", value="")
650
- min_lag = gr.Slider(0, 400, value=0, step=5, label="Minimum filing lag days")
651
- max_rows = gr.Slider(10, 250, value=50, step=10, label="Rows")
652
- trade_rows = gr.Dataframe(value=trade_search("", 0, 50), label="Disclosed transaction rows", interactive=False, wrap=True)
653
- search_box.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
654
- min_lag.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
655
- max_rows.change(trade_search, inputs=[search_box, min_lag, max_rows], outputs=trade_rows)
656
- gr.Plot(value=trade_type_chart(), label="Transaction type by year")
657
-
658
- with gr.Tab("Market Checks"):
659
- gr.HTML(market_note_html())
660
- gr.Plot(value=return_chart(), label="Exploratory market-context chart")
661
- gr.Dataframe(value=return_handoff_display(), label="Plain-English market-check rows", interactive=False, wrap=True)
662
- gr.HTML("<div class='boundary' style='border-left:5px solid #8a4b00;background:#fff7e8;color:#3f2a00;padding:14px 16px;border-radius:8px;margin:12px 0 20px;font-size:1rem;line-height:1.55;'><strong style='color:#2f1d00;'>Bottom line:</strong> these rows are blocked from public alpha claims. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>")
663
-
664
- with gr.Tab("Receipts"):
665
- gr.HTML("<div class='section-intro' style='background:#ffffff;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:8px 0 14px;color:#334155;font-size:1rem;line-height:1.55;'><strong style='color:#0f172a;'>Receipts are the audit trail.</strong> The files below show what was included, what was intentionally omitted, and where source or market-data gaps remain.</div>")
666
- gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
667
- gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
668
- gr.File(
669
- value=str(DATA / "public_release_manifest.json"),
670
- label="Public release manifest",
671
- interactive=False,
672
- )
673
- gr.File(
674
- value=str(DATA / "dataset_bundle" / "return_tests.csv"),
675
- label="Return-test summary CSV",
676
- interactive=False,
677
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
 
679
 
680
  if __name__ == "__main__":
 
82
 
83
  def claim_label(value: str) -> str:
84
  labels = {
85
+ "blocked_market_data_policy": "Not enough public market data for a market-beating claim",
86
+ "blocked": "Not enough public data for a market-beating claim",
87
  }
88
  return labels.get(str(value), str(value).replace("_", " "))
89
 
 
101
  TRADES = read_jsonl_gz("trades.jsonl.gz")
102
  WEALTH = read_jsonl_gz("net_worth_intervals.jsonl.gz")
103
  FILINGS = read_jsonl_gz("filings.jsonl.gz")
104
+ SENATORS = pd.read_csv(DATASET / "senators.csv")
105
 
106
  TRADES["transaction_year"] = pd.to_datetime(TRADES["transaction_date"], errors="coerce").dt.year
107
  TRADES["filing_lag_days_num"] = pd.to_numeric(TRADES.get("filing_lag_days"), errors="coerce")
 
112
  WEALTH[col] = pd.to_numeric(WEALTH.get(col), errors="coerce")
113
 
114
 
115
+ def title_name(value: str) -> str:
116
+ text = str(value or "").replace("-", " ").strip()
117
+ return " ".join(part.capitalize() for part in text.split())
118
+
119
+
120
+ def build_filer_index() -> pd.DataFrame:
121
+ filing_counts = FILINGS.groupby("senator_id").size().rename("filings")
122
+ trade_counts = TRADES.groupby("senator_id").size().rename("transactions")
123
+ wealth_counts = WEALTH.groupby("senator_id").size().rename("wealth_ranges")
124
+ median_lag = TRADES.groupby("senator_id")["filing_lag_days_num"].median().round().rename("median_filing_lag_days")
125
+ ids = sorted(set(FILINGS["senator_id"]) | set(TRADES["senator_id"]) | set(WEALTH["senator_id"]) | set(SENATORS["senator_id"]))
126
+ frame = pd.DataFrame({"senator_id": ids})
127
+ frame = frame.merge(filing_counts, on="senator_id", how="left")
128
+ frame = frame.merge(trade_counts, on="senator_id", how="left")
129
+ frame = frame.merge(wealth_counts, on="senator_id", how="left")
130
+ frame = frame.merge(median_lag, on="senator_id", how="left")
131
+ roster = SENATORS[["senator_id", "display_name", "state", "party"]].copy()
132
+ frame = frame.merge(roster, on="senator_id", how="left")
133
+
134
+ filing_names = (
135
+ FILINGS.assign(
136
+ filing_display=(
137
+ FILINGS.get("filer_first_name", "").fillna("").map(title_name)
138
+ + " "
139
+ + FILINGS.get("filer_last_name", "").fillna("").map(title_name)
140
+ ).str.strip()
141
+ )
142
+ .groupby("senator_id")["filing_display"]
143
+ .agg(lambda values: values.dropna().iloc[0] if len(values.dropna()) else "")
144
+ )
145
+ frame = frame.merge(filing_names.rename("filing_display"), on="senator_id", how="left")
146
+ frame["display_name"] = frame["display_name"].fillna(frame["filing_display"]).fillna("")
147
+ frame.loc[frame["display_name"].eq(""), "display_name"] = frame.loc[frame["display_name"].eq(""), "senator_id"]
148
+ for col in ["filings", "transactions", "wealth_ranges"]:
149
+ frame[col] = frame[col].fillna(0).astype(int)
150
+ frame["median_filing_lag_days"] = frame["median_filing_lag_days"].fillna("")
151
+ frame["state"] = frame["state"].fillna("")
152
+ frame["party"] = frame["party"].fillna("")
153
+ frame["search_text"] = (
154
+ frame["display_name"].astype(str)
155
+ + " "
156
+ + frame["senator_id"].astype(str)
157
+ + " "
158
+ + frame["state"].astype(str)
159
+ + " "
160
+ + frame["party"].astype(str)
161
+ ).str.lower()
162
+ return frame.sort_values(["transactions", "filings", "display_name"], ascending=[False, False, True])
163
+
164
+
165
+ FILER_INDEX = build_filer_index()
166
+
167
+
168
+ def name_for_id(senator_id: str) -> str:
169
+ match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
170
+ if match.empty:
171
+ return str(senator_id)
172
+ row = match.iloc[0]
173
+ suffix = ""
174
+ if row.get("state") or row.get("party"):
175
+ suffix = f" ({row.get('party', '')}-{row.get('state', '')})".replace(" (-", " (").replace("-)", ")")
176
+ return f"{row['display_name']}{suffix}"
177
+
178
+
179
+ PUBLIC_FILER_COLUMNS = [
180
+ "Name in records",
181
+ "State",
182
+ "Party",
183
+ "Filings",
184
+ "Transactions",
185
+ "Wealth ranges",
186
+ "Typical filing lag (days)",
187
+ ]
188
+
189
+ PUBLIC_WEALTH_COLUMNS = [
190
+ "Report year",
191
+ "Low estimate",
192
+ "Midpoint estimate",
193
+ "High estimate",
194
+ "Range width",
195
+ "Open-ended top bracket",
196
+ "How to read it",
197
+ ]
198
+
199
+ PUBLIC_TRADE_COLUMNS = [
200
+ "Filer",
201
+ "Reported transaction date",
202
+ "Public filing date",
203
+ "Filing lag days",
204
+ "Disclosure transaction type",
205
+ "Reported owner",
206
+ "Asset name from filing",
207
+ "Ticker text from filing",
208
+ "Amount low",
209
+ "Amount high",
210
+ "What this row does not prove",
211
+ ]
212
+
213
+
214
+ def resolve_filer(query: str) -> str:
215
+ q = str(query or "").strip().lower()
216
+ if q:
217
+ exact = FILER_INDEX[FILER_INDEX["senator_id"].str.lower().eq(q)]
218
+ if not exact.empty:
219
+ return str(exact.iloc[0]["senator_id"])
220
+ matches = FILER_INDEX[FILER_INDEX["search_text"].str.contains(q, regex=False)]
221
+ if not matches.empty:
222
+ return str(matches.iloc[0]["senator_id"])
223
+ wealth_ids = set(WEALTH["senator_id"])
224
+ matches = FILER_INDEX[FILER_INDEX["senator_id"].isin(wealth_ids)].sort_values(["wealth_ranges", "transactions"], ascending=[False, False])
225
+ return str(matches.iloc[0]["senator_id"]) if not matches.empty else str(FILER_INDEX.iloc[0]["senator_id"])
226
+
227
+
228
+ def empty_state_figure(message: str):
229
+ fig = go.Figure()
230
+ fig.add_annotation(
231
+ text=message,
232
+ x=0.5,
233
+ y=0.5,
234
+ xref="paper",
235
+ yref="paper",
236
+ showarrow=False,
237
+ font=dict(size=16, color="#334155", family=FONT_FAMILY),
238
+ )
239
+ fig.update_layout(height=360, margin=dict(l=24, r=24, t=34, b=24))
240
+ return style_figure(fig)
241
+
242
+
243
  def hero_html() -> str:
244
  return """
245
+ <section class="hero-panel">
246
+ <div class="eyebrow">Official Senate disclosure records</div>
247
+ <h1>Senate stock disclosures, made easier to read</h1>
248
+ <p class="hero-answer">
249
+ Search public Senate filings for reported trades, asset and wealth ranges, filing dates, and links back to source records.
250
+ </p>
251
+ <div class="not-verdict">It is not an accusation and it does not prove motive, illegality, insider trading, or market-beating performance.</div>
 
 
 
 
 
 
 
 
 
 
 
252
  </section>
253
  """
254
 
 
256
  def plain_english_cards_html() -> str:
257
  cards = [
258
  (
259
+ "Wealth is a range",
260
+ "Senate forms use dollar brackets, so this app shows low-to-high estimates, not exact net worth.",
261
  "#e8f7f3",
262
  "#0f766e",
263
  ),
264
  (
265
+ "Transactions are disclosures",
266
+ "Rows come from public forms. They do not include exact execution prices or private profit.",
267
  "#eef4ff",
268
  "#3155b7",
269
  ),
270
  (
271
+ "The math lives in Advanced",
272
+ "Benchmark charts are context only, so they stay out of the simple reading path.",
273
  "#fff7e8",
274
  "#8a4b00",
275
  ),
276
  ]
277
  cells = "".join(
278
  f"""
279
+ <div class="plain-card" style="background:{bg};border-color:{accent};">
280
+ <div class="plain-card-title" style="color:{accent};">{title}</div>
281
+ <div class="plain-card-body">{body}</div>
282
  </div>
283
  """
284
  for title, body, bg, accent in cards
285
  )
286
+ return f"<div class='plain-cards'>{cells}</div>"
287
 
288
 
289
  def metric_html() -> str:
290
  counts = MANIFEST.get("counts", {})
291
  dataset = counts.get("dataset", {})
 
292
  items = [
293
+ (fmt_int(dataset.get("filings", "")), "public filings"),
294
+ (fmt_int(dataset.get("trades", "")), "reported trades"),
295
+ (fmt_int(dataset.get("net_worth_intervals", "")), "wealth ranges"),
 
 
 
296
  ]
297
  cells = "".join(
298
+ f"<span class='count-pill'><strong>{value}</strong> {label}</span>"
299
+ for value, label in items
 
 
 
 
 
 
300
  )
301
+ return f"<div class='count-strip'><span class='count-label'>Behind the app:</span>{cells}<span class='count-note'>More technical tables live in Advanced.</span></div>"
302
 
303
 
304
  def boundary_html() -> str:
305
  return """
306
+ <div class='boundary'>
307
+ <strong>Plain-English boundary:</strong> Public disclosure records can show what was reported and when.
308
+ They cannot, by themselves, prove why someone traded or whether a private return was earned.
 
309
  </div>
310
  """
311
 
312
 
313
  def overview_intro_html() -> str:
314
  return """
315
+ <div class="simple-start">
316
+ <h2>Understand it in five seconds</h2>
317
+ <p>This is a search tool for public Senate financial disclosure records. It helps answer who filed, what was reported, when it became public, and where the original record trail lives.</p>
318
+ <div class="question-grid">
319
+ <div><strong>1. Search a senator</strong><span>See reported filings, trades, and wealth ranges.</span></div>
320
+ <div><strong>2. Search an asset</strong><span>Find transaction rows that mention a company, fund, or ticker text.</span></div>
321
+ <div><strong>3. Open Advanced</strong><span>Use this only for market context, source gaps, and downloads.</span></div>
322
  </div>
323
  </div>
324
  """
325
 
326
 
327
+ def public_source_summary_html() -> str:
328
+ return """
329
+ <div class="path-panel">
330
+ <strong>Best first step:</strong> search a senator by name, then open the original filing when you need the source.
331
+ <p>Public disclosure forms show what was reported and when. They usually do not show intent, exact dollar amounts, or the reason for a trade.</p>
332
+ </div>
333
+ """
334
+
335
+
336
  def market_note_html() -> str:
337
  return """
338
  <div class="section-intro" style="background:#f8fafc;border:1px solid #cbd5e1;border-radius:10px;padding:15px 16px;margin:12px 0;">
339
+ <div style="color:#0f172a;font-size:1.08rem;font-weight:800;margin-bottom:5px;">How to read this benchmark-context chart</div>
340
  <div style="color:#334155;font-size:1rem;line-height:1.55;">
341
  Bars above zero mean the disclosed transaction set moved more than a simple benchmark in that window under one assumption.
342
  Bars below zero mean it moved less. Because this public bundle uses no-cost adjusted-close data, every row remains exploratory.
 
353
  name=str(date_basis).capitalize(),
354
  x=frame["window_label"],
355
  y=frame["equal_weight_alpha_pct"],
356
+ customdata=frame[["claim_status_label", "n_events"]],
357
  hovertemplate=(
358
  "%{x}<br>"
359
  "Benchmark difference: %{y:.3f}%<br>"
360
  "Rows: %{customdata[1]}<br>"
361
+ "%{customdata[0]}<extra></extra>"
 
362
  ),
363
  )
364
  )
365
  fig.add_hline(y=0, line_width=1, line_color="#555")
366
  fig.update_layout(
367
+ title="Advanced benchmark context",
368
+ yaxis_title="Market move vs benchmark (%)",
369
  xaxis_title="Market window after date",
370
  barmode="group",
371
  margin=dict(l=64, r=20, t=70, b=62),
 
414
  counts.columns = ["ticker", "rows"]
415
  fig = px.bar(counts, x="rows", y="ticker", orientation="h", color="rows", color_continuous_scale="Viridis")
416
  fig.update_layout(
417
+ title="Stock-symbol text found most often in disclosures",
418
+ xaxis_title="Disclosure rows",
419
+ yaxis_title="Symbol text from filing",
420
  margin=dict(l=80, r=20, t=60, b=50),
421
  coloraxis_showscale=False,
422
  )
 
431
  def wealth_chart(senator_id: str):
432
  frame = WEALTH[WEALTH["senator_id"] == senator_id].sort_values("report_year_num")
433
  if frame.empty:
434
+ fig = go.Figure()
435
+ fig.add_annotation(
436
+ text="No wealth-range rows found for this filer.",
437
+ x=0.5,
438
+ y=0.5,
439
+ xref="paper",
440
+ yref="paper",
441
+ showarrow=False,
442
+ font=dict(size=16, color="#334155", family=FONT_FAMILY),
443
+ )
444
+ fig.update_layout(height=360, margin=dict(l=20, r=20, t=40, b=20))
445
+ return style_figure(fig)
446
  fig = go.Figure()
447
  fig.add_trace(
448
  go.Scatter(
 
477
  )
478
  )
479
  fig.update_layout(
480
+ title=f"Reported wealth range over time: {name_for_id(senator_id)}",
481
  xaxis_title="Report year",
482
  yaxis_title="Estimated range from disclosure brackets ($)",
483
  margin=dict(l=70, r=20, t=60, b=50),
 
509
  )
510
 
511
 
512
+ def filer_search(query: str, max_rows: int = 12) -> pd.DataFrame:
513
+ q = str(query or "").strip().lower()
514
+ if not q:
515
+ return pd.DataFrame(columns=PUBLIC_FILER_COLUMNS)
516
+ frame = FILER_INDEX.copy()
517
+ frame = frame[frame["search_text"].str.contains(q, regex=False)]
518
+ frame = frame.head(int(max_rows))
519
+ display = frame[["display_name", "state", "party", "filings", "transactions", "wealth_ranges", "median_filing_lag_days"]].copy()
520
+ return display.rename(
521
+ columns={
522
+ "display_name": "Name in records",
523
+ "state": "State",
524
+ "party": "Party",
525
+ "filings": "Filings",
526
+ "transactions": "Transactions",
527
+ "wealth_ranges": "Wealth ranges",
528
+ "median_filing_lag_days": "Typical filing lag (days)",
529
+ }
530
+ )
531
+
532
+
533
+ def filer_profile_html(query: str) -> str:
534
+ if not str(query or "").strip():
535
+ return """
536
+ <div class='profile-card empty-state'>
537
+ <h2>Search a senator to start</h2>
538
+ <p>Type a name, state, party, or filer ID. The result shows what appears in the public disclosure dataset, not a judgment about the person.</p>
539
+ </div>
540
+ """
541
+ senator_id = resolve_filer(query)
542
+ match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
543
+ if match.empty:
544
+ return "<div class='profile-card'><strong>No match found.</strong> Try a name, state, or filer ID.</div>"
545
+ row = match.iloc[0]
546
+ lag = row.get("median_filing_lag_days", "")
547
+ lag_text = f"{int(lag)} days" if str(lag).strip() else "not enough transaction rows"
548
+ return f"""
549
+ <div class='profile-card'>
550
+ <div class='profile-kicker'>Selected public filer</div>
551
+ <h2>{name_for_id(senator_id)}</h2>
552
+ <div class='profile-grid'>
553
+ <span><strong>{fmt_int(row.get('filings', 0))}</strong> filings</span>
554
+ <span><strong>{fmt_int(row.get('transactions', 0))}</strong> transaction rows</span>
555
+ <span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> wealth ranges</span>
556
+ <span><strong>{lag_text}</strong> typical filing lag</span>
557
+ </div>
558
+ </div>
559
+ """
560
+
561
+
562
+ def filer_wealth_chart(query: str):
563
+ if not str(query or "").strip():
564
+ return empty_state_figure("Search a senator to see reported wealth ranges.")
565
+ return wealth_chart(resolve_filer(query))
566
+
567
+
568
+ def filer_wealth_table(query: str) -> pd.DataFrame:
569
+ if not str(query or "").strip():
570
+ return pd.DataFrame(columns=PUBLIC_WEALTH_COLUMNS)
571
+ return wealth_table(resolve_filer(query))
572
+
573
+
574
  def trade_search(query: str, min_lag: int, max_rows: int) -> pd.DataFrame:
575
  frame = TRADES.copy()
576
  if query:
577
  q = query.lower().strip()
578
+ matching_ids = FILER_INDEX[FILER_INDEX["search_text"].str.contains(q, regex=False)]["senator_id"].tolist()
579
  frame = frame[
580
  frame["asset_name"].fillna("").str.lower().str.contains(q, regex=False)
581
  | frame["ticker_candidate"].fillna("").str.lower().str.contains(q, regex=False)
582
  | frame["senator_id"].fillna("").str.lower().str.contains(q, regex=False)
583
+ | frame["senator_id"].isin(matching_ids)
584
  ]
585
  frame = frame[frame["filing_lag_days_num"].fillna(-1) >= min_lag]
586
+ frame = frame.head(int(max_rows)).copy()
587
+ frame["filer_name"] = frame["senator_id"].fillna("").map(name_for_id)
588
  cols = [
589
+ "filer_name",
590
  "transaction_date",
591
  "filing_date",
592
  "filing_lag_days",
 
598
  "amount_max",
599
  "what_this_does_not_prove",
600
  ]
601
+ return frame[cols].rename(
602
  columns={
603
+ "filer_name": "Filer",
604
  "transaction_date": "Reported transaction date",
605
  "filing_date": "Public filing date",
606
  "filing_lag_days": "Filing lag days",
607
  "transaction_type": "Disclosure transaction type",
608
  "owner": "Reported owner",
609
  "asset_name": "Asset name from filing",
610
+ "ticker_candidate": "Ticker text from filing",
611
  "amount_min": "Amount low",
612
  "amount_max": "Amount high",
613
  "what_this_does_not_prove": "What this row does not prove",
 
615
  )
616
 
617
 
618
+ def public_trade_search(query: str) -> pd.DataFrame:
619
+ if not str(query or "").strip():
620
+ return pd.DataFrame(columns=PUBLIC_TRADE_COLUMNS)
621
+ return trade_search(query, 0, 50)
622
+
623
+
624
  def return_handoff_display() -> pd.DataFrame:
625
  frame = RETURN_HANDOFF.copy()
626
  if "date_basis" in frame:
 
639
  "midpoint_weight_alpha_pct",
640
  "bootstrap_ci_low_pct",
641
  "bootstrap_ci_high_pct",
 
642
  "claim_status",
643
  "plain_english_status",
644
  ]
 
647
  columns={
648
  "date_basis": "Measured from",
649
  "window": "Window",
650
+ "events": "Trade windows checked",
651
+ "senators": "Filers",
652
+ "tickers": "Stock symbols found",
653
  "equal_weight_alpha_pct": "Equal-weight benchmark difference (%)",
654
  "midpoint_weight_alpha_pct": "Midpoint-weight benchmark difference (%)",
655
  "bootstrap_ci_low_pct": "Bootstrap low (%)",
656
  "bootstrap_ci_high_pct": "Bootstrap high (%)",
657
+ "claim_status": "Status",
 
658
  "plain_english_status": "Plain English status",
659
  }
660
  )
 
678
  return pd.DataFrame(rows)
679
 
680
 
681
+ def audit_summary_html() -> str:
682
+ counts = MANIFEST.get("counts", {})
683
+ dataset = counts.get("dataset", {})
684
+ source_gaps = SOURCE_GAPS.get("source_gaps", [])
685
+ return f"""
686
+ <div class="audit-summary">
687
+ <h2>Source summary</h2>
688
+ <div class="audit-grid">
689
+ <div><strong>{fmt_int(dataset.get('source_artifacts', ''))}</strong><span>source artifacts tracked</span></div>
690
+ <div><strong>{fmt_int(len(source_gaps))}</strong><span>known source-gap notes</span></div>
691
+ <div><strong>No</strong><span>raw market-price file redistributed</span></div>
692
+ </div>
693
+ <p>Use the downloads below if you want to audit the package. Most visitors do not need this section.</p>
694
+ </div>
695
+ """
696
+
697
+
698
  CSS = """
699
  :root {
700
  --accent: #1b6b63;
 
704
  --line: #cbd5e1;
705
  --soft: #f8fafc;
706
  --font: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
707
+ color-scheme: light;
708
  }
709
  html,
710
  body,
 
715
  .gradio-container .contain {
716
  background: #f6f8fb !important;
717
  color: var(--ink) !important;
718
+ color-scheme: light !important;
719
  }
720
  .dark .gradio-container,
721
  .dark .gradio-container .main,
 
723
  .dark .gradio-container .contain {
724
  background: #f6f8fb !important;
725
  color: var(--ink) !important;
726
+ color-scheme: light !important;
727
  }
728
  .gradio-container,
729
  .gradio-container * {
 
734
  letter-spacing: 0;
735
  }
736
  .gradio-container {
737
+ max-width: 1080px !important;
738
  margin: auto;
739
  color: var(--ink);
740
  font-size: 16px;
741
  line-height: 1.48;
742
  padding: 0 18px 28px;
743
  }
744
+ .hero-panel {
745
+ background: #ffffff;
746
+ border: 1px solid #cbd5e1;
747
+ border-radius: 14px;
748
+ padding: 26px 28px;
749
+ margin: 14px 0 14px;
750
+ box-shadow: 0 14px 34px rgba(15,23,42,.08);
751
+ }
752
+ .hero-panel .eyebrow {
753
+ color: #0f766e;
754
+ font-size: .86rem;
755
+ font-weight: 800;
756
+ letter-spacing: .04em;
757
+ text-transform: uppercase;
758
+ margin-bottom: 8px;
759
+ }
760
+ .hero-panel h1 {
761
+ color: #0f172a !important;
762
+ font-size: 2.55rem !important;
763
+ line-height: 1.04 !important;
764
+ font-weight: 800 !important;
765
+ margin: 0 0 10px !important;
766
+ }
767
+ .hero-answer {
768
+ color: #1f2937 !important;
769
+ font-size: 1.12rem !important;
770
+ line-height: 1.55 !important;
771
+ max-width: 830px;
772
+ margin: 0 !important;
773
+ }
774
+ .not-verdict {
775
+ display: inline-block;
776
+ margin-top: 14px;
777
+ color: #6b3b00;
778
+ background: #fff7e8;
779
+ border: 1px solid #e6bd7a;
780
+ border-radius: 999px;
781
+ padding: 8px 12px;
782
+ font-weight: 800;
783
+ }
784
+ .plain-cards {
785
+ display: grid;
786
+ grid-template-columns: repeat(3, minmax(0, 1fr));
787
+ gap: 12px;
788
+ margin: 14px 0;
789
+ }
790
+ .plain-card {
791
+ border: 1px solid;
792
+ border-radius: 12px;
793
+ padding: 16px;
794
+ min-height: 120px;
795
+ }
796
+ .plain-card-title {
797
+ font-size: 1.08rem;
798
+ font-weight: 800;
799
+ margin-bottom: 8px;
800
+ }
801
+ .plain-card-body {
802
+ color: #1f2937;
803
+ font-size: 1rem;
804
+ line-height: 1.45;
805
+ font-weight: 600;
806
+ }
807
+ .count-strip {
808
+ display: flex;
809
+ flex-wrap: wrap;
810
+ gap: 8px;
811
+ align-items: center;
812
+ background: #ffffff;
813
+ border: 1px solid #d7dee8;
814
+ border-radius: 12px;
815
+ padding: 12px 14px;
816
+ margin: 12px 0;
817
+ }
818
+ .count-label {
819
+ color: #334155;
820
+ font-weight: 800;
821
+ }
822
+ .count-pill {
823
+ background: #eef7f5;
824
+ color: #0f3f3a;
825
+ border: 1px solid #b8dcd6;
826
+ border-radius: 999px;
827
+ padding: 6px 10px;
828
+ font-weight: 700;
829
+ }
830
+ .count-pill strong {
831
+ color: #0f766e !important;
832
+ font-weight: 800 !important;
833
+ }
834
+ .count-note {
835
+ color: #64748b;
836
+ font-weight: 700;
837
+ }
838
+ .simple-start,
839
+ .path-panel,
840
+ .section-intro,
841
+ .profile-card {
842
+ background: #ffffff;
843
+ border: 1px solid #cbd5e1;
844
+ border-radius: 12px;
845
+ padding: 16px 18px;
846
+ margin: 10px 0 14px;
847
+ color: #1f2937;
848
+ }
849
+ .simple-start h2,
850
+ .profile-card h2 {
851
+ color: #0f172a !important;
852
+ font-size: 1.45rem !important;
853
+ font-weight: 800 !important;
854
+ line-height: 1.2 !important;
855
+ margin: 0 0 8px !important;
856
+ }
857
+ .question-grid,
858
+ .profile-grid,
859
+ .audit-grid {
860
+ display: grid;
861
+ grid-template-columns: repeat(3, minmax(0, 1fr));
862
+ gap: 10px;
863
+ margin-top: 12px;
864
+ }
865
+ .question-grid div,
866
+ .profile-grid span,
867
+ .audit-grid div {
868
+ background: #f8fafc;
869
+ border: 1px solid #d7dee8;
870
+ border-radius: 10px;
871
+ padding: 12px;
872
+ color: #1f2937;
873
+ }
874
+ .question-grid strong,
875
+ .profile-grid strong,
876
+ .audit-grid strong {
877
+ display: block;
878
+ color: #0f172a !important;
879
+ font-weight: 800 !important;
880
+ margin-bottom: 4px;
881
+ }
882
+ .question-grid span,
883
+ .profile-grid span,
884
+ .audit-grid span {
885
+ color: #334155;
886
+ font-weight: 700;
887
+ }
888
+ .audit-summary h2 {
889
+ color: #0f172a !important;
890
+ font-size: 1.35rem !important;
891
+ font-weight: 800 !important;
892
+ margin: 0 0 8px !important;
893
+ }
894
+ .profile-kicker {
895
+ color: #0f766e;
896
+ font-size: .84rem;
897
+ font-weight: 800;
898
+ letter-spacing: .04em;
899
+ text-transform: uppercase;
900
+ margin-bottom: 6px;
901
+ }
902
+ .gradio-container a,
903
+ .gradio-container a *,
904
+ .gradio-container strong,
905
+ .gradio-container b {
906
+ color: #0f172a !important;
907
+ }
908
  .gradio-container h1,
909
  .gradio-container .prose h1,
910
  .gradio-container .markdown h1 {
 
927
  .gradio-container button[role="tab"] {
928
  color: #0f172a !important;
929
  font-size: 1rem !important;
930
+ font-weight: 700 !important;
931
  }
932
  .gradio-container button[role="tab"][aria-selected="true"] {
933
  color: var(--accent) !important;
 
943
  font-size: 1rem !important;
944
  background: #ffffff !important;
945
  }
946
+ .gradio-container code {
947
+ color: #0f172a !important;
948
+ background: #eef4f8 !important;
949
+ border: 1px solid #d7dee8 !important;
950
+ border-radius: 6px !important;
951
+ padding: 1px 5px !important;
952
+ }
953
+ [role="listbox"],
954
+ [role="option"],
955
+ .gradio-container .options,
956
+ .gradio-container .option,
957
+ .gradio-container .svelte-select-list,
958
+ .gradio-container .dropdown-options {
959
+ background: #ffffff !important;
960
+ color: #111827 !important;
961
+ }
962
  .gradio-container .block,
963
  .gradio-container .form,
964
  .gradio-container .panel,
965
  .gradio-container .plot-container,
966
  .gradio-container .table-wrap,
967
+ .gradio-container .dataframe,
968
+ .gradio-container .input-container,
969
+ .gradio-container .container {
970
  background: #ffffff !important;
971
  color: #111827 !important;
972
  }
973
  .metrics { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; margin: 14px 0 18px; }
974
  .metric { border: 1px solid var(--line); border-radius: 8px; padding: 13px 14px; background: #ffffff; }
975
  .metric-value { font-size: 1.45rem; font-weight: 800; color: var(--accent); line-height: 1.18; overflow-wrap: anywhere; }
976
+ .metric-label { font-size: .92rem; color: var(--muted); font-weight: 700; margin-top: 5px; }
977
  .boundary {
978
  border-left: 5px solid var(--warn);
979
  background: #fff7e8;
 
996
  .gradio-container th {
997
  background: #eef4f8 !important;
998
  color: #0f172a !important;
999
+ font-weight: 700 !important;
1000
  }
1001
  .gradio-container td {
1002
  background: #ffffff !important;
 
1006
  border-color: var(--line) !important;
1007
  }
1008
  @media (max-width: 900px) {
1009
+ .plain-cards, .question-grid, .profile-grid, .audit-grid { grid-template-columns: 1fr !important; }
1010
+ .hero-panel h1 { font-size: 2rem !important; }
1011
  }
1012
  @media (max-width: 620px) {
1013
+ .plain-cards { grid-template-columns: 1fr !important; }
1014
  }
1015
  """
1016
 
1017
 
1018
+ with gr.Blocks(title="Senate Disclosure Reader") as demo:
1019
  gr.HTML(hero_html())
1020
  gr.HTML(plain_english_cards_html())
1021
  gr.HTML(metric_html())
 
1024
  with gr.Tabs():
1025
  with gr.Tab("Start Here"):
1026
  gr.HTML(overview_intro_html())
 
 
 
1027
  gr.HTML(
1028
+ "<div class='path-panel'>"
1029
+ "<div><strong>Most people should use the first two tabs.</strong></div>"
1030
+ "<p><b>Search Senator</b> answers, 'What did this filer report over time?' <b>Search Trades</b> answers, 'What transaction rows mention this company, fund, or ticker text?' <b>Advanced</b> keeps assumptions and receipts out of the way.</p>"
1031
  "</div>"
1032
  )
1033
 
1034
+ with gr.Tab("Search Senator"):
1035
+ gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The chart shows a reported range, not exact wealth.</div>")
1036
+ filer_query = gr.Textbox(
1037
+ label="Name, state, or filer ID",
1038
+ value="",
1039
+ placeholder="Type a senator name, state, party, or filer ID",
1040
+ show_label=True,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1041
  )
1042
+ filer_results = gr.Dataframe(value=filer_search(""), label="Matching filers", interactive=False, wrap=True)
1043
+ filer_profile = gr.HTML(filer_profile_html(""))
1044
+ wealth_plot = gr.Plot(value=filer_wealth_chart(""), label="Reported wealth range", show_label=False)
1045
+ with gr.Accordion("Show the range rows", open=False):
1046
+ wealth_rows = gr.Dataframe(value=filer_wealth_table(""), label="Rows behind the chart", interactive=False, wrap=True)
1047
+ for event in (filer_query.input, filer_query.submit, filer_query.change):
1048
+ event(filer_search, inputs=filer_query, outputs=filer_results)
1049
+ event(filer_profile_html, inputs=filer_query, outputs=filer_profile)
1050
+ event(filer_wealth_chart, inputs=filer_query, outputs=wealth_plot)
1051
+ event(filer_wealth_table, inputs=filer_query, outputs=wealth_rows)
1052
+
1053
+ with gr.Tab("Search Trades"):
1054
+ gr.HTML("<div class='section-intro'><strong>Search transaction disclosures.</strong> These rows show what was reported on forms. They do not show exact execution price or private profit.</div>")
1055
+ search_box = gr.Textbox(label="Company, ticker, asset, or filer ID", value="", placeholder="Try: AAPL, Microsoft, SPY, Cruz")
1056
+ trade_rows = gr.Dataframe(value=public_trade_search(""), label="Matching transaction rows", interactive=False, wrap=True)
1057
+ for event in (search_box.input, search_box.submit, search_box.change):
1058
+ event(public_trade_search, inputs=search_box, outputs=trade_rows)
1059
+
1060
+ with gr.Tab("Advanced"):
1061
+ gr.HTML("<div class='section-intro'><strong>Advanced area.</strong> This is where the heavier charts, market assumptions, and receipts live.</div>")
1062
+ with gr.Accordion("Filing-delay chart", open=False):
1063
+ gr.Plot(value=filing_delay_chart(), show_label=False)
1064
+ with gr.Accordion("Stock-symbol text chart", open=False):
1065
+ gr.HTML("<div class='small-note'>Ticker text is parsed from disclosure forms. It is not always a verified security identifier.</div>")
1066
+ gr.Plot(value=top_ticker_chart(), show_label=False)
1067
+ with gr.Accordion("Benchmark context", open=False):
1068
+ gr.HTML(market_note_html())
1069
+ gr.Plot(value=return_chart(), show_label=False)
1070
+ gr.Dataframe(value=return_handoff_display(), label="Market-check rows", interactive=False, wrap=True)
1071
+ gr.HTML("<div class='boundary'><strong>Bottom line:</strong> these rows are not public alpha proof. A positive cell is not proof of intent, illegality, causality, insider trading, or realized private return.</div>")
1072
+ with gr.Accordion("Audit trail and downloads", open=False):
1073
+ gr.HTML(audit_summary_html())
1074
+ gr.Dataframe(value=source_gap_table(), label="Source-gap report", interactive=False, wrap=True)
1075
+ gr.Dataframe(value=file_inventory(), label="Bundle inventory", interactive=False, wrap=True)
1076
+ gr.File(
1077
+ value=str(DATA / "public_release_manifest.json"),
1078
+ label="Public release manifest",
1079
+ interactive=False,
1080
+ )
1081
+ gr.File(
1082
+ value=str(DATA / "dataset_bundle" / "return_tests.csv"),
1083
+ label="Return-test summary CSV",
1084
+ interactive=False,
1085
+ )
1086
 
1087
 
1088
  if __name__ == "__main__":
space_bundle_manifest.json CHANGED
@@ -8,8 +8,8 @@
8
  "files": [
9
  {
10
  "path": "app.py",
11
- "sha256": "8c303e8e6ee0e11d29512e99f663d5454048db85bbe7a016182df47db4756f24",
12
- "byte_count": 30041
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
@@ -98,8 +98,8 @@
98
  },
99
  {
100
  "path": "README.md",
101
- "sha256": "e52e46bb2b6cc3dd1c334a0ce3465e5aa0d8c97329a4e0107afccad082f32156",
102
- "byte_count": 544
103
  },
104
  {
105
  "path": "requirements.txt",
@@ -107,5 +107,5 @@
107
  "byte_count": 47
108
  }
109
  ],
110
- "manifest_hash": "a0663b17e37274c65850f2c9e89b588acb6dac6dc3d88e7f74c2407c4408893c"
111
  }
 
8
  "files": [
9
  {
10
  "path": "app.py",
11
+ "sha256": "2cdc157335044caa3d49298cbd2618f787d304f08134774d9fa6abdad06c93b1",
12
+ "byte_count": 39823
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
 
98
  },
99
  {
100
  "path": "README.md",
101
+ "sha256": "5350c409b1ddf35423439b7aeb54f413df524f97c6698e4e90d28d2880a08ee9",
102
+ "byte_count": 488
103
  },
104
  {
105
  "path": "requirements.txt",
 
107
  "byte_count": 47
108
  }
109
  ],
110
+ "manifest_hash": "0240d33cb57c5cf2224a610558dcfad56e3a96ff3bf4c0b0a6ee87c42205427f"
111
  }