cjc0013 commited on
Commit
57aa27c
·
verified ·
1 Parent(s): 49fdd60

Simplify public disclosure reader

Browse files
Files changed (2) hide show
  1. app.py +135 -78
  2. space_bundle_manifest.json +3 -3
app.py CHANGED
@@ -240,9 +240,9 @@ def public_filer_cards_html(
240
  lag = str(row.get("Typical filing lag (days)", "")).strip()
241
  lag_text = f"{lag} day typical filing lag" if lag else "filing lag not available"
242
  counts = [
243
- f"{fmt_int(row.get('Filings', 0))} filings",
244
- f"{fmt_int(row.get('Transactions', 0))} transaction rows",
245
- f"{fmt_int(row.get('Forms with wealth info', 0))} forms with wealth info",
246
  lag_text,
247
  ]
248
  count_html = "".join(f"<span>{html_escape(item)}</span>" for item in counts)
@@ -252,7 +252,7 @@ def public_filer_cards_html(
252
  why_html = f"<p>{html_escape(why)}</p>" if why else ""
253
  cards.append(
254
  "<article class='filer-match-card'>"
255
- f"<div><h3>{html_escape(row.get('Name in records', ''))}</h3>{meta}</div>"
256
  f"<strong class='filer-status'>{html_escape(status_text)}</strong>"
257
  f"<div class='filer-counts'>{count_html}</div>"
258
  f"{why_html}"
@@ -263,8 +263,8 @@ def public_filer_cards_html(
263
  more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching public records.</p>"
264
  return (
265
  "<div class='result-card filer-results-card'>"
266
- "<h2>Matching public filer records</h2>"
267
- "<p>Pick the record with disclosure rows when a roster name and filing name split apart.</p>"
268
  f"<div class='filer-card-list'>{''.join(cards)}</div>"
269
  f"{more}"
270
  "</div>"
@@ -386,12 +386,12 @@ def name_for_id(senator_id: str) -> str:
386
 
387
 
388
  PUBLIC_FILER_COLUMNS = [
389
- "Name in records",
390
  "State",
391
  "Party",
392
- "Filings",
393
- "Transactions",
394
- "Forms with wealth info",
395
  "Typical filing lag (days)",
396
  "Record status",
397
  "Why shown",
@@ -500,15 +500,20 @@ def empty_state_figure(message: str):
500
  def hero_html() -> str:
501
  return """
502
  <section class="hero-panel">
503
- <div class="eyebrow">Official Senate disclosure records</div>
504
- <h1>Search a senator's public filings</h1>
505
  <p class="hero-answer">
506
- Type a name. See what Senate financial disclosure forms say: reported trades, a rough wealth range when available, and links to the original filings.
507
  </p>
508
  <p class="hero-why">
509
- Why someone might use this: these forms are one of the few public windows into financial interests lawmakers reported, when they reported them, and where the original receipt lives.
510
  </p>
511
- <div class="not-verdict">Public records only. Not exact net worth. Not an accusation.</div>
 
 
 
 
 
512
  </section>
513
  """
514
 
@@ -516,20 +521,20 @@ def hero_html() -> str:
516
  def plain_english_cards_html() -> str:
517
  cards = [
518
  (
519
- "Wealth is a range",
520
- "Senate forms use dollar brackets, so this app shows the rough range the form supports, not exact net worth.",
521
  "#e8f7f3",
522
  "#0f766e",
523
  ),
524
  (
525
- "Transactions are disclosures",
526
- "Rows come from public forms. They do not include exact execution prices or private profit.",
527
  "#eef4ff",
528
  "#3155b7",
529
  ),
530
  (
531
- "The math lives in Advanced",
532
- "Benchmark charts are context only, so they stay out of the simple reading path.",
533
  "#fff7e8",
534
  "#8a4b00",
535
  ),
@@ -574,11 +579,11 @@ def overview_intro_html() -> str:
574
  return """
575
  <div class="simple-start">
576
  <h2>Understand it in five seconds</h2>
577
- <p>Use this like a reader for public Senate forms.</p>
578
  <div class="question-grid">
579
- <div><strong>1. Type a name</strong><span>Start with a senator or disclosure filer.</span></div>
580
- <div><strong>2. Read the range</strong><span>The wealth number is rough because the form uses brackets.</span></div>
581
- <div><strong>3. Open the filing</strong><span>Use the source link when you need the receipt.</span></div>
582
  </div>
583
  </div>
584
  """
@@ -835,12 +840,12 @@ def filer_search(query: str, max_rows: int = 12) -> pd.DataFrame:
835
  display["record_status"] = frame.apply(data_status, axis=1).values
836
  return display.rename(
837
  columns={
838
- "display_name": "Name in records",
839
  "state": "State",
840
  "party": "Party",
841
- "filings": "Filings",
842
- "transactions": "Transactions",
843
- "wealth_ranges": "Forms with wealth info",
844
  "median_filing_lag_days": "Typical filing lag (days)",
845
  "record_status": "Record status",
846
  "why_shown": "Why shown",
@@ -869,7 +874,7 @@ def filer_search_html(query: str) -> str:
869
  )
870
  return note + public_filer_cards_html(
871
  results,
872
- "No matching public filer records found",
873
  "Try a last name, state abbreviation, party, filer ID, or common/legal-name variant.",
874
  max_rows=12,
875
  )
@@ -910,9 +915,9 @@ def filer_profile_html(query: str) -> str:
910
  <h2>{name_for_id(senator_id)}</h2>
911
  {status_note}
912
  <div class='profile-grid'>
913
- <span><strong>{fmt_int(row.get('filings', 0))}</strong> filings</span>
914
- <span><strong>{fmt_int(row.get('transactions', 0))}</strong> transaction rows</span>
915
- <span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> forms with wealth info</span>
916
  <span><strong>{lag_text}</strong> typical filing lag</span>
917
  </div>
918
  </div>
@@ -957,7 +962,7 @@ def floor_money_text(value: float) -> str:
957
 
958
 
959
  def disclosure_math_plain_text() -> str:
960
- return "reported assets minus reported debts, using the dollar ranges on the public form"
961
 
962
 
963
  def wealth_plain_result(row) -> dict[str, str]:
@@ -966,20 +971,20 @@ def wealth_plain_result(row) -> dict[str, str]:
966
  year = present_text(row.get("report_year", "latest"), "latest")
967
  if low_num is not None and high_num is not None:
968
  short = f"{fmt_money(low_num)} to {fmt_money(high_num)}"
969
- sentence = f"The {year} public form points to a rough {disclosure_math_plain_text()} range of {short}."
970
  missing = "Exact net worth is not reported."
971
- note = "The form gives both a low and high number, but still not an exact private valuation."
972
  elif low_num is not None:
973
  short = floor_money_text(low_num)
974
  sentence_value = short[:1].lower() + short[1:]
975
- sentence = f"The {year} public form shows {sentence_value} after {disclosure_math_plain_text()}."
976
- missing = "No maximum can be calculated."
977
- note = "The exact amount is not public here, and the actual number may be higher."
978
  elif high_num is not None:
979
  short = f"No more than {fmt_money(high_num)}"
980
- sentence = f"The {year} public form shows no more than {fmt_money(high_num)} after {disclosure_math_plain_text()}."
981
- missing = "No minimum can be calculated."
982
- note = "The exact amount is not public here, and the actual number may be lower."
983
  else:
984
  short = "Not enough dollar detail"
985
  sentence = f"The {year} public form does not give enough dollar detail for a wealth summary."
@@ -1005,26 +1010,25 @@ def wealth_year_explanation(row) -> str:
1005
  year = present_text(row.get("report_year", "this year"), "this year")
1006
  low_num = numeric_or_none(row.get("net_worth_min"))
1007
  high_num = numeric_or_none(row.get("net_worth_max"))
1008
- math_text = disclosure_math_plain_text()
1009
  if low_num is not None and high_num is not None:
1010
  return (
1011
- f"For {year}, this means the form supports a rough range from {fmt_money(low_num)} to {fmt_money(high_num)} "
1012
- f"after {math_text}. It is still not exact net worth."
1013
  )
1014
  if low_num is not None:
1015
  sentence = (
1016
- f"For {year}, this is the lowest number the form supports after {math_text}. "
1017
- "No maximum can be calculated from the public form, so the actual number may be higher."
1018
  )
1019
  if low_num < 0:
1020
- sentence += " A negative low end can happen when debts may exceed assets inside the broad reporting ranges."
1021
  return sentence
1022
  if high_num is not None:
1023
  return (
1024
- f"For {year}, this is the highest number the form supports after {math_text}. "
1025
- "No minimum can be calculated from the public form, so the actual number may be lower."
1026
  )
1027
- return f"For {year}, this row does not have enough dollar detail to summarize assets minus debts."
1028
 
1029
 
1030
  def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
@@ -1037,7 +1041,7 @@ def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
1037
  previous_low = numeric_or_none(previous.get("net_worth_min"))
1038
  previous_high = numeric_or_none(previous.get("net_worth_max"))
1039
  if None in {latest_low, latest_high, previous_low, previous_high}:
1040
- return "The earlier form is not precise enough for a clean year-to-year comparison."
1041
  latest_year = present_text(latest.get("report_year", "latest"))
1042
  previous_year = present_text(previous.get("report_year", "previous"))
1043
  if latest_low > previous_high:
@@ -1045,17 +1049,17 @@ def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
1045
  elif latest_high < previous_low:
1046
  direction = "is below"
1047
  else:
1048
- return f"Compared with {previous_year}, the {latest_year} rough range overlaps the earlier one, so the forms do not show a clear move."
1049
- return f"Compared with {previous_year}, the {latest_year} rough range {direction} the earlier rough range."
1050
 
1051
 
1052
  def wealth_missing_number_text(row) -> str:
1053
  low_num = numeric_or_none(row.get("net_worth_min"))
1054
  high_num = numeric_or_none(row.get("net_worth_max"))
1055
  if low_num is not None and high_num is None:
1056
- return "No maximum can be calculated."
1057
  if low_num is None and high_num is not None:
1058
- return "No minimum can be calculated."
1059
  if low_num is None and high_num is None:
1060
  return "Low and high numbers not available."
1061
  return "Low and high numbers available."
@@ -1092,11 +1096,14 @@ def filer_wealth_summary_html(query: str) -> str:
1092
  else "<span class='source-missing'>Source link not available in this row</span>"
1093
  )
1094
  latest_plain = wealth_plain_result(latest)
 
 
 
 
1095
  summary_cards = [
1096
- ("What the number is", "Assets minus debts from the public form"),
1097
- ("What it says", latest_plain["short"]),
1098
- ("What it cannot say", latest_plain["missing"]),
1099
- ("Receipt", f"Filed {latest_filing_date}"),
1100
  ]
1101
  card_html = "".join(
1102
  f"<div><strong>{html_escape(label)}</strong><span>{html_escape(value)}</span></div>"
@@ -1126,27 +1133,25 @@ def filer_wealth_summary_html(query: str) -> str:
1126
  more_note = f"<p class='table-note'>Showing 6 of {fmt_int(len(ordered))} past filing summaries. Download files and technical rows live in Advanced.</p>"
1127
  return (
1128
  "<div class='result-card wealth-result wealth-reader'>"
1129
- f"<div class='profile-kicker'>Public form summary</div>"
1130
- f"<h2>{html_escape(name_for_id(senator_id))}: what the public form says</h2>"
1131
- f"<div class='wealth-answer-card'><span>Latest public form</span><strong>{html_escape(latest_plain['short'])}</strong><p>{html_escape(latest_plain['sentence'])} {html_escape(latest_plain['note'])}</p></div>"
 
1132
  f"<div class='wealth-plain-grid'>{card_html}</div>"
1133
- "<div class='why-matter-card'>"
1134
- "<strong>Why this might matter</strong>"
1135
- "<p>These records let readers see broad patterns in reported assets, debts, transactions, filing dates, and source receipts over time. They help people ask better questions; they do not show exact wealth, private intent, wrongdoing, or whether any trade was profitable.</p>"
1136
- "</div>"
1137
- "<div class='wealth-explain'>"
1138
- "<strong>Why not exact?</strong> Senate forms use broad dollar ranges, not exact account balances. This is not a bank balance, taxable income, trading profit, or proof of gains."
1139
- f"<span>{html_escape(latest_plain['missing'])}</span>"
1140
- "</div>"
1141
  f"<p class='wealth-movement'>{html_escape(wealth_movement_sentence(ordered))}</p>"
1142
  f"<div class='receipt-strip'><span>Latest filing date: <strong>{html_escape(latest_filing_date)}</strong></span>{source_link}</div>"
1143
  "</div>"
1144
- "<div class='result-card wealth-years-card'>"
1145
- "<h2>Year-by-year public disclosure ranges</h2>"
1146
- "<div class='wealth-year-guide'><strong>How to read each year:</strong> the report year is the year named in the filing. The dollar phrase is the assets-minus-debts range the public form supports for that year. Floors are not rankings, and year-to-year changes may reflect the form's dollar ranges, purchases, sales, debts, filing differences, or what this public bundle could read.</div>"
1147
  f"<ul class='wealth-year-list'>{''.join(year_items)}</ul>"
1148
  f"{more_note}"
1149
- "</div>"
1150
  )
1151
 
1152
 
@@ -1421,6 +1426,20 @@ body,
1421
  margin: 10px 0 0 !important;
1422
  font-weight: 700 !important;
1423
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1424
  .not-verdict {
1425
  display: inline-block;
1426
  margin-top: 14px;
@@ -1707,7 +1726,7 @@ body,
1707
  .wealth-answer-card strong {
1708
  display: block;
1709
  color: #061a18 !important;
1710
- font-size: clamp(1.6rem, 2.8vw, 2.25rem) !important;
1711
  line-height: 1.08 !important;
1712
  font-weight: 800 !important;
1713
  margin-bottom: 8px;
@@ -1718,9 +1737,13 @@ body,
1718
  line-height: 1.55 !important;
1719
  margin: 0 !important;
1720
  }
 
 
 
 
1721
  .wealth-plain-grid {
1722
  display: grid;
1723
- grid-template-columns: repeat(4, minmax(0, 1fr));
1724
  gap: 10px;
1725
  margin-top: 12px;
1726
  }
@@ -1763,9 +1786,20 @@ body,
1763
  color: #2b1700 !important;
1764
  margin: 0 !important;
1765
  }
 
 
 
 
 
 
1766
  .wealth-reader h2 {
1767
  max-width: 860px;
1768
  }
 
 
 
 
 
1769
  .wealth-lede {
1770
  font-size: 1.12rem !important;
1771
  line-height: 1.5 !important;
@@ -1862,6 +1896,29 @@ body,
1862
  color: #334155 !important;
1863
  font-weight: 700 !important;
1864
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1865
  .wealth-movement {
1866
  margin-top: 12px !important;
1867
  font-weight: 700 !important;
@@ -2142,7 +2199,7 @@ with gr.Blocks(title="Senate Disclosure Reader") as demo:
2142
 
2143
  with gr.Tabs():
2144
  with gr.Tab("Search Senator"):
2145
- gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The first result says what the latest public form can and cannot show. Exact net worth is not public from these forms.</div>")
2146
  filer_query = gr.Textbox(
2147
  label="Name, state, or filer ID",
2148
  value="",
 
240
  lag = str(row.get("Typical filing lag (days)", "")).strip()
241
  lag_text = f"{lag} day typical filing lag" if lag else "filing lag not available"
242
  counts = [
243
+ f"{fmt_int(row.get('Public forms', 0))} public forms",
244
+ f"{fmt_int(row.get('Trade rows', 0))} trade rows",
245
+ f"{fmt_int(row.get('Wealth summaries', 0))} wealth summaries",
246
  lag_text,
247
  ]
248
  count_html = "".join(f"<span>{html_escape(item)}</span>" for item in counts)
 
252
  why_html = f"<p>{html_escape(why)}</p>" if why else ""
253
  cards.append(
254
  "<article class='filer-match-card'>"
255
+ f"<div><h3>{html_escape(row.get('Matching record', ''))}</h3>{meta}</div>"
256
  f"<strong class='filer-status'>{html_escape(status_text)}</strong>"
257
  f"<div class='filer-counts'>{count_html}</div>"
258
  f"{why_html}"
 
263
  more = f"<p class='table-note'>Showing {fmt_int(max_rows)} of {fmt_int(len(frame))} matching public records.</p>"
264
  return (
265
  "<div class='result-card filer-results-card'>"
266
+ "<h2>Matching records</h2>"
267
+ "<p>If more than one record appears, use the one that says disclosure rows were found.</p>"
268
  f"<div class='filer-card-list'>{''.join(cards)}</div>"
269
  f"{more}"
270
  "</div>"
 
386
 
387
 
388
  PUBLIC_FILER_COLUMNS = [
389
+ "Matching record",
390
  "State",
391
  "Party",
392
+ "Public forms",
393
+ "Trade rows",
394
+ "Wealth summaries",
395
  "Typical filing lag (days)",
396
  "Record status",
397
  "Why shown",
 
500
  def hero_html() -> str:
501
  return """
502
  <section class="hero-panel">
503
+ <div class="eyebrow">Official Senate disclosure forms</div>
504
+ <h1>See what senators reported</h1>
505
  <p class="hero-answer">
506
+ Search a name. The app turns public Senate financial disclosure forms into plain answers: reported wealth range, disclosed transaction rows, filing dates, and the source filing.
507
  </p>
508
  <p class="hero-why">
509
+ People might care because lawmakers file public financial disclosures. This makes those official records easier to read without turning them into accusations.
510
  </p>
511
+ <div class="reader-model">
512
+ <span>Forms, not accusations</span>
513
+ <span>Ranges, not exact net worth</span>
514
+ <span>Receipts, not guesses</span>
515
+ </div>
516
+ <div class="not-verdict">No claim of wrongdoing or market-beating performance.</div>
517
  </section>
518
  """
519
 
 
521
  def plain_english_cards_html() -> str:
522
  cards = [
523
  (
524
+ "Start with a person",
525
+ "Search a senator and read the latest public form summary before opening charts or tables.",
526
  "#e8f7f3",
527
  "#0f766e",
528
  ),
529
  (
530
+ "Read money as a range",
531
+ "Forms report dollar ranges. A result like 'at least $363K' is a disclosure floor, not a precise fortune.",
532
  "#eef4ff",
533
  "#3155b7",
534
  ),
535
  (
536
+ "Open the receipt",
537
+ "Every useful row points back to the public filing so readers can check the source record.",
538
  "#fff7e8",
539
  "#8a4b00",
540
  ),
 
579
  return """
580
  <div class="simple-start">
581
  <h2>Understand it in five seconds</h2>
582
+ <p>Use this like a receipt-backed reader for public Senate financial forms.</p>
583
  <div class="question-grid">
584
+ <div><strong>1. Who?</strong><span>Search the senator or filer name.</span></div>
585
+ <div><strong>2. What was reported?</strong><span>Read the latest wealth range and disclosed transaction rows.</span></div>
586
+ <div><strong>3. Where is the proof?</strong><span>Open the Senate filing linked beside the result.</span></div>
587
  </div>
588
  </div>
589
  """
 
840
  display["record_status"] = frame.apply(data_status, axis=1).values
841
  return display.rename(
842
  columns={
843
+ "display_name": "Matching record",
844
  "state": "State",
845
  "party": "Party",
846
+ "filings": "Public forms",
847
+ "transactions": "Trade rows",
848
+ "wealth_ranges": "Wealth summaries",
849
  "median_filing_lag_days": "Typical filing lag (days)",
850
  "record_status": "Record status",
851
  "why_shown": "Why shown",
 
874
  )
875
  return note + public_filer_cards_html(
876
  results,
877
+ "No matching public records found",
878
  "Try a last name, state abbreviation, party, filer ID, or common/legal-name variant.",
879
  max_rows=12,
880
  )
 
915
  <h2>{name_for_id(senator_id)}</h2>
916
  {status_note}
917
  <div class='profile-grid'>
918
+ <span><strong>{fmt_int(row.get('filings', 0))}</strong> public forms</span>
919
+ <span><strong>{fmt_int(row.get('transactions', 0))}</strong> trade rows</span>
920
+ <span><strong>{fmt_int(row.get('wealth_ranges', 0))}</strong> wealth summaries</span>
921
  <span><strong>{lag_text}</strong> typical filing lag</span>
922
  </div>
923
  </div>
 
962
 
963
 
964
  def disclosure_math_plain_text() -> str:
965
+ return "subtracting debts from assets reported on the public form"
966
 
967
 
968
  def wealth_plain_result(row) -> dict[str, str]:
 
971
  year = present_text(row.get("report_year", "latest"), "latest")
972
  if low_num is not None and high_num is not None:
973
  short = f"{fmt_money(low_num)} to {fmt_money(high_num)}"
974
+ sentence = f"For {year}, the public form puts {disclosure_math_plain_text()} between {short}."
975
  missing = "Exact net worth is not reported."
976
+ note = "That is a reported range, not an exact private valuation."
977
  elif low_num is not None:
978
  short = floor_money_text(low_num)
979
  sentence_value = short[:1].lower() + short[1:]
980
+ sentence = f"For {year}, the public form shows {sentence_value} after {disclosure_math_plain_text()}."
981
+ missing = "No calculable maximum from this filing."
982
+ note = "The exact amount is not public here, and the real number may be higher."
983
  elif high_num is not None:
984
  short = f"No more than {fmt_money(high_num)}"
985
+ sentence = f"For {year}, the public form shows no more than {fmt_money(high_num)} after {disclosure_math_plain_text()}."
986
+ missing = "No calculable minimum from this filing."
987
+ note = "The exact amount is not public here, and the real number may be lower."
988
  else:
989
  short = "Not enough dollar detail"
990
  sentence = f"The {year} public form does not give enough dollar detail for a wealth summary."
 
1010
  year = present_text(row.get("report_year", "this year"), "this year")
1011
  low_num = numeric_or_none(row.get("net_worth_min"))
1012
  high_num = numeric_or_none(row.get("net_worth_max"))
 
1013
  if low_num is not None and high_num is not None:
1014
  return (
1015
+ f"For {year}, the reported assets-minus-debts number lands somewhere from {fmt_money(low_num)} to {fmt_money(high_num)}. "
1016
+ "It is a public disclosure range, not an appraisal or exact net worth."
1017
  )
1018
  if low_num is not None:
1019
  sentence = (
1020
+ f"For {year}, the public form supports at least {fmt_money(low_num)}. "
1021
+ "This bundle cannot calculate a top end from that filing, so the real number could be higher."
1022
  )
1023
  if low_num < 0:
1024
+ sentence += " A negative floor can happen when reported debt ranges can exceed reported asset ranges."
1025
  return sentence
1026
  if high_num is not None:
1027
  return (
1028
+ f"For {year}, the public form supports no more than {fmt_money(high_num)}. "
1029
+ "This bundle cannot calculate a bottom end from that filing, so the real number could be lower."
1030
  )
1031
+ return f"For {year}, the filing rows do not have enough dollar detail here to summarize assets minus debts."
1032
 
1033
 
1034
  def wealth_movement_sentence(ordered: pd.DataFrame) -> str:
 
1041
  previous_low = numeric_or_none(previous.get("net_worth_min"))
1042
  previous_high = numeric_or_none(previous.get("net_worth_max"))
1043
  if None in {latest_low, latest_high, previous_low, previous_high}:
1044
+ return "The forms are too broad for a clean year-to-year comparison."
1045
  latest_year = present_text(latest.get("report_year", "latest"))
1046
  previous_year = present_text(previous.get("report_year", "previous"))
1047
  if latest_low > previous_high:
 
1049
  elif latest_high < previous_low:
1050
  direction = "is below"
1051
  else:
1052
+ return f"Compared with {previous_year}, the {latest_year} reported range overlaps the earlier one, so the forms do not show a clear move."
1053
+ return f"Compared with {previous_year}, the {latest_year} reported range {direction} the earlier reported range."
1054
 
1055
 
1056
  def wealth_missing_number_text(row) -> str:
1057
  low_num = numeric_or_none(row.get("net_worth_min"))
1058
  high_num = numeric_or_none(row.get("net_worth_max"))
1059
  if low_num is not None and high_num is None:
1060
+ return "No calculable maximum from this filing."
1061
  if low_num is None and high_num is not None:
1062
+ return "No calculable minimum from this filing."
1063
  if low_num is None and high_num is None:
1064
  return "Low and high numbers not available."
1065
  return "Low and high numbers available."
 
1096
  else "<span class='source-missing'>Source link not available in this row</span>"
1097
  )
1098
  latest_plain = wealth_plain_result(latest)
1099
+ index_match = FILER_INDEX[FILER_INDEX["senator_id"] == senator_id]
1100
+ index_row = index_match.iloc[0] if not index_match.empty else {}
1101
+ filing_count = int(index_row.get("filings", 0) or 0) if hasattr(index_row, "get") else 0
1102
+ transaction_count = int(index_row.get("transactions", 0) or 0) if hasattr(index_row, "get") else 0
1103
  summary_cards = [
1104
+ ("Year covered", latest_year),
1105
+ ("Filed with Senate", latest_filing_date),
1106
+ ("Records found", f"{fmt_int(filing_count)} public forms; {fmt_int(transaction_count)} trade rows"),
 
1107
  ]
1108
  card_html = "".join(
1109
  f"<div><strong>{html_escape(label)}</strong><span>{html_escape(value)}</span></div>"
 
1133
  more_note = f"<p class='table-note'>Showing 6 of {fmt_int(len(ordered))} past filing summaries. Download files and technical rows live in Advanced.</p>"
1134
  return (
1135
  "<div class='result-card wealth-result wealth-reader'>"
1136
+ f"<div class='profile-kicker'>Latest filing in plain English</div>"
1137
+ f"<h2>{html_escape(name_for_id(senator_id))}</h2>"
1138
+ "<p class='profile-lens'>This is a summary of public Senate disclosure forms, not a judgment about the person.</p>"
1139
+ f"<div class='wealth-answer-card'><span>What the latest financial form says</span><strong>{html_escape(latest_plain['short'])}</strong><p>{html_escape(latest_plain['sentence'])} {html_escape(latest_plain['note'])}</p><p class='why-care-line'>Why someone might care: it shows the financial picture this official publicly reported for that year, and lets readers compare filings over time with source receipts.</p></div>"
1140
  f"<div class='wealth-plain-grid'>{card_html}</div>"
1141
+ "<details class='reader-details'>"
1142
+ "<summary>How to read this number</summary>"
1143
+ "<p>Senate forms report assets and debts in dollar ranges. This app reads those ranges and summarizes the public form as a low-to-high range, or as a floor when the top end cannot be calculated.</p>"
1144
+ f"<p>{html_escape(latest_plain['missing'])} The number cannot show exact net worth, taxable income, trading profit, intent, wrongdoing, or whether any trade made money.</p>"
1145
+ "</details>"
 
 
 
1146
  f"<p class='wealth-movement'>{html_escape(wealth_movement_sentence(ordered))}</p>"
1147
  f"<div class='receipt-strip'><span>Latest filing date: <strong>{html_escape(latest_filing_date)}</strong></span>{source_link}</div>"
1148
  "</div>"
1149
+ "<details class='result-card wealth-years-card history-drawer'>"
1150
+ "<summary>Show year-by-year filings</summary>"
1151
+ "<div class='wealth-year-guide'><strong>What each year means</strong><span>The year is the report year named by the public filing. The dollar phrase is what that filing supports for reported assets minus reported debts. A floor is not a ranking, and changes over time can reflect broad form ranges, purchases, sales, debts, filing differences, or parser limits.</span></div>"
1152
  f"<ul class='wealth-year-list'>{''.join(year_items)}</ul>"
1153
  f"{more_note}"
1154
+ "</details>"
1155
  )
1156
 
1157
 
 
1426
  margin: 10px 0 0 !important;
1427
  font-weight: 700 !important;
1428
  }
1429
+ .reader-model {
1430
+ display: flex;
1431
+ flex-wrap: wrap;
1432
+ gap: 8px;
1433
+ margin-top: 14px;
1434
+ }
1435
+ .reader-model span {
1436
+ background: #eef8f6 !important;
1437
+ border: 1px solid #99d4cb !important;
1438
+ border-radius: 999px;
1439
+ color: #073d38 !important;
1440
+ font-weight: 800 !important;
1441
+ padding: 8px 11px;
1442
+ }
1443
  .not-verdict {
1444
  display: inline-block;
1445
  margin-top: 14px;
 
1726
  .wealth-answer-card strong {
1727
  display: block;
1728
  color: #061a18 !important;
1729
+ font-size: 2rem !important;
1730
  line-height: 1.08 !important;
1731
  font-weight: 800 !important;
1732
  margin-bottom: 8px;
 
1737
  line-height: 1.55 !important;
1738
  margin: 0 !important;
1739
  }
1740
+ .wealth-answer-card .why-care-line {
1741
+ margin-top: 10px !important;
1742
+ font-weight: 800 !important;
1743
+ }
1744
  .wealth-plain-grid {
1745
  display: grid;
1746
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1747
  gap: 10px;
1748
  margin-top: 12px;
1749
  }
 
1786
  color: #2b1700 !important;
1787
  margin: 0 !important;
1788
  }
1789
+ .wealth-year-guide span {
1790
+ color: #2b1700 !important;
1791
+ display: block;
1792
+ font-weight: 700 !important;
1793
+ line-height: 1.45 !important;
1794
+ }
1795
  .wealth-reader h2 {
1796
  max-width: 860px;
1797
  }
1798
+ .profile-lens {
1799
+ color: #334155 !important;
1800
+ font-weight: 800 !important;
1801
+ margin: 0 0 10px !important;
1802
+ }
1803
  .wealth-lede {
1804
  font-size: 1.12rem !important;
1805
  line-height: 1.5 !important;
 
1896
  color: #334155 !important;
1897
  font-weight: 700 !important;
1898
  }
1899
+ .reader-details,
1900
+ .history-drawer {
1901
+ color: #111827 !important;
1902
+ }
1903
+ .reader-details {
1904
+ background: #f8fafc !important;
1905
+ border: 1px solid #d7dee8 !important;
1906
+ border-radius: 10px;
1907
+ padding: 12px;
1908
+ margin-top: 12px;
1909
+ }
1910
+ .reader-details summary,
1911
+ .history-drawer summary {
1912
+ color: #0f172a !important;
1913
+ cursor: pointer;
1914
+ font-size: 1.05rem !important;
1915
+ font-weight: 800 !important;
1916
+ list-style-position: inside;
1917
+ }
1918
+ .reader-details p {
1919
+ color: #111827 !important;
1920
+ margin: 8px 0 0 !important;
1921
+ }
1922
  .wealth-movement {
1923
  margin-top: 12px !important;
1924
  font-weight: 700 !important;
 
2199
 
2200
  with gr.Tabs():
2201
  with gr.Tab("Search Senator"):
2202
+ gr.HTML("<div class='section-intro'><strong>Search a senator.</strong> The first result gives the latest filing in plain English. If this bundle has no usable rows for that exact record, it says that directly.</div>")
2203
  filer_query = gr.Textbox(
2204
  label="Name, state, or filer ID",
2205
  value="",
space_bundle_manifest.json CHANGED
@@ -8,8 +8,8 @@
8
  "files": [
9
  {
10
  "path": "app.py",
11
- "sha256": "71753950346a3ee4c449cc092e1752406b613285c8254f4be6b064a1e78df73a",
12
- "byte_count": 79376
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
@@ -107,5 +107,5 @@
107
  "byte_count": 47
108
  }
109
  ],
110
- "manifest_hash": "b98badb79a9cfb9e357f530af42affe26f415ef1c50807543b74853fcfb2ea7c"
111
  }
 
8
  "files": [
9
  {
10
  "path": "app.py",
11
+ "sha256": "019bf360828c2c49427393ec612e0f39ed360ca87edb543431f47c9a6a3ed007",
12
+ "byte_count": 81117
13
  },
14
  {
15
  "path": "data/audit/claim_boundary.json",
 
107
  "byte_count": 47
108
  }
109
  ],
110
+ "manifest_hash": "36f44dce48fedfbbf0a99d93baeb2dd2c8a1d9b3625bd53c5e5c418eec76a953"
111
  }