omerehrlich commited on
Commit
a04ea44
·
1 Parent(s): 51a7cc4

Drop taxonomy label 11; renumber 'None of the above' from 12 to 11

Browse files
api.py CHANGED
@@ -2,13 +2,11 @@ import json
2
  import os
3
  import threading
4
  import time
5
- from typing import Any, Dict, List, Optional
6
 
7
  import pandas as pd
8
  from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
9
  from fastapi.middleware.cors import CORSMiddleware
10
- from fastapi.middleware.gzip import GZipMiddleware
11
- from fastapi.responses import FileResponse
12
  from fastapi.staticfiles import StaticFiles
13
  from pydantic import BaseModel
14
 
@@ -30,10 +28,6 @@ progress_lock = threading.Lock()
30
  # Initialize FastAPI app
31
  app = FastAPI(title="Wiki Revision Classifier API")
32
 
33
- # Compress JSON responses larger than 1 KB. Big win for /demo and /results
34
- # (multi-MB JSON payloads shrink ~70% on the wire).
35
- app.add_middleware(GZipMiddleware, minimum_size=1024)
36
-
37
  # Enable CORS
38
  app.add_middleware(
39
  CORSMiddleware,
@@ -52,13 +46,12 @@ async def add_cache_control_headers(request: Request, call_next):
52
  response.headers["Expires"] = "0"
53
  return response
54
 
55
- # Prepare backend file directory.
56
- # Mounted at /files so it doesn't collide with CRA's /static asset paths.
57
  os.makedirs("visualizations", exist_ok=True)
58
  app.mount(
59
- "/files",
60
  StaticFiles(directory="visualizations"),
61
- name="files"
62
  )
63
 
64
  # Pydantic models
@@ -115,7 +108,7 @@ async def prepare_article(payload: PreparePayload) -> Dict[str, Any]:
115
 
116
  return {
117
  "previewRows": preview,
118
- "csvUrl": f"/files/{csv_filename}",
119
  "diffs": diffs,
120
  "revisionCount": revision_count,
121
  }
@@ -126,7 +119,7 @@ async def classify_article(payload: ClassifyPayload, background_tasks: Backgroun
126
  Start classification in the background and return immediately.
127
  Uses the flexible-linker pipeline + OpenAI Batch API (single batch).
128
  """
129
- api_key = payload.api_key
130
  model_name = payload.model or "gpt-5-mini"
131
 
132
  df, title = fetch_revision_history(payload.article)
@@ -165,7 +158,6 @@ async def classify_article(payload: ClassifyPayload, background_tasks: Backgroun
165
  background_tasks.add_task(
166
  process_classification,
167
  model_name=model_name,
168
- api_key=api_key,
169
  prompts=prompts,
170
  df=prompted_df,
171
  title=title,
@@ -224,7 +216,7 @@ def compute_grouped_idx(results_df: pd.DataFrame) -> pd.Series:
224
  return pd.Series(out, index=results_df.index, dtype=int)
225
 
226
 
227
- def process_classification(model_name, api_key, prompts, df, title, taxonomy):
228
  """Run the OpenAI batch classification in the background."""
229
  safe_title = title.replace(" ", "_")
230
  total = len(prompts)
@@ -247,7 +239,7 @@ def process_classification(model_name, api_key, prompts, df, title, taxonomy):
247
  with progress_lock:
248
  progress_map.setdefault(safe_title, {})["stage"] = "submitting"
249
 
250
- model = GPT(model_name=model_name, api_key=api_key)
251
  raw_responses = model.predict(prompts, status_callback=_update_batch_status)
252
 
253
  with progress_lock:
@@ -283,29 +275,8 @@ def process_classification(model_name, api_key, prompts, df, title, taxonomy):
283
  result_path = os.path.join("visualizations", result_filename)
284
  results_df.to_csv(result_path, index=False)
285
 
286
- # Stash the full DataFrame so /summarize can look up Explanations
287
- # for any (label, section, period) cell the user clicks.
288
- _store_dataset(title, results_df)
289
-
290
- # Generate the page-evolution summary now while we still have the
291
- # user's API key in scope. Persist alongside the results CSV.
292
- evolution_filename = f"{safe_title}_evolution.txt"
293
- evolution_path = os.path.join("visualizations", evolution_filename)
294
- try:
295
- with progress_lock:
296
- progress_map.setdefault(safe_title, {})["stage"] = "summarizing"
297
- evolution_text = generate_evolution_summary(
298
- results_df, api_key=api_key, model=model_name
299
- )
300
- if evolution_text:
301
- with open(evolution_path, "w", encoding="utf-8") as f:
302
- f.write(evolution_text)
303
- except Exception as e:
304
- # Don't fail the whole classification if the summary errors.
305
- print(f"[warn] evolution summary failed: {e}")
306
-
307
  print("Creating visualizations...")
308
- taxonomy[12] = {
309
  "category": "None of the above",
310
  "description": "No meaningful scientific changes were detected between the revisions; applies to non-scientific, formatting, or trivial edits."
311
  }
@@ -359,24 +330,12 @@ async def get_results(article: str) -> Dict[str, Any]:
359
 
360
  # Load results
361
  results_df = pd.read_csv(result_path)
362
- _store_dataset(article, results_df)
363
  preview_rows = results_df.head(5).to_dict(orient="records")
364
 
365
  # URL paths for frontend
366
- csv_url = f"/files/{result_filename}"
367
- distribution_url = f"/files/{dist_filename}"
368
- cooccurrence_url = f"/files/{cooc_filename}"
369
-
370
- # Optional: page-evolution summary (label 1 + 6 across years).
371
- evolution_filename = f"{safe_title}_evolution.txt"
372
- evolution_path = os.path.join("visualizations", evolution_filename)
373
- evolution_text: Optional[str] = None
374
- if os.path.exists(evolution_path):
375
- try:
376
- with open(evolution_path, "r", encoding="utf-8") as f:
377
- evolution_text = f.read().strip() or None
378
- except Exception:
379
- evolution_text = None
380
 
381
  return {
382
  "status": "complete",
@@ -385,484 +344,8 @@ async def get_results(article: str) -> Dict[str, Any]:
385
  "results": results_df.to_dict(orient="records"),
386
  "distributionUrl": distribution_url,
387
  "cooccurrenceUrl": cooccurrence_url,
388
- "evolutionSummary": evolution_text,
389
  }
390
 
391
- DEMOS = {
392
- "CRISPR": {
393
- "csv": "demo/crispr_results.csv",
394
- "title": "CRISPR",
395
- "evolution": "demo/crispr_evolution.txt",
396
- },
397
- }
398
-
399
- # Full per-article DataFrames retained in memory so the summarize endpoint
400
- # can look up Explanations for any (label, section, period) cell. Distinct
401
- # from `_demo_cache`, which holds the slim JSON payload sent to the browser.
402
- _dataset_cache: Dict[str, "pd.DataFrame"] = {}
403
- _dataset_cache_lock = threading.Lock()
404
-
405
-
406
- def _store_dataset(article: str, df: "pd.DataFrame") -> None:
407
- with _dataset_cache_lock:
408
- _dataset_cache[article] = df
409
-
410
-
411
- def _get_dataset(article: str) -> Optional["pd.DataFrame"]:
412
- with _dataset_cache_lock:
413
- return _dataset_cache.get(article)
414
-
415
- # Columns the frontend's interactive charts actually consume. We strip the
416
- # large free-text columns (Prompt/Explanation) from the bulk `results` array
417
- # to keep the JSON payload small; the first few preview rows keep them.
418
- CHART_COLUMNS = [
419
- "Section",
420
- "Timestamp",
421
- "Labels",
422
- "grouped_idx",
423
- "Level",
424
- "Flexible_Previous_Section",
425
- ]
426
-
427
- # In-memory cache for prepared demo payloads. Keyed by article name; cleared
428
- # on container restart. Reading + serializing the CSV is the slow part on
429
- # repeat clicks, so caching the final dict is the biggest win.
430
- _demo_cache: Dict[str, Dict[str, Any]] = {}
431
- _demo_cache_lock = threading.Lock()
432
-
433
-
434
- def _build_demo_payload(demo: Dict[str, str]) -> Dict[str, Any]:
435
- results_df = pd.read_csv(demo["csv"])
436
- # The bundled demo CSVs may lack grouped_idx (e.g. older offline pipelines);
437
- # compute it if absent so the frontend charts work uniformly.
438
- if "grouped_idx" not in results_df.columns:
439
- results_df["grouped_idx"] = compute_grouped_idx(results_df)
440
-
441
- # Stash the full DataFrame so /summarize can look up Explanations.
442
- _store_dataset(demo["title"], results_df)
443
-
444
- preview_rows = results_df.head(5).to_dict(orient="records")
445
-
446
- # Slim the bulk array: keep only columns the charts actually read.
447
- chart_cols = [c for c in CHART_COLUMNS if c in results_df.columns]
448
- slim_df = results_df[chart_cols]
449
-
450
- # Static, pre-generated evolution summary if the demo entry provides one.
451
- evolution_text: Optional[str] = None
452
- evo_path = demo.get("evolution")
453
- if evo_path and os.path.exists(evo_path):
454
- try:
455
- with open(evo_path, "r", encoding="utf-8") as f:
456
- evolution_text = f.read().strip() or None
457
- except Exception:
458
- evolution_text = None
459
-
460
- return {
461
- "status": "complete",
462
- "title": demo["title"],
463
- "previewRows": preview_rows,
464
- "results": slim_df.to_dict(orient="records"),
465
- "evolutionSummary": evolution_text,
466
- "csvUrl": f"/demo/download?article={demo['title']}",
467
- }
468
-
469
-
470
- @app.get("/demo")
471
- async def get_demo(article: str) -> Dict[str, Any]:
472
- """
473
- Return a pre-classified results dataset packaged with the Space.
474
- Lets users see the interactive visualizations without running their own
475
- classification.
476
- """
477
- demo = DEMOS.get(article)
478
- if demo is None:
479
- raise HTTPException(status_code=404, detail=f"No demo available for '{article}'")
480
- if not os.path.exists(demo["csv"]):
481
- raise HTTPException(status_code=500, detail=f"Demo file missing: {demo['csv']}")
482
-
483
- with _demo_cache_lock:
484
- cached = _demo_cache.get(article)
485
- if cached is not None:
486
- return cached
487
-
488
- payload = _build_demo_payload(demo)
489
- with _demo_cache_lock:
490
- _demo_cache[article] = payload
491
- return payload
492
-
493
-
494
- @app.get("/demo/download")
495
- async def download_demo_csv(article: str) -> FileResponse:
496
- """Serve the bundled demo CSV so users can save and re-load it later."""
497
- demo = DEMOS.get(article)
498
- if demo is None:
499
- raise HTTPException(status_code=404, detail=f"No demo available for '{article}'")
500
- if not os.path.exists(demo["csv"]):
501
- raise HTTPException(status_code=500, detail=f"Demo file missing: {demo['csv']}")
502
- return FileResponse(
503
- demo["csv"],
504
- media_type="text/csv",
505
- filename=f"{demo['title']}_classified_revisions.csv",
506
- )
507
-
508
-
509
- # ---------------------------------------------------------------------------
510
- # Summarize endpoint: small OpenAI call over the Explanation column for the
511
- # rows behind a clicked chart cell or line-chart point.
512
- # ---------------------------------------------------------------------------
513
-
514
- class SummarizePayload(BaseModel):
515
- article: str
516
- label: int
517
- period: str # one of "Y" | "2Q" | "Q" — matches the frontend Period type
518
- period_key: str # e.g. "2018", "2018Q3", "2018H1"
519
- section_ids: Optional[List[int]] = None # grouped_idx values; None = all sections
520
- api_key: str
521
- model: Optional[str] = "gpt-4o-mini"
522
- # When the dataset lives only in the user's browser (CSV they uploaded),
523
- # the frontend filters rows itself and sends just the matching explanation
524
- # snippets here. If provided, the server skips its own dataset lookup.
525
- explanations: Optional[List[str]] = None
526
-
527
-
528
- def _period_key(timestamp: pd.Timestamp, period: str) -> str:
529
- """Mirror of the frontend's periodKey() in taxonomy.ts."""
530
- if pd.isna(timestamp):
531
- return ""
532
- if not isinstance(timestamp, pd.Timestamp):
533
- timestamp = pd.Timestamp(timestamp)
534
- if timestamp.tzinfo is None:
535
- timestamp = timestamp.tz_localize("UTC")
536
- else:
537
- timestamp = timestamp.tz_convert("UTC")
538
- y = timestamp.year
539
- m = timestamp.month - 1
540
- if period == "Y":
541
- return str(y)
542
- q = m // 3
543
- if period == "Q":
544
- return f"{y}Q{q + 1}"
545
- # half-year ("2Q")
546
- return f"{y}H1" if q < 2 else f"{y}H2"
547
-
548
-
549
- def _parse_labels_field(raw: Any) -> List[int]:
550
- if raw is None or (isinstance(raw, float) and pd.isna(raw)) or raw == "":
551
- return []
552
- if isinstance(raw, list):
553
- return [int(x) for x in raw if str(x).strip().lstrip("-").isdigit()]
554
- s = str(raw).strip()
555
- if not s:
556
- return []
557
- try:
558
- import ast as _ast
559
- parsed = _ast.literal_eval(s.replace("'", '"'))
560
- if isinstance(parsed, list):
561
- return [int(x) for x in parsed if str(x).strip().lstrip("-").isdigit()]
562
- except Exception:
563
- pass
564
- inner = s.strip("[]")
565
- out: List[int] = []
566
- for tok in inner.split(","):
567
- tok = tok.strip()
568
- if tok and tok.lstrip("-").isdigit():
569
- out.append(int(tok))
570
- return out
571
-
572
-
573
- def _explanation_for_label(raw: Any, label: int) -> Optional[str]:
574
- """Explanation column stores a dict-like JSON string keyed by label."""
575
- if raw is None or (isinstance(raw, float) and pd.isna(raw)) or raw == "":
576
- return None
577
- s = str(raw).strip()
578
- if not s:
579
- return None
580
- try:
581
- import ast as _ast
582
- parsed = _ast.literal_eval(s)
583
- if isinstance(parsed, dict):
584
- # Keys may be int or str depending on how it was serialized.
585
- for k in (label, str(label)):
586
- if k in parsed:
587
- return str(parsed[k])
588
- except Exception:
589
- pass
590
- return None
591
-
592
-
593
- # Labels considered "scientifically significant" for the evolution summary.
594
- EVOLUTION_LABELS = {1, 6}
595
- # Years with fewer than this many matching edits are dropped from the prompt.
596
- EVOLUTION_MIN_PER_YEAR = 5
597
- # Cap explanations sent per year to keep the prompt bounded on huge corpora.
598
- EVOLUTION_MAX_PER_YEAR = 60
599
-
600
-
601
- def generate_evolution_summary(
602
- df: "pd.DataFrame", api_key: str, model: str = "gpt-4o-mini"
603
- ) -> Optional[str]:
604
- """
605
- Build a prose paragraph describing scientifically significant edits across
606
- years, grouped by year. Uses rows whose Labels include 1 (New Scientific
607
- Information) or 6 (Change in Scientific Narrative). Drops sparse years
608
- (< EVOLUTION_MIN_PER_YEAR matches) and caps very dense years.
609
-
610
- Returns None if there's nothing to summarize, or raises on OpenAI errors.
611
- """
612
- if "Labels" not in df.columns or "Timestamp" not in df.columns:
613
- return None
614
- if "Explanation" not in df.columns:
615
- return None
616
-
617
- # Pre-parse once.
618
- timestamps = pd.to_datetime(df["Timestamp"], utc=True, errors="coerce")
619
- parsed_labels = df["Labels"].apply(_parse_labels_field)
620
-
621
- # Group explanations by year, only for rows touching label 1 or 6.
622
- by_year: Dict[int, List[str]] = {}
623
- for idx, ts in timestamps.items():
624
- if pd.isna(ts):
625
- continue
626
- labels = parsed_labels.iloc[idx] if hasattr(parsed_labels, "iloc") else parsed_labels[idx]
627
- relevant = EVOLUTION_LABELS.intersection(labels)
628
- if not relevant:
629
- continue
630
- raw = df.at[idx, "Explanation"]
631
- snippets: List[str] = []
632
- for label in relevant:
633
- snippet = _explanation_for_label(raw, label)
634
- if snippet:
635
- snippets.append(f"(label {label}) {snippet}")
636
- if not snippets:
637
- continue
638
- year = int(ts.year)
639
- by_year.setdefault(year, []).extend(snippets)
640
-
641
- if not by_year:
642
- return None
643
-
644
- # Drop sparse years and cap dense ones.
645
- kept_years = sorted(y for y, items in by_year.items() if len(items) >= EVOLUTION_MIN_PER_YEAR)
646
- if not kept_years:
647
- return None
648
-
649
- # Build a year-headered prompt.
650
- lines: List[str] = []
651
- total_used = 0
652
- for year in kept_years:
653
- items = by_year[year][:EVOLUTION_MAX_PER_YEAR]
654
- total_used += len(items)
655
- lines.append(f"\n## {year} ({len(by_year[year])} matching edits)")
656
- for item in items:
657
- lines.append(f"- {item}")
658
- grouped_text = "\n".join(lines)
659
-
660
- user_prompt = (
661
- "You are summarizing the scientific evolution of a Wikipedia article "
662
- "based on per-revision edit explanations. The explanations below are "
663
- "grouped by year, drawn only from edits classified as introducing "
664
- "new scientific information (label 1) or changing the scientific "
665
- "narrative (label 6).\n\n"
666
- "Write ONE flowing paragraph (about 8–12 sentences) that traces "
667
- "how the article's scientific content evolved across years. "
668
- "Highlight the most dominant scientific changes per year, weaving in "
669
- "concrete topics (e.g., specific discoveries, mechanisms, applications, "
670
- "or controversies) by year (e.g., \"In 2017, findings about X were added\"). "
671
- "Mention years explicitly. Do not list every edit — synthesize the "
672
- "patterns. Do not invent details that aren't in the explanations. Do "
673
- "not use bullet points or section headers; output plain prose only.\n\n"
674
- f"Explanations grouped by year ({total_used} total snippets):\n"
675
- f"{grouped_text}"
676
- )
677
-
678
- import openai
679
- client = openai.OpenAI(api_key=api_key)
680
- completion = client.chat.completions.create(
681
- model=model or "gpt-4o-mini",
682
- messages=[
683
- {
684
- "role": "system",
685
- "content": (
686
- "You are a careful research assistant that writes compact, "
687
- "evidence-grounded prose summaries of how Wikipedia articles "
688
- "evolved scientifically over time."
689
- ),
690
- },
691
- {"role": "user", "content": user_prompt},
692
- ],
693
- )
694
- text = completion.choices[0].message.content or ""
695
- return text.strip() or None
696
-
697
-
698
- @app.post("/summarize")
699
- async def summarize_edits(payload: SummarizePayload) -> Dict[str, Any]:
700
- """
701
- Summarize the Explanation text for all rows that share a (label, period,
702
- section-group) cell using a fast OpenAI model. Used by interactive chart
703
- drill-downs.
704
- """
705
- # Client-supplied path: the dataset is in the user's browser, not cached
706
- # on the server. The frontend has already filtered rows by (label, period,
707
- # sections) and sent the relevant explanation snippets. Skip the server
708
- # dataset lookup entirely.
709
- client_snippets: Optional[List[str]] = None
710
- matched_count: Optional[int] = None
711
- if payload.explanations is not None:
712
- client_snippets = [s for s in payload.explanations if s and s.strip()]
713
- matched_count = len(payload.explanations)
714
- if not client_snippets:
715
- return {
716
- "summary": None,
717
- "edit_count": matched_count,
718
- "explanation_count": 0,
719
- "message": "Matched edits had no explanation text to summarize.",
720
- }
721
-
722
- df = None
723
- if client_snippets is None:
724
- df = _get_dataset(payload.article)
725
- if df is None:
726
- # Lazy-load the demo dataset if its cache got cleared (e.g. container restart).
727
- demo = DEMOS.get(payload.article)
728
- if demo is not None and os.path.exists(demo["csv"]):
729
- _build_demo_payload(demo)
730
- df = _get_dataset(payload.article)
731
- if df is None:
732
- raise HTTPException(
733
- status_code=404,
734
- detail=f"No dataset loaded for '{payload.article}'.",
735
- )
736
- if "Explanation" not in df.columns:
737
- raise HTTPException(
738
- status_code=400,
739
- detail="This dataset has no Explanation column to summarize.",
740
- )
741
-
742
- if client_snippets is not None:
743
- snippets = client_snippets
744
- # We don't know the unmatched count on the client path; use the count
745
- # the client reported.
746
- match_count_for_response = matched_count or len(snippets)
747
- else:
748
- # Filter to rows that contain the requested label.
749
- mask = df["Labels"].apply(lambda v: payload.label in _parse_labels_field(v))
750
-
751
- # Filter by period bucket.
752
- timestamps = pd.to_datetime(df["Timestamp"], utc=True, errors="coerce")
753
- period_mask = timestamps.apply(lambda t: _period_key(t, payload.period) == payload.period_key)
754
- mask = mask & period_mask
755
-
756
- # Filter by section group(s) if provided.
757
- if payload.section_ids is not None and "grouped_idx" in df.columns:
758
- section_set = set(int(s) for s in payload.section_ids)
759
- mask = mask & df["grouped_idx"].apply(
760
- lambda v: int(v) in section_set if pd.notna(v) else False
761
- )
762
-
763
- matched = df.loc[mask]
764
- if matched.empty:
765
- return {
766
- "summary": None,
767
- "edit_count": 0,
768
- "message": "No edits found for this cell.",
769
- }
770
-
771
- # Pull the relevant Explanation snippet for each matched row.
772
- snippets = []
773
- for _, row in matched.iterrows():
774
- text = _explanation_for_label(row.get("Explanation"), payload.label)
775
- if text:
776
- snippets.append(text)
777
-
778
- if not snippets:
779
- return {
780
- "summary": None,
781
- "edit_count": int(len(matched)),
782
- "message": "Matched edits had no explanation text to summarize.",
783
- }
784
- match_count_for_response = int(len(matched))
785
-
786
- # Cap to keep the prompt comfortably within the model's context.
787
- MAX_SNIPPETS = 100
788
- truncated = len(snippets) > MAX_SNIPPETS
789
- snippets_for_prompt = snippets[:MAX_SNIPPETS]
790
-
791
- # Compose the prompt.
792
- label_name_map = {
793
- 1: "New Scientific Information",
794
- 2: "Scientific Information Removed",
795
- 3: "Scientific Clarification Added",
796
- 4: "Scientific Technical Terms Added",
797
- 5: "Researcher Names Added",
798
- 6: "Change in Scientific Narrative",
799
- 7: "Academic References Added",
800
- 8: "Academic References Removed",
801
- 9: "Wikilink Added",
802
- 10: "Addition or Modification of Quantitative Information",
803
- 11: "Other Scientifically Relevant Change",
804
- 12: "None of the above",
805
- }
806
- label_name = label_name_map.get(payload.label, f"Label {payload.label}")
807
-
808
- section_note = (
809
- "across all sections"
810
- if not payload.section_ids
811
- else f"across {len(payload.section_ids)} section group(s)"
812
- )
813
-
814
- user_prompt = (
815
- f"You are summarizing Wikipedia revision edits for the article "
816
- f"'{payload.article}'.\n\n"
817
- f"Below are explanations from {len(snippets_for_prompt)} edits "
818
- f"classified as '{label_name}' (label {payload.label}) during "
819
- f"period {payload.period_key}, {section_note}.\n\n"
820
- f"Write a concise (4–6 sentences) summary describing what kinds of "
821
- f"changes happened, common themes, and notable additions or removals. "
822
- f"Be specific and cite concrete examples where possible. Do not "
823
- f"enumerate every edit; synthesize patterns.\n\n"
824
- f"Explanations:\n"
825
- + "\n\n".join(f"- {s}" for s in snippets_for_prompt)
826
- )
827
-
828
- if truncated:
829
- user_prompt += (
830
- f"\n\n(Note: {len(snippets) - MAX_SNIPPETS} additional explanations "
831
- f"were omitted to fit context.)"
832
- )
833
-
834
- try:
835
- import openai
836
- client = openai.OpenAI(api_key=payload.api_key)
837
- completion = client.chat.completions.create(
838
- model=payload.model or "gpt-4o-mini",
839
- messages=[
840
- {
841
- "role": "system",
842
- "content": (
843
- "You are a careful research assistant that summarizes "
844
- "lists of Wikipedia edit explanations into compact, "
845
- "high-signal prose."
846
- ),
847
- },
848
- {"role": "user", "content": user_prompt},
849
- ],
850
- )
851
- summary_text = completion.choices[0].message.content or ""
852
- except Exception as e:
853
- raise HTTPException(status_code=502, detail=f"OpenAI request failed: {e}")
854
-
855
- return {
856
- "summary": summary_text.strip(),
857
- "edit_count": match_count_for_response,
858
- "explanation_count": len(snippets),
859
- "truncated": truncated,
860
- "label": payload.label,
861
- "label_name": label_name,
862
- "period_key": payload.period_key,
863
- }
864
-
865
-
866
  @app.get("/progress")
867
  async def get_progress(article: str) -> Dict[str, Any]:
868
  """
@@ -882,31 +365,3 @@ async def get_progress(article: str) -> Dict[str, Any]:
882
  "batch": article_progress.get("batch"),
883
  "error": article_progress.get("error"),
884
  }
885
-
886
- # Serve the built React app at "/".
887
- # This must be registered AFTER all API routes so /prepare, /classify, /progress, /results,
888
- # /files still take precedence.
889
- from fastapi.responses import FileResponse # noqa: E402
890
-
891
- FRONTEND_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend_build")
892
-
893
- if os.path.isdir(FRONTEND_DIR):
894
- # CRA's compiled JS/CSS lives under build/static/...
895
- app.mount(
896
- "/static",
897
- StaticFiles(directory=os.path.join(FRONTEND_DIR, "static")),
898
- name="frontend-assets",
899
- )
900
-
901
- @app.get("/")
902
- async def serve_index() -> FileResponse:
903
- return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))
904
-
905
- @app.get("/{full_path:path}")
906
- async def spa_fallback(full_path: str) -> FileResponse:
907
- # Try a real file under frontend_build first (favicon.ico, manifest.json, etc.).
908
- candidate = os.path.join(FRONTEND_DIR, full_path)
909
- if os.path.isfile(candidate):
910
- return FileResponse(candidate)
911
- # Otherwise fall back to index.html (no client-side router today, but harmless).
912
- return FileResponse(os.path.join(FRONTEND_DIR, "index.html"))
 
2
  import os
3
  import threading
4
  import time
5
+ from typing import Any, Dict, Optional
6
 
7
  import pandas as pd
8
  from fastapi import BackgroundTasks, FastAPI, HTTPException, Request
9
  from fastapi.middleware.cors import CORSMiddleware
 
 
10
  from fastapi.staticfiles import StaticFiles
11
  from pydantic import BaseModel
12
 
 
28
  # Initialize FastAPI app
29
  app = FastAPI(title="Wiki Revision Classifier API")
30
 
 
 
 
 
31
  # Enable CORS
32
  app.add_middleware(
33
  CORSMiddleware,
 
46
  response.headers["Expires"] = "0"
47
  return response
48
 
49
+ # Prepare static directory
 
50
  os.makedirs("visualizations", exist_ok=True)
51
  app.mount(
52
+ "/static",
53
  StaticFiles(directory="visualizations"),
54
+ name="static"
55
  )
56
 
57
  # Pydantic models
 
108
 
109
  return {
110
  "previewRows": preview,
111
+ "csvUrl": f"/static/{csv_filename}",
112
  "diffs": diffs,
113
  "revisionCount": revision_count,
114
  }
 
119
  Start classification in the background and return immediately.
120
  Uses the flexible-linker pipeline + OpenAI Batch API (single batch).
121
  """
122
+ os.environ["OPENAI_API_KEY"] = payload.api_key
123
  model_name = payload.model or "gpt-5-mini"
124
 
125
  df, title = fetch_revision_history(payload.article)
 
158
  background_tasks.add_task(
159
  process_classification,
160
  model_name=model_name,
 
161
  prompts=prompts,
162
  df=prompted_df,
163
  title=title,
 
216
  return pd.Series(out, index=results_df.index, dtype=int)
217
 
218
 
219
+ def process_classification(model_name, prompts, df, title, taxonomy):
220
  """Run the OpenAI batch classification in the background."""
221
  safe_title = title.replace(" ", "_")
222
  total = len(prompts)
 
239
  with progress_lock:
240
  progress_map.setdefault(safe_title, {})["stage"] = "submitting"
241
 
242
+ model = GPT(model_name=model_name)
243
  raw_responses = model.predict(prompts, status_callback=_update_batch_status)
244
 
245
  with progress_lock:
 
275
  result_path = os.path.join("visualizations", result_filename)
276
  results_df.to_csv(result_path, index=False)
277
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  print("Creating visualizations...")
279
+ taxonomy[11] = {
280
  "category": "None of the above",
281
  "description": "No meaningful scientific changes were detected between the revisions; applies to non-scientific, formatting, or trivial edits."
282
  }
 
330
 
331
  # Load results
332
  results_df = pd.read_csv(result_path)
 
333
  preview_rows = results_df.head(5).to_dict(orient="records")
334
 
335
  # URL paths for frontend
336
+ csv_url = f"/static/{result_filename}"
337
+ distribution_url = f"/static/{dist_filename}"
338
+ cooccurrence_url = f"/static/{cooc_filename}"
 
 
 
 
 
 
 
 
 
 
 
339
 
340
  return {
341
  "status": "complete",
 
344
  "results": results_df.to_dict(orient="records"),
345
  "distributionUrl": distribution_url,
346
  "cooccurrenceUrl": cooccurrence_url,
 
347
  }
348
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  @app.get("/progress")
350
  async def get_progress(article: str) -> Dict[str, Any]:
351
  """
 
365
  "batch": article_progress.get("batch"),
366
  "error": article_progress.get("error"),
367
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend_build/asset-manifest.json CHANGED
@@ -1,12 +1,12 @@
1
  {
2
  "files": {
3
- "main.css": "/static/css/main.78d0a2b0.css",
4
- "main.js": "/static/js/main.ec549950.js",
5
  "static/js/206.b8423ec3.chunk.js": "/static/js/206.b8423ec3.chunk.js",
6
  "index.html": "/index.html"
7
  },
8
  "entrypoints": [
9
- "static/css/main.78d0a2b0.css",
10
- "static/js/main.ec549950.js"
11
  ]
12
  }
 
1
  {
2
  "files": {
3
+ "main.css": "/static/css/main.fb799031.css",
4
+ "main.js": "/static/js/main.0f49f162.js",
5
  "static/js/206.b8423ec3.chunk.js": "/static/js/206.b8423ec3.chunk.js",
6
  "index.html": "/index.html"
7
  },
8
  "entrypoints": [
9
+ "static/css/main.fb799031.css",
10
+ "static/js/main.0f49f162.js"
11
  ]
12
  }
frontend_build/index.html CHANGED
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.ec549950.js"></script><link href="/static/css/main.78d0a2b0.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
 
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>React App</title><script defer="defer" src="/static/js/main.0f49f162.js"></script><link href="/static/css/main.fb799031.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
frontend_build/static/css/{main.78d0a2b0.css → main.fb799031.css} RENAMED
@@ -1,3 +1,3 @@
1
  *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*
2
  ! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com
3
- */*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;font-variation-settings:normal;line-height:1.5;tab-size:4}body{line-height:inherit}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}html{font-feature-settings:"cv11","ss01";-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1));color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}h1,h2,h3{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.card{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-shadow:0 1px 2px 0 #0f172a0a,0 1px 3px 0 #0f172a0f;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 1px 3px 0 var(--tw-shadow-color);background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.75rem;border-width:1px}.btn-primary,.card{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-primary{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);align-items:center;background-color:#0891b2;background-color:rgb(8 145 178/var(--tw-bg-opacity,1));border-radius:.5rem;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity,1));display:inline-flex;font-weight:500;gap:.5rem;justify-content:center;padding:.625rem 1.25rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-primary:hover{--tw-bg-opacity:1;background-color:#0e7490;background-color:rgb(14 116 144/var(--tw-bg-opacity,1))}.btn-primary:disabled{--tw-bg-opacity:1;background-color:#94a3b8;background-color:rgb(148 163 184/var(--tw-bg-opacity,1));cursor:not-allowed}.btn-secondary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.5rem;border-width:1px;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));display:inline-flex;font-weight:500;gap:.5rem;justify-content:center;padding:.5rem 1rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-secondary:hover{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.input-base{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.5rem;border-width:1px;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));padding:.5rem .75rem;width:100%}.input-base::placeholder{--tw-placeholder-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-placeholder-opacity,1))}.input-base:focus{--tw-border-opacity:1;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:#0891b233;border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid #0000;outline-offset:2px}.input-base:disabled{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1));color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1))}.label{display:block;margin-bottom:.25rem}.label,.tab{--tw-text-opacity:1;color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1));font-size:.875rem;font-weight:500;line-height:1.25rem}.tab{border-bottom-width:2px;border-color:#0000;cursor:pointer;padding:.5rem 1rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.tab:hover{border-color:#94a3b8;border-color:rgb(148 163 184/var(--tw-border-opacity,1));color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.tab-active,.tab:hover{--tw-border-opacity:1;--tw-text-opacity:1}.tab-active{border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1));color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.sticky{position:-webkit-sticky;position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.\!mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-5{margin-left:1.25rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-72{max-height:18rem}.min-h-\[22px\]{min-height:22px}.min-h-screen{min-height:100vh}.\!w-auto{width:auto!important}.w-1\/2{width:50%}.w-3{width:.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-\[180px\]{min-width:180px}.min-w-\[78px\]{min-width:78px}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-\[260px\]{max-width:260px}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1}.flex-shrink-0,.shrink-0{flex-shrink:0}.border-separate{border-collapse:initial}.border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.rotate-90{--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-3{column-gap:.75rem}.gap-x-4{column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-200{--tw-border-opacity:1;border-color:#fde68a;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-amber-400{--tw-border-opacity:1;border-color:#fbbf24;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-brand-200{--tw-border-opacity:1;border-color:#a5f3fc;border-color:rgb(165 243 252/var(--tw-border-opacity,1))}.border-brand-300{--tw-border-opacity:1;border-color:#67e8f9;border-color:rgb(103 232 249/var(--tw-border-opacity,1))}.border-brand-500{--tw-border-opacity:1;border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1))}.border-emerald-400{--tw-border-opacity:1;border-color:#34d399;border-color:rgb(52 211 153/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:#fecaca;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-sky-300{--tw-border-opacity:1;border-color:#7dd3fc;border-color:rgb(125 211 252/var(--tw-border-opacity,1))}.border-slate-200{--tw-border-opacity:1;border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-300{--tw-border-opacity:1;border-color:#cbd5e1;border-color:rgb(203 213 225/var(--tw-border-opacity,1))}.border-surface-border{--tw-border-opacity:1;border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:#fffbeb;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:#3b82f6;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-600{--tw-bg-opacity:1;background-color:#2563eb;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-brand-50{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.bg-brand-50\/40{background-color:#ecfeff66}.bg-brand-500{--tw-bg-opacity:1;background-color:#0891b2;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-emerald-100{--tw-bg-opacity:1;background-color:#d1fae5;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:#f3f4f6;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:#e5e7eb;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-ink\/30{background-color:#0f172a4d}.bg-red-50{--tw-bg-opacity:1;background-color:#fef2f2;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-sky-50{--tw-bg-opacity:1;background-color:#f0f9ff;background-color:rgb(240 249 255/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-surface-border{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.bg-surface-subtle{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-surface-subtle\/60{background-color:#f8fafc99}.bg-transparent{background-color:initial}.bg-white{--tw-bg-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-brand-500{--tw-gradient-from:#0891b2 var(--tw-gradient-from-position);--tw-gradient-to:#0891b200 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-accent{--tw-gradient-to:#4338ca var(--tw-gradient-to-position)}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.\!px-2{padding-left:.5rem!important;padding-right:.5rem!important}.\!px-3{padding-left:.75rem!important;padding-right:.75rem!important}.\!py-1{padding-bottom:.25rem!important;padding-top:.25rem!important}.\!py-1\.5{padding-bottom:.375rem!important;padding-top:.375rem!important}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-1{padding-bottom:.25rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pr-1{padding-right:.25rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.\!text-xs{font-size:.75rem!important;line-height:1rem!important}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.text-amber-700{--tw-text-opacity:1;color:#b45309;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:#78350f;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:#000;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:#2563eb;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-brand-500{--tw-text-opacity:1;color:#0891b2;color:rgb(8 145 178/var(--tw-text-opacity,1))}.text-brand-600{--tw-text-opacity:1;color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.text-brand-700{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.text-emerald-900{--tw-text-opacity:1;color:#064e3b;color:rgb(6 78 59/var(--tw-text-opacity,1))}.text-ink{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-ink-muted{--tw-text-opacity:1;color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-ink-soft{--tw-text-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:#dc2626;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:#b91c1c;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:#991b1b;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-sky-800{--tw-text-opacity:1;color:#075985;color:rgb(7 89 133/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:#cbd5e1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.line-through{-webkit-text-decoration-line:line-through;text-decoration-line:line-through}.opacity-70{opacity:.7}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-\[width\]{transition-duration:.15s;transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.file\:mr-3::-webkit-file-upload-button{margin-right:.75rem}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:cursor-pointer::-webkit-file-upload-button{cursor:pointer}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-md::-webkit-file-upload-button{border-radius:.375rem}.file\:rounded-md::file-selector-button{border-radius:.375rem}.file\:border-0::-webkit-file-upload-button{border-width:0}.file\:border-0::file-selector-button{border-width:0}.file\:bg-brand-50::-webkit-file-upload-button{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.file\:bg-brand-50::file-selector-button{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.file\:bg-surface-subtle::-webkit-file-upload-button{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.file\:bg-surface-subtle::file-selector-button{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.file\:px-3::-webkit-file-upload-button{padding-left:.75rem;padding-right:.75rem}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::-webkit-file-upload-button{padding-bottom:.375rem;padding-top:.375rem}.file\:py-1\.5::file-selector-button{padding-bottom:.375rem;padding-top:.375rem}.file\:text-brand-700::-webkit-file-upload-button{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.file\:text-brand-700::file-selector-button{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.file\:text-ink::-webkit-file-upload-button{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.file\:text-ink::file-selector-button{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.last\:border-0:last-child{border-width:0}.hover\:border-ink-muted:hover{--tw-border-opacity:1;border-color:#475569;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:#1d4ed8;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-surface-subtle:hover{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.hover\:bg-surface-subtle\/40:hover{background-color:#f8fafc66}.hover\:text-brand-600:hover{--tw-text-opacity:1;color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.hover\:text-brand-700:hover{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.hover\:text-ink:hover{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.hover\:text-red-900:hover{--tw-text-opacity:1;color:#7f1d1d;color:rgb(127 29 29/var(--tw-text-opacity,1))}.hover\:file\:bg-brand-100::-webkit-file-upload-button:hover{--tw-bg-opacity:1;background-color:#cffafe;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.hover\:file\:bg-brand-100::file-selector-button:hover{--tw-bg-opacity:1;background-color:#cffafe;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.hover\:file\:bg-surface-border::-webkit-file-upload-button:hover{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.hover\:file\:bg-surface-border::file-selector-button:hover{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.focus\:ring-brand-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(8 145 178/var(--tw-ring-opacity,1))}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-\[420px\]{width:420px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}
 
1
  *,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }/*
2
  ! tailwindcss v3.4.19 | MIT License | https://tailwindcss.com
3
+ */*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}:host,html{-webkit-text-size-adjust:100%;font-feature-settings:normal;-webkit-tap-highlight-color:transparent;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;font-variation-settings:normal;line-height:1.5;tab-size:4}body{line-height:inherit}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-feature-settings:normal;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em;font-variation-settings:normal}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{font-feature-settings:inherit;color:inherit;font-family:inherit;font-size:100%;font-variation-settings:inherit;font-weight:inherit;letter-spacing:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]:where(:not([hidden=until-found])){display:none}html{font-feature-settings:"cv11","ss01";-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1));color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}h1,h2,h3{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.card{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-shadow:0 1px 2px 0 #0f172a0a,0 1px 3px 0 #0f172a0f;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color),0 1px 3px 0 var(--tw-shadow-color);background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.75rem;border-width:1px}.btn-primary,.card{box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.btn-primary{--tw-bg-opacity:1;--tw-text-opacity:1;--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);align-items:center;background-color:#0891b2;background-color:rgb(8 145 178/var(--tw-bg-opacity,1));border-radius:.5rem;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity,1));display:inline-flex;font-weight:500;gap:.5rem;justify-content:center;padding:.625rem 1.25rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-primary:hover{--tw-bg-opacity:1;background-color:#0e7490;background-color:rgb(14 116 144/var(--tw-bg-opacity,1))}.btn-primary:disabled{--tw-bg-opacity:1;background-color:#94a3b8;background-color:rgb(148 163 184/var(--tw-bg-opacity,1));cursor:not-allowed}.btn-secondary{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;align-items:center;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.5rem;border-width:1px;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));display:inline-flex;font-weight:500;gap:.5rem;justify-content:center;padding:.5rem 1rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.btn-secondary:hover{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.input-base{--tw-border-opacity:1;--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1));border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1));border-radius:.5rem;border-width:1px;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1));padding:.5rem .75rem;width:100%}.input-base::placeholder{--tw-placeholder-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-placeholder-opacity,1))}.input-base:focus{--tw-border-opacity:1;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);--tw-ring-color:#0891b233;border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1));box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);outline:2px solid #0000;outline-offset:2px}.input-base:disabled{--tw-bg-opacity:1;--tw-text-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1));color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1))}.label{display:block;margin-bottom:.25rem}.label,.tab{--tw-text-opacity:1;color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1));font-size:.875rem;font-weight:500;line-height:1.25rem}.tab{border-bottom-width:2px;border-color:#0000;cursor:pointer;padding:.5rem 1rem;transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.tab:hover{border-color:#94a3b8;border-color:rgb(148 163 184/var(--tw-border-opacity,1));color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.tab-active,.tab:hover{--tw-border-opacity:1;--tw-text-opacity:1}.tab-active{border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1));color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.visible{visibility:visible}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.left-0{left:0}.right-0{right:0}.top-0{top:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.mx-1{margin-left:.25rem;margin-right:.25rem}.mx-auto{margin-left:auto;margin-right:auto}.\!mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-5{margin-left:1.25rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-6{margin-top:1.5rem}.line-clamp-5{-webkit-box-orient:vertical;-webkit-line-clamp:5;display:-webkit-box;overflow:hidden}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1{height:.25rem}.h-1\.5{height:.375rem}.h-2{height:.5rem}.h-2\.5{height:.625rem}.h-3{height:.75rem}.h-4{height:1rem}.h-6{height:1.5rem}.h-9{height:2.25rem}.h-full{height:100%}.max-h-48{max-height:12rem}.max-h-72{max-height:18rem}.min-h-\[22px\]{min-height:22px}.min-h-screen{min-height:100vh}.\!w-auto{width:auto!important}.w-1\/2{width:50%}.w-2{width:.5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-6{width:1.5rem}.w-9{width:2.25rem}.w-full{width:100%}.min-w-\[180px\]{min-width:180px}.min-w-\[78px\]{min-width:78px}.min-w-full{min-width:100%}.max-w-3xl{max-width:48rem}.max-w-5xl{max-width:64rem}.max-w-\[200px\]{max-width:200px}.max-w-\[260px\]{max-width:260px}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.flex-1{flex:1 1}.flex-shrink-0,.shrink-0{flex-shrink:0}.border-separate{border-collapse:initial}.border-spacing-0{--tw-border-spacing-x:0px;--tw-border-spacing-y:0px;border-spacing:var(--tw-border-spacing-x) var(--tw-border-spacing-y)}.rotate-90{--tw-rotate:90deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-pointer{cursor:pointer}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-1\.5{gap:.375rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-x-3{column-gap:.75rem}.gap-x-4{column-gap:1rem}.gap-y-1{row-gap:.25rem}.space-y-0\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.125rem*var(--tw-space-y-reverse));margin-top:calc(.125rem*(1 - var(--tw-space-y-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.25rem*var(--tw-space-y-reverse));margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.375rem*var(--tw-space-y-reverse));margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.5rem*var(--tw-space-y-reverse));margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(.75rem*var(--tw-space-y-reverse));margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1rem*var(--tw-space-y-reverse));margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(1.25rem*var(--tw-space-y-reverse));margin-top:calc(1.25rem*(1 - var(--tw-space-y-reverse)))}.space-y-8>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-bottom:calc(2rem*var(--tw-space-y-reverse));margin-top:calc(2rem*(1 - var(--tw-space-y-reverse)))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-sm{border-radius:.125rem}.rounded-xl{border-radius:.75rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-amber-200{--tw-border-opacity:1;border-color:#fde68a;border-color:rgb(253 230 138/var(--tw-border-opacity,1))}.border-amber-400{--tw-border-opacity:1;border-color:#fbbf24;border-color:rgb(251 191 36/var(--tw-border-opacity,1))}.border-brand-200{--tw-border-opacity:1;border-color:#a5f3fc;border-color:rgb(165 243 252/var(--tw-border-opacity,1))}.border-brand-300{--tw-border-opacity:1;border-color:#67e8f9;border-color:rgb(103 232 249/var(--tw-border-opacity,1))}.border-brand-500{--tw-border-opacity:1;border-color:#0891b2;border-color:rgb(8 145 178/var(--tw-border-opacity,1))}.border-emerald-400{--tw-border-opacity:1;border-color:#34d399;border-color:rgb(52 211 153/var(--tw-border-opacity,1))}.border-red-200{--tw-border-opacity:1;border-color:#fecaca;border-color:rgb(254 202 202/var(--tw-border-opacity,1))}.border-sky-300{--tw-border-opacity:1;border-color:#7dd3fc;border-color:rgb(125 211 252/var(--tw-border-opacity,1))}.border-slate-200{--tw-border-opacity:1;border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.border-slate-300{--tw-border-opacity:1;border-color:#cbd5e1;border-color:rgb(203 213 225/var(--tw-border-opacity,1))}.border-surface-border{--tw-border-opacity:1;border-color:#e2e8f0;border-color:rgb(226 232 240/var(--tw-border-opacity,1))}.bg-amber-50{--tw-bg-opacity:1;background-color:#fffbeb;background-color:rgb(255 251 235/var(--tw-bg-opacity,1))}.bg-blue-500{--tw-bg-opacity:1;background-color:#3b82f6;background-color:rgb(59 130 246/var(--tw-bg-opacity,1))}.bg-blue-600{--tw-bg-opacity:1;background-color:#2563eb;background-color:rgb(37 99 235/var(--tw-bg-opacity,1))}.bg-brand-50{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.bg-brand-50\/40{background-color:#ecfeff66}.bg-brand-500{--tw-bg-opacity:1;background-color:#0891b2;background-color:rgb(8 145 178/var(--tw-bg-opacity,1))}.bg-emerald-100{--tw-bg-opacity:1;background-color:#d1fae5;background-color:rgb(209 250 229/var(--tw-bg-opacity,1))}.bg-gray-100{--tw-bg-opacity:1;background-color:#f3f4f6;background-color:rgb(243 244 246/var(--tw-bg-opacity,1))}.bg-gray-200{--tw-bg-opacity:1;background-color:#e5e7eb;background-color:rgb(229 231 235/var(--tw-bg-opacity,1))}.bg-ink\/30{background-color:#0f172a4d}.bg-red-50{--tw-bg-opacity:1;background-color:#fef2f2;background-color:rgb(254 242 242/var(--tw-bg-opacity,1))}.bg-sky-50{--tw-bg-opacity:1;background-color:#f0f9ff;background-color:rgb(240 249 255/var(--tw-bg-opacity,1))}.bg-slate-50{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-surface-border{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.bg-surface-subtle{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.bg-surface-subtle\/60{background-color:#f8fafc99}.bg-transparent{background-color:initial}.bg-white{--tw-bg-opacity:1;background-color:#fff;background-color:rgb(255 255 255/var(--tw-bg-opacity,1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-brand-500{--tw-gradient-from:#0891b2 var(--tw-gradient-from-position);--tw-gradient-to:#0891b200 var(--tw-gradient-to-position);--tw-gradient-stops:var(--tw-gradient-from),var(--tw-gradient-to)}.to-accent{--tw-gradient-to:#4338ca var(--tw-gradient-to-position)}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.\!px-2{padding-left:.5rem!important;padding-right:.5rem!important}.\!px-3{padding-left:.75rem!important;padding-right:.75rem!important}.\!py-1{padding-bottom:.25rem!important;padding-top:.25rem!important}.\!py-1\.5{padding-bottom:.375rem!important;padding-top:.375rem!important}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-bottom:.125rem;padding-top:.125rem}.py-1{padding-bottom:.25rem;padding-top:.25rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-5{padding-bottom:1.25rem;padding-top:1.25rem}.py-8{padding-bottom:2rem;padding-top:2rem}.pb-1{padding-bottom:.25rem}.pb-1\.5{padding-bottom:.375rem}.pb-2{padding-bottom:.5rem}.pr-1{padding-right:.25rem}.pt-5{padding-top:1.25rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-top{vertical-align:top}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.\!text-sm{font-size:.875rem!important;line-height:1.25rem!important}.\!text-xs{font-size:.75rem!important;line-height:1rem!important}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.lowercase{text-transform:lowercase}.leading-none{line-height:1}.leading-relaxed{line-height:1.625}.leading-snug{line-height:1.375}.tracking-wide{letter-spacing:.025em}.text-amber-700{--tw-text-opacity:1;color:#b45309;color:rgb(180 83 9/var(--tw-text-opacity,1))}.text-amber-900{--tw-text-opacity:1;color:#78350f;color:rgb(120 53 15/var(--tw-text-opacity,1))}.text-black{--tw-text-opacity:1;color:#000;color:rgb(0 0 0/var(--tw-text-opacity,1))}.text-blue-600{--tw-text-opacity:1;color:#2563eb;color:rgb(37 99 235/var(--tw-text-opacity,1))}.text-brand-500{--tw-text-opacity:1;color:#0891b2;color:rgb(8 145 178/var(--tw-text-opacity,1))}.text-brand-600{--tw-text-opacity:1;color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.text-brand-700{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.text-emerald-900{--tw-text-opacity:1;color:#064e3b;color:rgb(6 78 59/var(--tw-text-opacity,1))}.text-ink{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.text-ink-muted{--tw-text-opacity:1;color:#475569;color:rgb(71 85 105/var(--tw-text-opacity,1))}.text-ink-soft{--tw-text-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-red-600{--tw-text-opacity:1;color:#dc2626;color:rgb(220 38 38/var(--tw-text-opacity,1))}.text-red-700{--tw-text-opacity:1;color:#b91c1c;color:rgb(185 28 28/var(--tw-text-opacity,1))}.text-red-800{--tw-text-opacity:1;color:#991b1b;color:rgb(153 27 27/var(--tw-text-opacity,1))}.text-sky-800{--tw-text-opacity:1;color:#075985;color:rgb(7 89 133/var(--tw-text-opacity,1))}.text-slate-300{--tw-text-opacity:1;color:#cbd5e1;color:rgb(203 213 225/var(--tw-text-opacity,1))}.text-slate-400{--tw-text-opacity:1;color:#94a3b8;color:rgb(148 163 184/var(--tw-text-opacity,1))}.text-white{--tw-text-opacity:1;color:#fff;color:rgb(255 255 255/var(--tw-text-opacity,1))}.underline{-webkit-text-decoration-line:underline;text-decoration-line:underline}.line-through{-webkit-text-decoration-line:line-through;text-decoration-line:line-through}.opacity-70{opacity:.7}.shadow-xl{--tw-shadow:0 20px 25px -5px #0000001a,0 8px 10px -6px #0000001a;--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color);box-shadow:0 0 #0000,0 0 #0000,var(--tw-shadow);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid #0000;outline-offset:2px}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-text-decoration-color,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-\[width\]{transition-duration:.15s;transition-property:width;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-duration:.15s;transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-colors{transition-duration:.15s;transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-opacity{transition-duration:.15s;transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-transform{transition-duration:.15s;transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-300{transition-duration:.3s}.file\:mr-3::-webkit-file-upload-button{margin-right:.75rem}.file\:mr-3::file-selector-button{margin-right:.75rem}.file\:cursor-pointer::-webkit-file-upload-button{cursor:pointer}.file\:cursor-pointer::file-selector-button{cursor:pointer}.file\:rounded-md::-webkit-file-upload-button{border-radius:.375rem}.file\:rounded-md::file-selector-button{border-radius:.375rem}.file\:border-0::-webkit-file-upload-button{border-width:0}.file\:border-0::file-selector-button{border-width:0}.file\:bg-brand-50::-webkit-file-upload-button{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.file\:bg-brand-50::file-selector-button{--tw-bg-opacity:1;background-color:#ecfeff;background-color:rgb(236 254 255/var(--tw-bg-opacity,1))}.file\:bg-surface-subtle::-webkit-file-upload-button{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.file\:bg-surface-subtle::file-selector-button{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.file\:px-3::-webkit-file-upload-button{padding-left:.75rem;padding-right:.75rem}.file\:px-3::file-selector-button{padding-left:.75rem;padding-right:.75rem}.file\:py-1\.5::-webkit-file-upload-button{padding-bottom:.375rem;padding-top:.375rem}.file\:py-1\.5::file-selector-button{padding-bottom:.375rem;padding-top:.375rem}.file\:text-brand-700::-webkit-file-upload-button{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.file\:text-brand-700::file-selector-button{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.file\:text-ink::-webkit-file-upload-button{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.file\:text-ink::file-selector-button{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.last\:border-0:last-child{border-width:0}.hover\:border-ink-muted:hover{--tw-border-opacity:1;border-color:#475569;border-color:rgb(71 85 105/var(--tw-border-opacity,1))}.hover\:bg-blue-700:hover{--tw-bg-opacity:1;background-color:#1d4ed8;background-color:rgb(29 78 216/var(--tw-bg-opacity,1))}.hover\:bg-surface-subtle:hover{--tw-bg-opacity:1;background-color:#f8fafc;background-color:rgb(248 250 252/var(--tw-bg-opacity,1))}.hover\:bg-surface-subtle\/40:hover{background-color:#f8fafc66}.hover\:text-brand-600:hover{--tw-text-opacity:1;color:#0e7490;color:rgb(14 116 144/var(--tw-text-opacity,1))}.hover\:text-brand-700:hover{--tw-text-opacity:1;color:#155e75;color:rgb(21 94 117/var(--tw-text-opacity,1))}.hover\:text-ink:hover{--tw-text-opacity:1;color:#0f172a;color:rgb(15 23 42/var(--tw-text-opacity,1))}.hover\:text-red-900:hover{--tw-text-opacity:1;color:#7f1d1d;color:rgb(127 29 29/var(--tw-text-opacity,1))}.hover\:file\:bg-brand-100::-webkit-file-upload-button:hover{--tw-bg-opacity:1;background-color:#cffafe;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.hover\:file\:bg-brand-100::file-selector-button:hover{--tw-bg-opacity:1;background-color:#cffafe;background-color:rgb(207 250 254/var(--tw-bg-opacity,1))}.hover\:file\:bg-surface-border::-webkit-file-upload-button:hover{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.hover\:file\:bg-surface-border::file-selector-button:hover{--tw-bg-opacity:1;background-color:#e2e8f0;background-color:rgb(226 232 240/var(--tw-bg-opacity,1))}.focus\:ring-2:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),0 0 #0000;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(59 130 246/var(--tw-ring-opacity,1))}.focus\:ring-brand-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(8 145 178/var(--tw-ring-opacity,1))}@media (min-width:640px){.sm\:col-span-2{grid-column:span 2/span 2}.sm\:w-\[420px\]{width:420px}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:text-xl{font-size:1.25rem;line-height:1.75rem}}@media (min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}
frontend_build/static/js/{main.ec549950.js → main.0f49f162.js} RENAMED
The diff for this file is too large to render. See raw diff
 
frontend_build/static/js/{main.ec549950.js.LICENSE.txt → main.0f49f162.js.LICENSE.txt} RENAMED
File without changes