File size: 27,975 Bytes
ea38845
 
3169621
ea38845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
ea38845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3169621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea38845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea38845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea38845
 
 
 
 
 
 
 
396ecfd
 
 
ea38845
 
 
 
 
 
 
 
 
964d30b
 
 
 
 
 
 
 
8857402
 
 
 
 
e8825ff
 
964d30b
 
e8825ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8825ff
 
 
 
 
 
 
 
 
 
 
 
 
 
6418461
 
 
 
 
 
 
 
 
 
e8825ff
 
 
 
 
 
 
 
 
 
 
 
 
3169621
 
 
 
 
 
 
 
ea38845
3169621
ea38845
 
3169621
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
 
 
 
e8825ff
 
 
 
 
 
 
 
 
 
3169621
ea38845
 
 
e8825ff
3169621
396ecfd
ea38845
 
3169621
 
ea38845
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396ecfd
 
 
 
 
 
 
 
 
 
 
 
ea38845
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
from __future__ import annotations

import html
import os
from functools import lru_cache
from pathlib import Path

import gradio as gr
import pandas as pd


ROOT = Path(__file__).parent
DATA = ROOT / "data"


@lru_cache(maxsize=1)
def metrics():
    import json

    return json.loads((DATA / "summary_metrics.json").read_text(encoding="utf-8"))


@lru_cache(maxsize=1)
def places():
    return pd.read_csv(DATA / "place_exposure.csv")


@lru_cache(maxsize=1)
def missing_geocodes():
    return pd.read_csv(DATA / "missing_geocode_examples.csv")


@lru_cache(maxsize=1)
def public_infrastructure_sites():
    path = DATA / "public_infrastructure_sites.csv"
    if not path.exists():
        return pd.DataFrame()
    return pd.read_csv(path)


@lru_cache(maxsize=1)
def events():
    return pd.read_csv(DATA / "event_explanatory_features.csv.gz")


def state_choices():
    state_values = sorted(str(value) for value in places()["state"].dropna().unique())
    return ["All"] + state_values


def bucket_choices():
    values = sorted(str(value) for value in missing_geocodes()["reason_bucket"].dropna().unique())
    return ["All"] + values


def population_bins():
    return ["All", "lt_10k", "10k_50k", "50k_250k", "250k_1m", "gte_1m", "unknown_population"]


def score_cards():
    m = metrics()
    assessment = m["assessment"]
    population = m["population_summary"]
    airport = m["airport_summary"]
    missing = m["missing_geocode_summary"]
    return f"""

<div class="score-grid">

  <div class="score-card"><div class="label">Population signal</div><div class="score">{assessment["population_explanation_strength_score"]}/100</div><div class="small">log-pop/report correlation {population["log_population_log_event_count_pearson"]}</div></div>

  <div class="score-card"><div class="label">Airport confounder</div><div class="score">{assessment["airport_confounder_strength_score"]}/100</div><div class="small">event median {airport["event_nearest_major_airport_distance_median_miles"]} mi</div></div>

  <div class="score-card"><div class="label">Nuclear non-support</div><div class="score">{assessment["nuclear_non_support_strength_score"]}/100</div><div class="small">matched controls beat nuclear in the primary test</div></div>

  <div class="score-card"><div class="label">Missing geocodes</div><div class="score">{missing["missing_geocode_rows"]}</div><div class="small">{missing["missing_geocode_share"]:.1%} of U.S. rows</div></div>

</div>

"""


def fmt_int(value):
    try:
        return f"{int(float(value)):,}"
    except (TypeError, ValueError):
        return str(value)


def fmt_float(value, digits=2):
    try:
        return f"{float(value):,.{digits}f}"
    except (TypeError, ValueError):
        return str(value)


def pct(value, digits=1):
    try:
        return f"{float(value) * 100:.{digits}f}%"
    except (TypeError, ValueError):
        return str(value)


def bar_row(label, value, max_value, color_class, note=""):
    width = 0 if max_value == 0 else max(2, min(100, (float(value) / float(max_value)) * 100))
    safe_label = html.escape(str(label))
    safe_note = html.escape(str(note))
    return f"""

    <div class="bar-row" role="img" aria-label="{safe_label}: {fmt_float(value, 2)} {safe_note}">

      <div class="bar-label"><span>{safe_label}</span><strong>{fmt_float(value, 2)}</strong></div>

      <div class="bar-track"><div class="bar-fill {color_class}" style="width:{width:.1f}%"></div></div>

      <div class="bar-note">{safe_note}</div>

    </div>

    """


def share_bar(label, value, color_class, note=""):
    safe_label = html.escape(str(label))
    safe_note = html.escape(str(note))
    try:
        width = max(2, min(100, float(value) * 100))
    except (TypeError, ValueError):
        width = 0
    return f"""

    <div class="share-row" role="img" aria-label="{safe_label}: {pct(value)} {safe_note}">

      <div class="share-label"><span>{safe_label}</span><strong>{pct(value)}</strong></div>

      <div class="bar-track"><div class="bar-fill {color_class}" style="width:{width:.1f}%"></div></div>

      <div class="bar-note">{safe_note}</div>

    </div>

    """


def visual_story_html():
    m = metrics()
    assessment = m["assessment"]
    stats = m["nuclear_statistical_tests"]
    primary = stats["tests_by_radius"]["50"]
    population = m["population_summary"]
    airport = m["airport_summary"]
    missing = m["missing_geocode_summary"]

    nuclear_mean = float(primary["nuclear_mean_reports_per_site"])
    control_mean = float(primary["control_mean_reports_per_site"])
    comparison_max = max(nuclear_mean, control_mean)
    nuclear_bars = (
        bar_row("Nuclear power-plant sites", nuclear_mean, comparison_max, "fill-nuclear", "mean reports per site within 50 miles")
        + bar_row("Matched non-nuclear power controls", control_mean, comparison_max, "fill-control", "mean reports per site within 50 miles")
    )

    bin_labels = {
        "lt_10k": "Places under 10k people",
        "10k_50k": "10k to 50k people",
        "50k_250k": "50k to 250k people",
        "250k_1m": "250k to 1M people",
        "gte_1m": "1M+ people",
        "unknown_population": "Unknown population",
    }
    population_bins = population["population_bin_event_distribution"]
    pop_rows = []
    for key in ["lt_10k", "10k_50k", "50k_250k", "250k_1m", "gte_1m", "unknown_population"]:
        value = population_bins.get(key, {}).get("event_share", 0)
        pop_rows.append(share_bar(bin_labels[key], value, "fill-population", f"{fmt_int(population_bins.get(key, {}).get('event_count', 0))} rows"))
    population_bars = "".join(pop_rows)

    airport_rows = []
    for radius in (10, 25, 50):
        event_key = f"event_share_within_{radius}_miles_major_airport"
        pop_key = f"population_weighted_share_within_{radius}_miles_major_airport"
        airport_rows.append(f"""

        <div class="paired-block">

          <div class="paired-title">Within {radius} miles of a major scheduled airport</div>

          {share_bar("Public report rows", airport[event_key], "fill-airport", "event-place centroids")}

          {share_bar("Population-weighted places", airport[pop_key], "fill-baseline", "baseline geography")}

        </div>

        """)
    airport_bars = "".join(airport_rows)

    missing_max = max(missing["reason_bucket_counts"].values()) if missing.get("reason_bucket_counts") else 1
    missing_bars = "".join(
        bar_row(label.replace("_", " ").title(), count, missing_max, "fill-gap", "rows needing recovery")
        for label, count in missing.get("reason_bucket_counts", {}).items()
    )

    return f"""

<section class="hero-panel">

  <div class="eyebrow">Public-source evidence surface</div>

  <h1>The nuclear-specific UAP proximity claim does not hold in this dataset</h1>

  <p class="lede">We tested public geocoded report rows against nuclear power-plant sites and matched non-nuclear power-plant controls. The stronger visible pattern is reporting geography: where people live, report, and see the sky.</p>

  <div class="claim-path" aria-label="Question, test, result">

    <div><span>Question</span><strong>Do reports cluster near nuclear plants?</strong></div>

    <div><span>Control</span><strong>Compare with similar non-nuclear power sites</strong></div>

    <div><span>Result</span><strong>No nuclear-specific lift observed</strong></div>

  </div>

</section>



<section class="visual-grid">

  <article class="viz-card wide">

    <h2>Matched controls are the comparison anchor</h2>

    <p>The 50-mile test compares nuclear power-plant sites with matched non-nuclear power-plant controls. If the nuclear claim were visible here, the nuclear bar would be clearly higher.</p>

    {nuclear_bars}

    <div class="takeaway">Nuclear/control ratio: <strong>{primary["nuclear_to_control_mean_ratio"]}</strong>. One-sided p-value for nuclear greater than controls: <strong>{primary["p_value_one_sided_nuclear_greater"]}</strong>.</div>

  </article>



  <article class="viz-card">

    <h2>Population is the first pattern to notice</h2>

    <p>Report rows are not evenly distributed across places. Place population and report count move together on a log scale.</p>

    <div class="big-number">{population["log_population_log_event_count_pearson"]}</div>

    <div class="big-number-label">log population vs. log report-count correlation</div>

    <div class="takeaway">{fmt_int(population["matched_population_event_count"])} of {fmt_int(population["event_count"])} rows matched to Census population places.</div>

  </article>



  <article class="viz-card">

    <h2>Airport proximity mostly follows people</h2>

    <p>Major airports are close to many reports, but they are also close to much of the U.S. population.</p>

    <div class="big-number">{airport["event_nearest_major_airport_distance_median_miles"]} mi</div>

    <div class="big-number-label">median report-row distance to a major scheduled airport</div>

    <div class="takeaway">Population-weighted Census-place median: <strong>{airport["population_weighted_nearest_major_airport_distance_median_miles"]} mi</strong>.</div>

  </article>



  <article class="viz-card wide">

    <h2>Reports span place sizes, with large places carrying much of the volume</h2>

    <p>These bars show the share of report rows by Census-place population bin. The table tabs remain available for exact rows.</p>

    {population_bars}

  </article>



  <article class="viz-card wide">

    <h2>Airport proximity should be read against the population baseline</h2>

    <p>If public report rows and the population baseline are close, airport proximity is a confounder rather than a standalone explanation.</p>

    {airport_bars}

  </article>



  <article class="viz-card wide">

    <h2>Uncertainty is visible, not hidden</h2>

    <p>{fmt_int(missing["missing_geocode_rows"])} U.S. rows did not resolve to Census place centroids. They are excluded from spatial tests until recovered by a public geocoder path.</p>

    {missing_bars}

  </article>

</section>



<section class="claim-box">

  <h2>How to read the evidence</h2>

  <div class="claim-grid">

    <div><strong>Supported</strong><span>Population/reporting geography is a strong observable factor.</span></div>

    <div><strong>Confounder</strong><span>Airport proximity is measurable, but close to the population baseline.</span></div>

    <div><strong>Not supported here</strong><span>A nuclear-specific proximity lift after matched power-plant controls.</span></div>

    <div><strong>Still outside scope</strong><span>Classified activity, exact witness GPS, and case-level truth claims.</span></div>

  </div>

</section>

"""


def overview_markdown():
    m = metrics()
    assessment = m["assessment"]
    stats = m["nuclear_statistical_tests"]
    primary = stats["tests_by_radius"]["50"]
    can_claim = "\n".join(f"- {item}" for item in assessment["what_we_can_claim"])
    cannot_claim = "\n".join(f"- {item}" for item in assessment["what_we_cannot_claim"])
    return f"""

# Nuclear UAP Evidence Surface



This is a public-source, reduced analytical dataset for one question: do public UFO/UAP report rows cluster around nuclear power plants after ordinary controls?



**Finding:** {assessment["plain_english_assessment"]}



Primary 50-mile test:



- Public geocoded report rows: `{stats["uap_event_count"]:,}`

- Nuclear sites: `{stats["nuclear_site_count"]}`

- Matched non-nuclear controls: `{stats["control_site_count"]}`

- Nuclear/control mean ratio: `{primary["nuclear_to_control_mean_ratio"]}`

- One-sided p-value for nuclear greater than controls: `{primary["p_value_one_sided_nuclear_greater"]}`



## What it is



- A public-source evidence surface with hashes and receipts.

- A reduced table of place/date/shape/geocode/proximity features.

- A way to inspect population, airport, and nuclear-site proximity factors.



## What it is not



- It is not proof that any report is true or false.

- It is not a claim that aircraft explain every report.

- It is not exact sighting GPS; rows use public Census place centroids.

- It is not a test of classified weapons-site activity.



## What we can claim



{can_claim}



## What we cannot claim



{cannot_claim}

"""


def public_infrastructure_html():
    layer_stats = metrics().get("public_infrastructure_layers") or {}
    reports = layer_stats.get("layer_reports") or {}
    if not reports:
        return ""
    cards = []
    labels = {
        "public_military_installation": "Public military installations",
        "public_nuclear_security_enterprise": "Public NNSA / nuclear-security-enterprise sites",
        "public_strategic_delivery_installation": "Public strategic-delivery subset",
    }
    for key in [
        "public_military_installation",
        "public_nuclear_security_enterprise",
        "public_strategic_delivery_installation",
    ]:
        report = reports.get(key) or {}
        if not report:
            continue
        near_50 = (report.get("near_any_counts") or {}).get("50", 0)
        share_50 = report.get("event_share_within_50_miles", 0)
        cards.append(
            f"""

            <div class="infra-card">

              <div class="infra-label">{html.escape(labels.get(key, key))}</div>

              <div class="infra-sites">{fmt_int(report.get("site_count", 0))} sites</div>

              <div class="infra-detail">{fmt_int(near_50)} public report rows within 50 miles ({pct(share_50)})</div>

            </div>

            """
        )
    can_claim = "".join(f"<li>{html.escape(str(item))}</li>" for item in layer_stats.get("what_we_can_claim", []))
    cannot_claim = "".join(f"<li>{html.escape(str(item))}</li>" for item in layer_stats.get("what_we_cannot_claim", []))
    return f"""

<section class="infra-panel">

  <div class="eyebrow">Added public infrastructure layer</div>

  <h2>Military and nuclear-security infrastructure are descriptive context, not a causation test</h2>

  <p>This layer adds official NTAD military installations and public NNSA/nuclear-security-enterprise anchors, including an NNSS public coordinate receipt. It helps people inspect proximity without implying classified activity, weapons storage, or attraction.</p>

  <div class="infra-grid">{''.join(cards)}</div>

  <div class="infra-claim-grid">

    <div><strong>What this layer can say</strong><ul>{can_claim}</ul></div>

    <div><strong>What this layer cannot say</strong><ul>{cannot_claim}</ul></div>

  </div>

</section>

"""


def filter_places(state, min_events, sort_by):
    df = places().copy()
    if state and state != "All":
        df = df[df["state"] == state]
    df = df[df["event_count"].fillna(0) >= int(min_events)]
    sort_col = "event_count" if sort_by == "Report count" else "events_per_100k_population"
    df = df.sort_values(sort_col, ascending=False)
    cols = [
        "place_name",
        "state",
        "population",
        "event_count",
        "events_per_100k_population",
        "nearest_major_airport_name",
        "nearest_major_airport_distance_miles",
    ]
    return df[cols].head(200)


def filter_events(state, pop_bin, airport_radius, sample_rows):
    df = events().copy()
    if state and state != "All":
        df = df[df["state"] == state]
    if pop_bin and pop_bin != "All":
        df = df[df["population_bin"] == pop_bin]
    if airport_radius == "Within 10 miles":
        df = df[df["within_10_miles_major_airport"] == "yes"]
    elif airport_radius == "Within 25 miles":
        df = df[df["within_25_miles_major_airport"] == "yes"]
    elif airport_radius == "Beyond 50 miles":
        df = df[df["within_50_miles_major_airport"] == "no"]
    cols = [
        "event_date",
        "city",
        "state",
        "shape",
        "population_bin",
        "nearest_major_airport_name",
        "nearest_major_airport_distance_miles",
        "nearest_nuclear_distance_miles",
        "nearest_control_distance_miles",
    ]
    return df[cols].head(int(sample_rows))


def filter_missing(bucket):
    df = missing_geocodes().copy()
    if bucket and bucket != "All":
        df = df[df["reason_bucket"] == bucket]
    return df.head(200)


def filter_public_infrastructure(layer):
    df = public_infrastructure_sites().copy()
    if df.empty:
        return df
    if layer and layer != "All":
        df = df[df["site_layer"] == layer]
    cols = [
        "site_name",
        "site_layer",
        "site_kind",
        "state",
        "coordinate_basis",
        "source_name",
        "source_confidence",
        "public_role",
    ]
    return df[[col for col in cols if col in df.columns]].head(300)


def download_files():
    files = [
        DATA / "event_explanatory_features.csv.gz",
        DATA / "place_exposure.csv",
        DATA / "missing_geocode_examples.csv",
        DATA / "nuclear_sites.csv",
        DATA / "matched_controls.csv",
        DATA / "site_proximity_summary.csv",
        DATA / "public_infrastructure_sites.csv",
        DATA / "public_infrastructure_site_summary.csv",
        DATA / "event_public_infrastructure_proximity.csv.gz",
        DATA / "statistical_tests.json",
        DATA / "summary_metrics.json",
        DATA / "source_receipts_combined.csv",
        DATA / "artifact_manifest.json",
    ]
    return [str(path) for path in files if path.exists()]


css = """

html,

body,

#root,

.app,

gradio-app {

  background: #f3f6fa !important;

  color: #18232f !important;

}

html body p,

html body a,

html body [role="link"] {

  color: #294860 !important;

}

.gradio-container {

  max-width: 1180px !important;

  margin-left: auto !important;

  margin-right: auto !important;

  color-scheme: light;

  background: #f3f6fa !important;

  color: #18232f !important;

  --body-background-fill: #f3f6fa;

  --body-text-color: #18232f;

  --block-background-fill: #ffffff;

  --block-border-color: #d7dde8;

  --block-label-text-color: #24324a;

  --input-background-fill: #ffffff;

  --input-border-color: #c8d2de;

  --button-secondary-background-fill: #ffffff;

  --button-secondary-text-color: #18232f;

}

.gradio-container,

.gradio-container h1,

.gradio-container h2,

.gradio-container h3,

.gradio-container h4,

.gradio-container p,

.gradio-container li,

.gradio-container label,

.gradio-container td,

.gradio-container th,

.gradio-container input,

.gradio-container textarea,

.gradio-container select {

  color: #18232f !important;

}

.gradio-container input,

.gradio-container textarea,

.gradio-container select,

.gradio-container [data-testid="dropdown"],

.gradio-container [data-testid="dropdown"] *,

.gradio-container .dropdown,

.gradio-container .dropdown *,

.gradio-container .select-wrap,

.gradio-container .select-wrap *,

.gradio-container .wrap-inner,

.gradio-container .wrap-inner *,

.gradio-container .secondary-wrap,

.gradio-container .secondary-wrap *,

.gradio-container [role="listbox"],

.gradio-container [role="listbox"] *,

.gradio-container [role="option"],

.gradio-container [role="option"] {

  color: #18232f !important;

  background-color: #ffffff !important;

  opacity: 1 !important;

}

.gradio-container [role="option"]:hover,

.gradio-container [role="option"][aria-selected="true"],

.gradio-container .option:hover,

.gradio-container .item:hover {

  color: #0f2533 !important;

  background-color: #e7f1f7 !important;

}

.gradio-container .prose,

.gradio-container .markdown,

.gradio-container [data-testid="markdown"],

.gradio-container [data-testid="block-label"],

.gradio-container .dataframe,

.gradio-container .wrap {

  color: #18232f !important;

  background: transparent;

}

.gradio-container button,

.gradio-container [role="tab"],

.gradio-container [role="button"] {

  color: #18232f !important;

}

.gradio-container a,

.gradio-container footer,

.gradio-container footer *,

.gradio-container [data-testid="built-with"],

.gradio-container [data-testid="built-with"] * {

  color: #294860 !important;

}

.gradio-container footer {

  background: #f3f6fa !important;

}

.gradio-container table,

.gradio-container thead,

.gradio-container tbody,

.gradio-container tr,

.gradio-container td,

.gradio-container th {

  background-color: #ffffff !important;

}

.gradio-container code {

  color: #18232f !important;

  background: #eef2f6 !important;

  border: 1px solid #d7dde8;

}

.hero-panel { border: 1px solid #d7dde8; border-radius: 6px; padding: 24px; background: #f7fbfd; margin-bottom: 18px; }

.eyebrow { color: #48606f; font-size: 13px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; }

.hero-panel h1 { font-size: 32px; line-height: 1.14; margin: 8px 0 10px; color: #18232f; }

.lede { font-size: 17px; max-width: 860px; color: #344554; }

.claim-path { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 10px; margin-top: 18px; }

.claim-path div { border-left: 4px solid #12719e; background: white; padding: 12px; }

.claim-path span { display: block; color: #5b6975; font-size: 13px; font-weight: 700; }

.claim-path strong { display: block; margin-top: 4px; color: #1d2b36; }

.score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 12px; margin: 16px 0; }

.score-card, .viz-card, .claim-box { border: 1px solid #d7dde8; border-radius: 6px; padding: 16px; background: #fff; }

.label { font-weight: 700; color: #24324a; }

.score { font-size: 30px; font-weight: 800; margin: 6px 0; }

.small, .bar-note, .big-number-label { font-size: 13px; color: #536070; }

.visual-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }

.viz-card h2, .claim-box h2 { font-size: 18px; margin: 0 0 8px; color: #18232f; }

.viz-card p { color: #415160; margin-top: 0; }

.wide { grid-column: 1 / -1; }

.bar-row, .share-row { margin: 12px 0; }

.bar-label, .share-label { display: flex; justify-content: space-between; gap: 12px; font-size: 14px; margin-bottom: 4px; }

.bar-track { height: 14px; background: #eceff3; border-radius: 4px; overflow: hidden; }

.bar-fill { height: 100%; border-radius: 4px; }

.fill-nuclear { background: #12719e; }

.fill-control { background: #ca5800; }

.fill-population { background: #408941; }

.fill-airport { background: #af1f6b; }

.fill-baseline { background: #696969; }

.fill-gap { background: #e88e2d; }

.takeaway { margin-top: 12px; padding: 10px 12px; background: #f4f6f8; border-left: 4px solid #696969; color: #25313c; }

.big-number { font-size: 42px; font-weight: 850; color: #12719e; line-height: 1; margin-top: 12px; }

.paired-block { margin: 14px 0 18px; }

.paired-title { font-weight: 750; margin-bottom: 8px; color: #25313c; }

.claim-box { margin-top: 16px; background: #fbfcff; }

.claim-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; }

.claim-grid div { background: white; border: 1px solid #d7dde8; border-radius: 6px; padding: 12px; }

.claim-grid strong { display: block; color: #18232f; margin-bottom: 5px; }

.claim-grid span { color: #40515f; }

.infra-panel { border: 1px solid #d7dde8; border-radius: 6px; padding: 18px; background: #fff; margin: 16px 0; }

.infra-panel h2 { font-size: 20px; margin: 6px 0 8px; color: #18232f; }

.infra-panel p { color: #40515f; margin-top: 0; }

.infra-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 10px; margin: 14px 0; }

.infra-card { border: 1px solid #d7dde8; border-radius: 6px; padding: 12px; background: #f7fbfd; }

.infra-label { color: #425466; font-weight: 750; font-size: 13px; }

.infra-sites { color: #12719e; font-size: 28px; font-weight: 850; margin-top: 4px; }

.infra-detail { color: #25313c; font-size: 13px; }

.infra-claim-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 10px; }

.infra-claim-grid div { background: #fbfcff; border: 1px solid #d7dde8; border-radius: 6px; padding: 12px; }

.infra-claim-grid ul { margin: 8px 0 0; padding-left: 18px; color: #40515f; }

@media (prefers-color-scheme: dark) {

  .gradio-container {

    background: #f3f6fa !important;

    color: #18232f !important;

  }

  .hero-panel, .score-card, .viz-card, .claim-box, .claim-grid div {

    background-color: #ffffff !important;

    color: #18232f !important;

  }

}

@media (max-width: 760px) { .visual-grid { grid-template-columns: 1fr; } .hero-panel h1 { font-size: 26px; } }

"""


with gr.Blocks(css=css, title="Nuclear UAP Evidence Surface", theme=gr.themes.Soft()) as demo:
    gr.HTML(visual_story_html())
    gr.HTML(public_infrastructure_html())
    gr.HTML(score_cards())
    with gr.Tabs():
        with gr.Tab("Claim Boundaries"):
            gr.Markdown(overview_markdown())
        with gr.Tab("Explore Places"):
            with gr.Row():
                place_state = gr.Dropdown(choices=state_choices(), value="All", label="State")
                min_events = gr.Slider(0, 100, value=10, step=1, label="Minimum report rows")
                place_sort = gr.Radio(["Report count", "Per-capita rate"], value="Report count", label="Sort")
            place_table = gr.Dataframe(value=filter_places("All", 10, "Report count"), interactive=False, wrap=True)
            for control in (place_state, min_events, place_sort):
                control.change(filter_places, [place_state, min_events, place_sort], place_table)
        with gr.Tab("Explore Event Rows"):
            with gr.Row():
                event_state = gr.Dropdown(choices=state_choices(), value="All", label="State")
                event_bin = gr.Dropdown(choices=population_bins(), value="All", label="Population bin")
                airport_filter = gr.Radio(["Any", "Within 10 miles", "Within 25 miles", "Beyond 50 miles"], value="Any", label="Major airport proximity")
                sample_rows = gr.Slider(25, 500, value=100, step=25, label="Rows")
            event_table = gr.Dataframe(value=filter_events("All", "All", "Any", 100), interactive=False, wrap=True)
            for control in (event_state, event_bin, airport_filter, sample_rows):
                control.change(filter_events, [event_state, event_bin, airport_filter, sample_rows], event_table)
        with gr.Tab("Missing Geocodes"):
            bucket = gr.Dropdown(choices=bucket_choices(), value="All", label="Gap bucket")
            missing_table = gr.Dataframe(value=filter_missing("All"), interactive=False, wrap=True)
            bucket.change(filter_missing, bucket, missing_table)
        with gr.Tab("Military / NNSA Layer"):
            infra_layer = gr.Dropdown(
                choices=[
                    "All",
                    "public_military_installation",
                    "public_nuclear_security_enterprise",
                ],
                value="All",
                label="Public infrastructure layer",
            )
            infra_table = gr.Dataframe(value=filter_public_infrastructure("All"), interactive=False, wrap=True)
            infra_layer.change(filter_public_infrastructure, infra_layer, infra_table)
        with gr.Tab("Downloads & Receipts"):
            gr.Markdown("Download the reduced analytical tables, source receipts, hashes, and manifest. These exports do not include raw witness summaries.")
            gr.File(value=download_files(), file_count="multiple", label="Dataset files")


if __name__ == "__main__":
    server_port = int(os.environ.get("GRADIO_SERVER_PORT", "7860"))
    server_name = os.environ.get("GRADIO_SERVER_NAME") or None
    demo.launch(server_name=server_name, server_port=server_port)