devaanand Claude Opus 5 (1M context) commited on
Commit
bb4c28d
·
1 Parent(s): a7f2409

fix(ui): one compact corpus row instead of two large cards

Browse files

Three problems with the first cut, all fair.

CAMEL appeared twice, as two "corpora". It is one dataset in two sizes,
so the sizes are now options in a single dropdown and it reads as one
dataset.

It was badged DEFAULT, which overstates it. The PI asked for CAMEL to be
"available and ready-to-use"; that is a starting point, not a claim that
this is THE corpus. The field is renamed default -> preselected, it
decides ordering and what loads first, and nothing is badged.

The two expanded cards took most of Step 1 and pushed the upload control
below the fold, which is the wrong hierarchy: most researchers bring
their own data. Step 1 is now one labelled row - dropdown, info button,
action - and the provenance (description, text column, preprocessing,
citation, dataset link) moved into a modal behind the info button, where
it is one click away without occupying the step.

Also: the listing now knows this instance's row ceiling. The full corpus
is 57,174 rows against a deployed CCR_MAX_ROWS of 50,000, so picking it
failed at ingest AFTER the click. It is now shown as unavailable with
the reason, and becomes usable the moment the deployment raises the
limit - no code change, and the test pins both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

CHANGELOG.md CHANGED
@@ -33,9 +33,11 @@ Format: [Keep a Changelog](https://keepachangelog.com/). User-visible changes on
33
  travels with your results. The full 57,174-text corpus, annotated for 25
34
  cultural and moral constructs, is linked on Hugging Face; the platform ships a
35
  sample because it runs in seconds. The complete 57,174-text corpus, with the
36
- annotations for all 25 constructs, is offered alongside it for real analyses.
37
- Each corpus has an "About this dataset" panel showing the text column,
38
- preprocessing and citation. Corpus by Zewail et al. (2026). (spec 0010)
 
 
39
  - Anonymous work is now private to the browser that made it, and is adopted into
40
  your account when you sign in. Anonymous visitors previously shared one bucket,
41
  so they saw each other's projects and custom constructs; each visitor now has
 
33
  travels with your results. The full 57,174-text corpus, annotated for 25
34
  cultural and moral constructs, is linked on Hugging Face; the platform ships a
35
  sample because it runs in seconds. The complete 57,174-text corpus, with the
36
+ annotations for all 25 constructs, is offered in the same dropdown for real
37
+ analyses, and an info button shows the text column, preprocessing and citation
38
+ for whichever is selected. A corpus larger than the instance's row limit is
39
+ shown as unavailable with the reason, instead of failing after you pick it.
40
+ Corpus by Zewail et al. (2026). (spec 0010)
41
  - Anonymous work is now private to the browser that made it, and is adopted into
42
  your account when you sign in. Anonymous visitors previously shared one bucket,
43
  so they saw each other's projects and custom constructs; each visitor now has
backend/app/example_corpora.py CHANGED
@@ -38,7 +38,9 @@ class ExampleCorpus:
38
  citation: str
39
  source_url: str
40
  preprocessing: str
41
- default: bool = False
 
 
42
  storage_key: str = ""
43
  # Shown on the card so a researcher can judge fit before running.
44
  detail: str = ""
@@ -57,10 +59,29 @@ class ExampleCorpus:
57
  and a missing object surfaces as a clear 404 on selection instead."""
58
  return self.path.exists() if self.bundled else True
59
 
60
- def public(self) -> dict:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  data = asdict(self)
62
- data["available"] = self.available()
63
  data["bundled"] = self.bundled
 
 
 
64
  data.pop("storage_key", None) # internal location, not the user's business
65
  return data
66
 
@@ -97,7 +118,7 @@ EXAMPLES: list[ExampleCorpus] = [
97
  "999 texts - median 25 words, mean 69. Runs in seconds on any model. "
98
  "Best for a first look at how CCR works."
99
  ),
100
- default=True,
101
  ),
102
  ExampleCorpus(
103
  id="camel_full",
@@ -139,6 +160,15 @@ def get(example_id: str) -> ExampleCorpus | None:
139
  return BY_ID.get(example_id)
140
 
141
 
142
- def listed() -> list[dict]:
143
- """Available examples, the default first."""
144
- return [e.public() for e in sorted(EXAMPLES, key=lambda e: not e.default) if e.available()]
 
 
 
 
 
 
 
 
 
 
38
  citation: str
39
  source_url: str
40
  preprocessing: str
41
+ # First in the list and selected on load. NOT a claim that it is 'the'
42
+ # corpus: the PI's ask was for CAMEL to be available and ready to use.
43
+ preselected: bool = False
44
  storage_key: str = ""
45
  # Shown on the card so a researcher can judge fit before running.
46
  detail: str = ""
 
59
  and a missing object surfaces as a clear 404 on selection instead."""
60
  return self.path.exists() if self.bundled else True
61
 
62
+ def blocked_reason(self, row_ceiling: int) -> str:
63
+ """Why this instance cannot run this corpus, or "" if it can.
64
+
65
+ A corpus larger than CCR_MAX_ROWS is refused at ingest, so offering it
66
+ as a working option produces a dead end. Saying so up front, and why,
67
+ beats an error after the click - and it corrects itself the moment the
68
+ deployment raises the limit.
69
+ """
70
+ if not self.available():
71
+ return "Not loaded on this instance."
72
+ if row_ceiling and self.n_rows > row_ceiling:
73
+ return (
74
+ f"Too large for this instance: {self.n_rows:,} texts against a "
75
+ f"{row_ceiling:,}-row limit."
76
+ )
77
+ return ""
78
+
79
+ def public(self, row_ceiling: int = 0) -> dict:
80
  data = asdict(self)
 
81
  data["bundled"] = self.bundled
82
+ blocked = self.blocked_reason(row_ceiling)
83
+ data["usable"] = not blocked
84
+ data["blocked_reason"] = blocked
85
  data.pop("storage_key", None) # internal location, not the user's business
86
  return data
87
 
 
118
  "999 texts - median 25 words, mean 69. Runs in seconds on any model. "
119
  "Best for a first look at how CCR works."
120
  ),
121
+ preselected=True,
122
  ),
123
  ExampleCorpus(
124
  id="camel_full",
 
160
  return BY_ID.get(example_id)
161
 
162
 
163
+ def listed(row_ceiling: int = 0) -> list[dict]:
164
+ """Examples this instance has, preselected one first.
165
+
166
+ Entries too large for the row ceiling are still listed but marked unusable
167
+ with a reason, rather than hidden: a corpus quietly vanishing is harder to
168
+ explain than one that says why it is unavailable.
169
+ """
170
+ return [
171
+ e.public(row_ceiling)
172
+ for e in sorted(EXAMPLES, key=lambda e: not e.preselected)
173
+ if e.available()
174
+ ]
backend/app/guide.html CHANGED
@@ -208,13 +208,13 @@ after that they stay warm.</p>
208
  <h2 id="samples">0. Sample datasets</h2>
209
  <p><b>Nothing to upload?</b> Step 1 of the workspace offers the <b>CAMEL corpus sample</b>
210
  as a built-in dataset: click <i>Use this corpus</i> and go straight to picking a construct.
211
- Two versions are offered. The <b>sample</b> (999 texts, the default) keeps the platform mix
212
  of the full corpus and finishes in seconds, which is the fastest way to see how CCR works.
213
  The <b>full corpus</b> is all 57,174 texts with the lab's annotations for 25 cultural and
214
  moral constructs included, so CCR scores can be compared against human ratings; expect a few
215
- minutes on MiniLM and considerably longer on the large models. Each card has an
216
- <i>About this dataset</i> panel with the text column, preprocessing and citation. If you
217
- publish results from either, cite:</p>
218
  <p class="cite">Zewail, A., Setia, A., Mohammadsadegh, R., Seker, F., Hajian, A., Sosa, H.,
219
  Morhayim, L., Reddy, S., &amp; Atari, M. (2026). The Cultural and Moral Expressions in
220
  Language (CAMEL) corpus [Preprint].</p>
 
208
  <h2 id="samples">0. Sample datasets</h2>
209
  <p><b>Nothing to upload?</b> Step 1 of the workspace offers the <b>CAMEL corpus sample</b>
210
  as a built-in dataset: click <i>Use this corpus</i> and go straight to picking a construct.
211
+ Two sizes are offered from one dropdown. The <b>sample</b> (999 texts) keeps the platform mix
212
  of the full corpus and finishes in seconds, which is the fastest way to see how CCR works.
213
  The <b>full corpus</b> is all 57,174 texts with the lab's annotations for 25 cultural and
214
  moral constructs included, so CCR scores can be compared against human ratings; expect a few
215
+ minutes on MiniLM and considerably longer on the large models. The <i>i</i> button beside the dropdown
216
+ shows the text column, preprocessing and citation for whichever is selected. If you publish
217
+ results from either, cite:</p>
218
  <p class="cite">Zewail, A., Setia, A., Mohammadsadegh, R., Seker, F., Hajian, A., Sosa, H.,
219
  Morhayim, L., Reddy, S., &amp; Atari, M. (2026). The Cultural and Moral Expressions in
220
  Language (CAMEL) corpus [Preprint].</p>
backend/app/main.py CHANGED
@@ -700,7 +700,7 @@ def list_corpora(project_id: str, db: Session = Depends(get_db)):
700
  @app.get("/api/example-corpora")
701
  def list_example_corpora():
702
  """Bundled corpora a visitor can analyse without uploading anything."""
703
- return example_corpora.listed()
704
 
705
 
706
  @app.post(
 
700
  @app.get("/api/example-corpora")
701
  def list_example_corpora():
702
  """Bundled corpora a visitor can analyse without uploading anything."""
703
+ return example_corpora.listed(corpus_max_rows())
704
 
705
 
706
  @app.post(
backend/static/assets/index-5w0TZmkt.js ADDED
The diff for this file is too large to render. See raw diff
 
backend/static/assets/index-CrSc0N6z.js DELETED
The diff for this file is too large to render. See raw diff
 
backend/static/assets/{index-DIYdnO4u.css → index-DBdfew4A.css} RENAMED
@@ -1 +1 @@
1
- :root{--brand-ink: #151515;--brand: #26736f;--brand-dark: #1d5a57;--accent: #339b98;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #e7f3f2;--control-height: 40px;--radius: 12px;--radius-sm: 9px;--shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);--shadow-md: 0 2px 6px rgba(16, 24, 40, .05), 0 12px 24px -16px rgba(16, 24, 40, .14);--shadow-lg: 0 24px 60px -12px rgba(16, 24, 40, .26);--ring: 0 0 0 3px rgba(51, 155, 152, .18)}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 2dppx){body{-webkit-font-smoothing:antialiased}}*{scrollbar-width:thin;scrollbar-color:#c3c9d1 transparent}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-thumb{background:#c3c9d1;border-radius:999px}*::-webkit-scrollbar-track{background:transparent}.app{display:flex;flex-direction:column;min-height:100vh}a{color:var(--brand)}.header{background:linear-gradient(180deg,#1b1b1c,var(--brand-ink));color:#fff;padding:14px 28px;display:flex;align-items:center;flex-wrap:wrap;gap:14px;position:sticky;top:0;z-index:30;border-bottom:1px solid rgba(255,255,255,.07)}.header .brand{flex:0 0 auto;display:inline-flex;align-items:center;gap:10px;font-size:17px;font-weight:650;letter-spacing:.2px;white-space:nowrap;color:#fff;text-decoration:none}.brand-logo{height:28px;width:auto;display:block}.header .brand:hover{opacity:.88}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--brand)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:var(--radius-sm);background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--brand);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:18px 20px;margin-bottom:16px;box-shadow:var(--shadow-sm)}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--brand);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--brand);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:var(--radius-sm);font:inherit;font-weight:600;cursor:pointer;line-height:1.2;transition:background-color .15s ease}button.primary:hover{background:var(--brand-dark)}button.primary:active{transform:translateY(1px)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed;transform:none}a.google-btn{background:var(--brand);color:#fff;border:none;min-height:var(--control-height);display:flex;align-items:center;justify-content:center;padding:0 18px;border-radius:var(--radius-sm);font:inherit;font-weight:600;cursor:pointer;line-height:1.2;text-decoration:none;width:100%;box-sizing:border-box}a.google-btn:hover{background:var(--brand-dark)}button.ghost,a.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:var(--radius-sm);font:inherit;cursor:pointer;line-height:1.2;text-decoration:none;transition:border-color .15s ease,color .15s ease}button.ghost:hover,a.ghost:hover{border-color:var(--brand);color:var(--brand);background:var(--accent-soft)}button.ghost:active,a.ghost:active{transform:translateY(1px)}button.linkish{background:none;border:none;color:var(--brand);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],input[type=email],input[type=password],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;background:#fff;color:var(--ink)}input[type=text],input[type=email],input[type=password],select{height:var(--control-height)}input[type=text]:focus,input[type=email]:focus,input[type=password]:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent);box-shadow:var(--ring)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(51,155,152,.5);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:var(--radius-sm);font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--brand);color:var(--brand)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.field-hint{margin-top:0;font-weight:400;color:var(--muted);font-size:12px}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.75}.pill.running:before{animation:pill-pulse 1.2s ease-in-out infinite}@keyframes pill-pulse{50%{opacity:.2}}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:linear-gradient(90deg,var(--brand),var(--accent));height:100%;transition:width .4s ease;border-radius:999px}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:var(--radius-sm);padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:var(--radius-sm);margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:8px;border-bottom:1px solid #f0f1f3;vertical-align:top;overflow-wrap:anywhere}table.docs tbody tr:last-child td{border-bottom:none}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}table.docs tbody tr:hover td{background:var(--bg)}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 14px}.stat .v{font-size:21px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--accent);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.anchor-toggle{margin-top:14px;display:flex;flex-direction:column;gap:3px}.anchor-panel{margin-top:10px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--bg)}.anchor-metric{margin:12px 0 0;padding:8px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);display:flex;gap:18px;flex-wrap:wrap;align-items:center}.anchor-metric legend{padding:0 4px}.check{display:inline-flex;align-items:center;gap:7px;cursor:pointer;font-size:13.5px}.check input{width:auto;height:auto;margin:0}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:var(--radius-sm);padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}.header-nav{margin-left:auto;display:flex;align-items:center;gap:14px;color:#fff;flex-wrap:wrap}.header-link{background:none;border:none;padding:0;color:#ffffffd1;font:inherit;font-size:13px;cursor:pointer;text-decoration:none;transition:color .15s ease}.header-link:hover{color:#fff;text-decoration:underline}.header-link.current{color:#fff;font-weight:600}.header-btn{background:#ffffff1f;color:#fff;border:1px solid rgba(255,255,255,.45);padding:5px 14px;border-radius:7px;font:inherit;font-size:13px;cursor:pointer;text-decoration:none;transition:background-color .15s ease}.header-btn:hover{background:#ffffff38}.project-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}.project-title{font-size:17px;font-weight:650;margin-right:10px}button.danger{color:var(--err);border-color:#f0c4be}button.danger:hover{color:var(--err);border-color:var(--err)}button.danger-solid{background:var(--err)}button.danger-solid:hover{background:#93261b}button.danger-solid:disabled{background:#c9ccd1}.picker{position:relative}.picker-display{width:100%;display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#fff;font:inherit;color:var(--ink);cursor:pointer;text-align:left}.picker-display:hover{border-color:var(--brand)}.picker-display .picker-caret{margin-left:auto;color:var(--muted);font-size:11px}.picker-search{width:100%;padding:8px 12px;border:1px solid var(--brand);border-radius:var(--radius-sm);font:inherit;background:#fff}.picker-search:focus{outline:none;box-shadow:0 0 0 3px #339b9826}.picker-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:50;width:100%;max-width:640px;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-lg);max-height:340px;overflow-y:auto;padding:4px 0 6px;animation:float-in .14s ease-out}@keyframes float-in{0%{opacity:0;transform:translateY(-4px)}}.picker-group{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:8px 12px 3px;position:sticky;top:0;background:var(--card)}.picker-option{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:6px 12px;cursor:pointer;font-size:13.5px}.picker-option:hover,.picker-option.active{background:var(--accent-soft)}.picker-option.selected .picker-name{font-weight:650;color:var(--brand)}.picker-name{min-width:0}.picker-meta{flex-shrink:0;font-size:11.5px;color:var(--muted);white-space:nowrap}.picker-empty{padding:12px;margin:0}.picker-delete{flex-shrink:0;border:0;background:none;cursor:pointer;color:var(--muted);font-size:15px;line-height:1;padding:0 2px;opacity:0;transition:opacity .12s,color .12s}.picker-option:hover .picker-delete,.picker-delete:focus-visible{opacity:1}.picker-delete:hover,.picker-delete:focus-visible{color:var(--err)}.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a66;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;z-index:40;padding:16px}.modal{background:var(--card);border-radius:14px;padding:26px 28px 24px;width:100%;max-width:460px;box-shadow:var(--shadow-lg);animation:modal-in .16s ease-out}@keyframes modal-in{0%{opacity:0;transform:translateY(6px) scale(.985)}}.modal h3{margin:0 0 8px;font-size:18px}.modal .hint{color:var(--muted);font-size:13px;line-height:1.5;margin:0 0 4px}.modal form.mt{margin-top:18px}.modal label.field{margin-bottom:16px}.modal label.field:last-of-type{margin-bottom:20px}.modal .row{gap:10px}.modal .row>.primary{flex:1}.modal p.small.muted.mt{margin-top:18px;padding-top:16px;border-top:1px solid var(--line);font-size:12.5px;line-height:1.6}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header .brand{font-size:16px}.header-nav{gap:10px 14px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}}.empty-state{text-align:center;max-width:560px;margin:48px auto 0;padding:36px 28px 28px}.empty-state h3{font-size:17px}.empty-state .hint{font-size:13px}.empty-actions{display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap;margin:18px 0 16px}.ghost-link{color:var(--brand);font-weight:600;text-decoration:none}.ghost-link:hover{text-decoration:underline}.welcome{padding-top:1.2rem}.welcome-hero{padding:26px 28px;background:linear-gradient(135deg,#ffffff 55%,var(--accent-soft))}.welcome-hero h2{margin:0 0 .6rem;font-size:1.5rem}.welcome-hero p{max-width:62ch}.welcome-cta{display:flex;gap:1rem;align-items:center;flex-wrap:wrap;margin:1.1rem 0 .6rem}.welcome-cta .ghost-link{font-size:.95rem}.welcome-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.welcome-step h3{margin:.2rem 0 .3rem}.step-n{width:1.9rem;height:1.9rem;border-radius:50%;background:var(--brand);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center}@media (max-width: 700px){.welcome-steps{grid-template-columns:1fr}}.construct-selected{margin-top:8px;border:1px solid var(--line);border-radius:var(--radius-sm);padding:8px 12px;background:#fff}.construct-selected summary,.construct-results summary{cursor:pointer;display:flex;align-items:baseline;gap:8px;list-style:none}.construct-selected summary::-webkit-details-marker,.construct-results summary::-webkit-details-marker{display:none}.construct-selected summary:before,.construct-results summary:before{content:"▸";color:var(--muted);font-size:11px;transition:transform .15s ease;align-self:center}details[open]>summary:before{transform:rotate(90deg)}.construct-selected-name{font-weight:600;font-size:13.5px}.construct-remove{margin-left:auto;flex-shrink:0}.construct-results summary .picker-meta{margin-left:6px}.construct-results h4{margin:14px 0 4px;font-size:13.5px}.new-construct{border:1px solid var(--line);border-radius:var(--radius-sm);padding:14px 16px 16px;margin-top:12px;background:var(--bg)}.new-construct-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.source-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin:0 0 14px}.source-card{text-align:left;padding:10px 12px;border:1.5px solid var(--line);border-radius:var(--radius-sm);background:var(--card);cursor:pointer;font:inherit;color:var(--ink);transition:border-color .12s,background .12s}.source-card:hover{border-color:var(--accent)}.source-card.active{border-color:var(--brand);background:var(--accent-soft)}.source-card b{display:block;font-size:13.5px;margin-bottom:2px}.source-card span{display:block;font-size:12px;color:var(--muted);line-height:1.35}.field-row{display:flex;gap:14px;align-items:end;flex-wrap:wrap}.field-row .field{margin-bottom:0}.field-row>.small{align-self:center;margin:0 0 8px}.ai-modeline{display:flex;align-items:center;gap:8px;margin:2px 0 10px}.infotip{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:none;cursor:help;font:inherit;line-height:1}.infotip-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--accent-soft);color:var(--brand);border:1px solid var(--brand);font-size:11px;font-weight:700;font-style:normal}.infotip:hover .infotip-icon,.infotip:focus-visible .infotip-icon{background:var(--brand);color:#fff}.infotip:focus-visible{outline:none}.infotip:focus-visible .infotip-icon{box-shadow:var(--ring)}.infotip-pop{position:absolute;left:0;top:calc(100% + 8px);z-index:40;width:300px;max-width:78vw;padding:10px 12px;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);font-size:12px;line-height:1.45;text-align:left;white-space:normal;overflow-wrap:anywhere;opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility .12s;pointer-events:none}.infotip:hover .infotip-pop,.infotip:focus-within .infotip-pop{opacity:1;visibility:visible;transform:translateY(0)}.infotip-pop b{display:block;margin-bottom:4px;color:var(--brand-ink)}.infotip-pop ul{margin:0;padding-left:16px}.infotip-pop li{margin:2px 0;color:var(--ink)}.infotip-pop a{display:inline-block;margin-top:6px;color:var(--brand);font-weight:600}.site-footer{max-width:920px;margin:40px auto 0;padding:18px 1rem 28px;border-top:1px solid var(--line);color:var(--muted);font-size:12.5px;line-height:1.6}.site-footer p{margin:0 0 6px}.site-footer p:last-child{margin-bottom:0}.site-footer strong{color:var(--ink)}.site-footer a{color:var(--brand);text-decoration:none}.site-footer a:hover{text-decoration:underline}@media (prefers-reduced-motion: reduce){.infotip-pop{transition:none}.picker-panel,.modal{animation:none}.pill.running:before{animation:none}.construct-selected summary:before,.construct-results summary:before{transition:none}button.primary:active,button.ghost:active,a.ghost:active{transform:none}}.keep-notice{margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--accent-soft);border:1px solid var(--border)}.linklike{border:0;background:none;padding:0;cursor:pointer;color:var(--brand);text-decoration:underline;font:inherit}.examples{margin:0 0 16px}.examples-label{margin-bottom:8px}.example-card{display:flex;align-items:flex-start;gap:14px;padding:12px 14px;border:1px solid var(--border);border-radius:10px;background:var(--accent-soft)}.example-card+.example-card{margin-top:8px}.example-card button{flex-shrink:0}.example-name{font-weight:600;font-size:14px}.example-desc{margin:4px 0 0;line-height:1.5}.example-cite{margin:6px 0 0;line-height:1.45;opacity:.85}.badge-default{display:inline-block;margin-left:8px;padding:1px 7px;border-radius:999px;background:var(--brand);color:#fff;font-size:10.5px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;vertical-align:1px}.example-more{margin-top:6px}.example-more summary{cursor:pointer;color:var(--brand)}.example-more p{margin:6px 0;line-height:1.5}
 
1
+ :root{--brand-ink: #151515;--brand: #26736f;--brand-dark: #1d5a57;--accent: #339b98;--ink: #1d2129;--muted: #667085;--line: #e5e7eb;--bg: #f7f7f8;--card: #ffffff;--ok: #157f3d;--err: #b42318;--accent-soft: #e7f3f2;--control-height: 40px;--radius: 12px;--radius-sm: 9px;--shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);--shadow-md: 0 2px 6px rgba(16, 24, 40, .05), 0 12px 24px -16px rgba(16, 24, 40, .14);--shadow-lg: 0 24px 60px -12px rgba(16, 24, 40, .26);--ring: 0 0 0 3px rgba(51, 155, 152, .18)}*{box-sizing:border-box}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,sans-serif;color:var(--ink);background:var(--bg);font-size:14.5px;line-height:1.5}@media (-webkit-min-device-pixel-ratio: 2),(min-resolution: 2dppx){body{-webkit-font-smoothing:antialiased}}*{scrollbar-width:thin;scrollbar-color:#c3c9d1 transparent}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-thumb{background:#c3c9d1;border-radius:999px}*::-webkit-scrollbar-track{background:transparent}.app{display:flex;flex-direction:column;min-height:100vh}a{color:var(--brand)}.header{background:linear-gradient(180deg,#1b1b1c,var(--brand-ink));color:#fff;padding:14px 28px;display:flex;align-items:center;flex-wrap:wrap;gap:14px;position:sticky;top:0;z-index:30;border-bottom:1px solid rgba(255,255,255,.07)}.header .brand{flex:0 0 auto;display:inline-flex;align-items:center;gap:10px;font-size:17px;font-weight:650;letter-spacing:.2px;white-space:nowrap;color:#fff;text-decoration:none}.brand-logo{height:28px;width:auto;display:block}.header .brand:hover{opacity:.88}.header .sub{flex:1 1 280px;min-width:0;font-size:12.5px;opacity:.85}.layout{display:flex;flex:1;min-width:0;min-height:0}.sidebar{width:250px;background:var(--card);border-right:1px solid var(--line);padding:18px 14px;flex-shrink:0;display:flex;flex-direction:column;min-height:0}.sidebar h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.7px;color:var(--muted);margin:0 0 10px 4px;display:flex;align-items:center;gap:8px}.sidebar h2 .count{background:var(--bg);border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:10.5px;letter-spacing:0;color:var(--muted)}.sidebar-filter{width:100%;padding:7px 10px;margin-bottom:10px;border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;font-size:13px;background:#fff;color:var(--ink)}.sidebar-filter:focus{outline:none;border-color:var(--brand)}.project-list{flex:1;min-height:0;overflow-y:auto;margin:0 -4px;padding:0 4px 4px}.group-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);margin:10px 4px 5px}div:first-child>.group-label{margin-top:2px}.project-item{display:block;width:100%;text-align:left;padding:9px 12px;margin-bottom:5px;border:1px solid transparent;border-radius:var(--radius-sm);background:none;cursor:pointer;font:inherit;color:var(--ink)}.project-item:hover{background:var(--bg)}.project-item.active{background:var(--accent-soft);border-color:var(--brand);font-weight:600}.project-item .project-name{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.project-item .date{display:block;font-size:11.5px;color:var(--muted);font-weight:400}.project-create{margin-top:12px;padding-top:12px;border-top:1px solid var(--line)}.project-create>button{width:100%}.main{flex:1;padding:22px 28px;overflow-y:auto;min-width:0}.card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);padding:18px 20px;margin-bottom:16px;box-shadow:var(--shadow-sm)}.card h3{margin:0 0 4px;font-size:15px}.card .hint{color:var(--muted);font-size:12.5px;margin:0 0 12px}.step-badge{display:inline-flex;align-items:center;justify-content:center;width:21px;height:21px;border-radius:50%;background:var(--brand);color:#fff;font-size:12px;font-weight:700;margin-right:8px;vertical-align:-3px}button{white-space:nowrap}button.primary{background:var(--brand);color:#fff;border:none;min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 18px;border-radius:var(--radius-sm);font:inherit;font-weight:600;cursor:pointer;line-height:1.2;transition:background-color .15s ease}button.primary:hover{background:var(--brand-dark)}button.primary:active{transform:translateY(1px)}button.primary:disabled{background:#c9ccd1;cursor:not-allowed;transform:none}a.google-btn{background:var(--brand);color:#fff;border:none;min-height:var(--control-height);display:flex;align-items:center;justify-content:center;padding:0 18px;border-radius:var(--radius-sm);font:inherit;font-weight:600;cursor:pointer;line-height:1.2;text-decoration:none;width:100%;box-sizing:border-box}a.google-btn:hover{background:var(--brand-dark)}button.ghost,a.ghost{background:none;border:1px solid var(--line);color:var(--ink);min-height:var(--control-height);display:inline-flex;align-items:center;justify-content:center;padding:0 14px;border-radius:var(--radius-sm);font:inherit;cursor:pointer;line-height:1.2;text-decoration:none;transition:border-color .15s ease,color .15s ease}button.ghost:hover,a.ghost:hover{border-color:var(--brand);color:var(--brand);background:var(--accent-soft)}button.ghost:active,a.ghost:active{transform:translateY(1px)}button.linkish{background:none;border:none;color:var(--brand);font:inherit;cursor:pointer;padding:0;text-decoration:underline}input[type=text],input[type=email],input[type=password],textarea,select{width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:var(--radius-sm);font:inherit;background:#fff;color:var(--ink)}input[type=text],input[type=email],input[type=password],select{height:var(--control-height)}input[type=text]:focus,input[type=email]:focus,input[type=password]:focus,textarea:focus,select:focus{outline:none;border-color:var(--accent);box-shadow:var(--ring)}button:focus-visible,input:focus-visible,textarea:focus-visible,select:focus-visible{outline:2px solid rgba(51,155,152,.5);outline-offset:2px}input[type=file]{display:block;max-width:100%;margin-top:6px;font-size:13px;color:var(--muted)}input[type=file]::file-selector-button{background:#fff;border:1px solid var(--line);color:var(--ink);padding:7px 14px;border-radius:var(--radius-sm);font:inherit;font-size:13px;cursor:pointer;margin-right:10px}input[type=file]::file-selector-button:hover{border-color:var(--brand);color:var(--brand)}.row>button{align-self:flex-end;margin-bottom:1px}textarea{resize:vertical}label.field{display:block;margin-bottom:10px;font-size:13px;font-weight:600}label.field>*{margin-top:4px;font-weight:400}.field-hint{margin-top:0;font-weight:400;color:var(--muted);font-size:12px}.row{display:flex;gap:14px;flex-wrap:wrap}.row>*{min-width:0}.row>.grow{flex:1;min-width:min(220px,100%)}.language-control{min-width:170px}.model-control{min-width:260px}.run-settings{display:grid;grid-template-columns:minmax(160px,230px) minmax(320px,720px) max-content;justify-content:start;gap:14px;align-items:end}.run-settings .field{margin-bottom:0}.run-button{min-width:180px;height:var(--control-height)}.construct-row{display:grid;grid-template-columns:minmax(320px,1120px) max-content;justify-content:start;align-items:end;gap:14px}.results-toolbar{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-bottom:14px;flex-wrap:wrap}.result-actions{justify-content:flex-end}.result-actions a{display:inline-flex;text-decoration:none}.pill{display:inline-flex;align-items:center;gap:6px;padding:2px 10px;border-radius:999px;font-size:11.5px;font-weight:600}.pill:before{content:"";width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.75}.pill.running:before{animation:pill-pulse 1.2s ease-in-out infinite}@keyframes pill-pulse{50%{opacity:.2}}.pill.completed{background:#e6f4ea;color:var(--ok)}.pill.running{background:#fff3e0;color:#b45309}.pill.queued{background:#eef2f7;color:var(--muted)}.pill.failed{background:#fdecea;color:var(--err)}.progress-track{background:var(--line);border-radius:999px;height:7px;overflow:hidden}.progress-fill{background:linear-gradient(90deg,var(--brand),var(--accent));height:100%;transition:width .4s ease;border-radius:999px}.warnings{background:#fff8e6;border:1px solid #f2dfa8;color:#7a5b00;border-radius:var(--radius-sm);padding:10px 14px}.error-banner{background:#fdecea;color:var(--err);border:1px solid #f5c6c0;padding:10px 14px;border-radius:var(--radius-sm);margin-bottom:14px;font-size:13px}.table-wrap{width:100%;overflow-x:auto}table.docs{width:100%;border-collapse:collapse;font-size:13px}table.docs th{text-align:left;color:var(--muted);font-size:11.5px;text-transform:uppercase;letter-spacing:.5px;padding:6px 8px;border-bottom:1px solid var(--line)}table.docs td{padding:8px;border-bottom:1px solid #f0f1f3;vertical-align:top;overflow-wrap:anywhere}table.docs tbody tr:last-child td{border-bottom:none}table.docs td.score{font-variant-numeric:tabular-nums;font-weight:600;white-space:nowrap}table.docs tbody tr:hover td{background:var(--bg)}.stat-grid{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:4px}.stat{flex:1;min-width:110px;background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 14px}.stat .v{font-size:21px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.02em}.stat .k{font-size:11.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.5px}.item-bar-row{display:flex;align-items:center;gap:10px;margin-bottom:7px}.item-bar-label{flex:1;font-size:12.5px;min-width:0;overflow-wrap:anywhere}.item-bar-track{flex:1.2;background:var(--bg);border-radius:999px;height:10px}.item-bar-fill{background:var(--accent);opacity:.85;height:100%;border-radius:999px}.item-bar-val{width:52px;text-align:right;font-variant-numeric:tabular-nums;font-size:12.5px;font-weight:600}.construct-items{margin:8px 0 0;padding-left:20px;color:var(--muted);font-size:12.5px}.construct-items li{margin-bottom:2px}.anchor-toggle{margin-top:14px;display:flex;flex-direction:column;gap:3px}.anchor-panel{margin-top:10px;padding:12px 14px;border:1px solid var(--line);border-radius:var(--radius-sm);background:var(--bg)}.anchor-metric{margin:12px 0 0;padding:8px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);display:flex;gap:18px;flex-wrap:wrap;align-items:center}.anchor-metric legend{padding:0 4px}.check{display:inline-flex;align-items:center;gap:7px;cursor:pointer;font-size:13.5px}.check input{width:auto;height:auto;margin:0}.meta-footer{font-size:12px;color:var(--muted);background:var(--bg);border-radius:var(--radius-sm);padding:10px 14px;margin-top:14px;font-variant-numeric:tabular-nums}.meta-footer code{font-size:11.5px}.muted{color:var(--muted)}.small{font-size:12.5px}.mt{margin-top:12px}.header-nav{margin-left:auto;display:flex;align-items:center;gap:14px;color:#fff;flex-wrap:wrap}.header-link{background:none;border:none;padding:0;color:#ffffffd1;font:inherit;font-size:13px;cursor:pointer;text-decoration:none;transition:color .15s ease}.header-link:hover{color:#fff;text-decoration:underline}.header-link.current{color:#fff;font-weight:600}.header-btn{background:#ffffff1f;color:#fff;border:1px solid rgba(255,255,255,.45);padding:5px 14px;border-radius:7px;font:inherit;font-size:13px;cursor:pointer;text-decoration:none;transition:background-color .15s ease}.header-btn:hover{background:#ffffff38}.project-header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;flex-wrap:wrap}.project-title{font-size:17px;font-weight:650;margin-right:10px}button.danger{color:var(--err);border-color:#f0c4be}button.danger:hover{color:var(--err);border-color:var(--err)}button.danger-solid{background:var(--err)}button.danger-solid:hover{background:#93261b}button.danger-solid:disabled{background:#c9ccd1}.picker{position:relative}.picker-display{width:100%;display:flex;align-items:center;gap:10px;padding:8px 12px;border:1px solid var(--line);border-radius:var(--radius-sm);background:#fff;font:inherit;color:var(--ink);cursor:pointer;text-align:left}.picker-display:hover{border-color:var(--brand)}.picker-display .picker-caret{margin-left:auto;color:var(--muted);font-size:11px}.picker-search{width:100%;padding:8px 12px;border:1px solid var(--brand);border-radius:var(--radius-sm);font:inherit;background:#fff}.picker-search:focus{outline:none;box-shadow:0 0 0 3px #339b9826}.picker-panel{position:absolute;top:calc(100% + 6px);left:0;z-index:50;width:100%;max-width:640px;background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow-lg);max-height:340px;overflow-y:auto;padding:4px 0 6px;animation:float-in .14s ease-out}@keyframes float-in{0%{opacity:0;transform:translateY(-4px)}}.picker-group{font-size:10.5px;text-transform:uppercase;letter-spacing:.6px;color:var(--muted);padding:8px 12px 3px;position:sticky;top:0;background:var(--card)}.picker-option{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:6px 12px;cursor:pointer;font-size:13.5px}.picker-option:hover,.picker-option.active{background:var(--accent-soft)}.picker-option.selected .picker-name{font-weight:650;color:var(--brand)}.picker-name{min-width:0}.picker-meta{flex-shrink:0;font-size:11.5px;color:var(--muted);white-space:nowrap}.picker-empty{padding:12px;margin:0}.picker-delete{flex-shrink:0;border:0;background:none;cursor:pointer;color:var(--muted);font-size:15px;line-height:1;padding:0 2px;opacity:0;transition:opacity .12s,color .12s}.picker-option:hover .picker-delete,.picker-delete:focus-visible{opacity:1}.picker-delete:hover,.picker-delete:focus-visible{color:var(--err)}.modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:18px}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;background:#14161a66;backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);display:flex;align-items:center;justify-content:center;z-index:40;padding:16px}.modal{background:var(--card);border-radius:14px;padding:26px 28px 24px;width:100%;max-width:460px;box-shadow:var(--shadow-lg);animation:modal-in .16s ease-out}@keyframes modal-in{0%{opacity:0;transform:translateY(6px) scale(.985)}}.modal h3{margin:0 0 8px;font-size:18px}.modal .hint{color:var(--muted);font-size:13px;line-height:1.5;margin:0 0 4px}.modal form.mt{margin-top:18px}.modal label.field{margin-bottom:16px}.modal label.field:last-of-type{margin-bottom:20px}.modal .row{gap:10px}.modal .row>.primary{flex:1}.modal p.small.muted.mt{margin-top:18px;padding-top:16px;border-top:1px solid var(--line);font-size:12.5px;line-height:1.6}@media (max-width: 820px){body{font-size:14px}.header{padding:12px 16px;align-items:flex-start;gap:2px 12px}.header .brand{font-size:16px}.header-nav{gap:10px 14px}.header .sub{flex:1 1 210px;font-size:12px;line-height:1.35}.layout{display:block}.sidebar{width:100%;border-right:0;border-bottom:1px solid var(--line);padding:14px}.project-list{max-height:220px;margin-right:0;flex:none}.project-create{border-top:0}.main{width:100%;padding:16px 14px 28px;overflow:visible}.card{padding:16px;margin-bottom:14px}.row,.run-settings,.construct-row,.results-toolbar,.result-actions{gap:10px}.row,.results-toolbar,.result-actions{flex-direction:column;align-items:stretch}.run-settings,.construct-row{grid-template-columns:1fr}.row>.grow,.construct-row>.grow,.language-control,.model-control{width:100%;min-width:0}.main .row>button,.main .row>a,.main .row>a>button,.results-toolbar>button{align-self:stretch;margin-bottom:0;width:100%}.run-button{width:100%;min-width:0}.stat-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr))}.stat{min-width:0}.item-bar-row{display:grid;grid-template-columns:minmax(0,1fr) 56px;gap:6px 10px}.item-bar-track{grid-column:1 / -1;width:100%}.item-bar-val{width:auto}table.docs{min-width:520px}}@media (max-width: 460px){.header .sub{flex-basis:100%}.stat-grid{grid-template-columns:1fr}}.empty-state{text-align:center;max-width:560px;margin:48px auto 0;padding:36px 28px 28px}.empty-state h3{font-size:17px}.empty-state .hint{font-size:13px}.empty-actions{display:flex;gap:16px;align-items:center;justify-content:center;flex-wrap:wrap;margin:18px 0 16px}.ghost-link{color:var(--brand);font-weight:600;text-decoration:none}.ghost-link:hover{text-decoration:underline}.welcome{padding-top:1.2rem}.welcome-hero{padding:26px 28px;background:linear-gradient(135deg,#ffffff 55%,var(--accent-soft))}.welcome-hero h2{margin:0 0 .6rem;font-size:1.5rem}.welcome-hero p{max-width:62ch}.welcome-cta{display:flex;gap:1rem;align-items:center;flex-wrap:wrap;margin:1.1rem 0 .6rem}.welcome-cta .ghost-link{font-size:.95rem}.welcome-steps{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:1rem}.welcome-step h3{margin:.2rem 0 .3rem}.step-n{width:1.9rem;height:1.9rem;border-radius:50%;background:var(--brand);color:#fff;font-weight:700;display:flex;align-items:center;justify-content:center}@media (max-width: 700px){.welcome-steps{grid-template-columns:1fr}}.construct-selected{margin-top:8px;border:1px solid var(--line);border-radius:var(--radius-sm);padding:8px 12px;background:#fff}.construct-selected summary,.construct-results summary{cursor:pointer;display:flex;align-items:baseline;gap:8px;list-style:none}.construct-selected summary::-webkit-details-marker,.construct-results summary::-webkit-details-marker{display:none}.construct-selected summary:before,.construct-results summary:before{content:"▸";color:var(--muted);font-size:11px;transition:transform .15s ease;align-self:center}details[open]>summary:before{transform:rotate(90deg)}.construct-selected-name{font-weight:600;font-size:13.5px}.construct-remove{margin-left:auto;flex-shrink:0}.construct-results summary .picker-meta{margin-left:6px}.construct-results h4{margin:14px 0 4px;font-size:13.5px}.new-construct{border:1px solid var(--line);border-radius:var(--radius-sm);padding:14px 16px 16px;margin-top:12px;background:var(--bg)}.new-construct-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}.source-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:10px;margin:0 0 14px}.source-card{text-align:left;padding:10px 12px;border:1.5px solid var(--line);border-radius:var(--radius-sm);background:var(--card);cursor:pointer;font:inherit;color:var(--ink);transition:border-color .12s,background .12s}.source-card:hover{border-color:var(--accent)}.source-card.active{border-color:var(--brand);background:var(--accent-soft)}.source-card b{display:block;font-size:13.5px;margin-bottom:2px}.source-card span{display:block;font-size:12px;color:var(--muted);line-height:1.35}.field-row{display:flex;gap:14px;align-items:end;flex-wrap:wrap}.field-row .field{margin-bottom:0}.field-row>.small{align-self:center;margin:0 0 8px}.ai-modeline{display:flex;align-items:center;gap:8px;margin:2px 0 10px}.infotip{position:relative;display:inline-flex;align-items:center;justify-content:center;padding:0;border:none;background:none;cursor:help;font:inherit;line-height:1}.infotip-icon{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;border-radius:50%;background:var(--accent-soft);color:var(--brand);border:1px solid var(--brand);font-size:11px;font-weight:700;font-style:normal}.infotip:hover .infotip-icon,.infotip:focus-visible .infotip-icon{background:var(--brand);color:#fff}.infotip:focus-visible{outline:none}.infotip:focus-visible .infotip-icon{box-shadow:var(--ring)}.infotip-pop{position:absolute;left:0;top:calc(100% + 8px);z-index:40;width:300px;max-width:78vw;padding:10px 12px;background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:var(--radius-sm);box-shadow:var(--shadow-md);font-size:12px;line-height:1.45;text-align:left;white-space:normal;overflow-wrap:anywhere;opacity:0;visibility:hidden;transform:translateY(-3px);transition:opacity .12s,transform .12s,visibility .12s;pointer-events:none}.infotip:hover .infotip-pop,.infotip:focus-within .infotip-pop{opacity:1;visibility:visible;transform:translateY(0)}.infotip-pop b{display:block;margin-bottom:4px;color:var(--brand-ink)}.infotip-pop ul{margin:0;padding-left:16px}.infotip-pop li{margin:2px 0;color:var(--ink)}.infotip-pop a{display:inline-block;margin-top:6px;color:var(--brand);font-weight:600}.site-footer{max-width:920px;margin:40px auto 0;padding:18px 1rem 28px;border-top:1px solid var(--line);color:var(--muted);font-size:12.5px;line-height:1.6}.site-footer p{margin:0 0 6px}.site-footer p:last-child{margin-bottom:0}.site-footer strong{color:var(--ink)}.site-footer a{color:var(--brand);text-decoration:none}.site-footer a:hover{text-decoration:underline}@media (prefers-reduced-motion: reduce){.infotip-pop{transition:none}.picker-panel,.modal{animation:none}.pill.running:before{animation:none}.construct-selected summary:before,.construct-results summary:before{transition:none}button.primary:active,button.ghost:active,a.ghost:active{transform:none}}.keep-notice{margin-top:12px;padding:10px 12px;border-radius:8px;background:var(--accent-soft);border:1px solid var(--border)}.linklike{border:0;background:none;padding:0;cursor:pointer;color:var(--brand);text-decoration:underline;font:inherit}.examples{margin:0 0 14px;max-width:620px}.examples-row{align-items:center;gap:8px;margin-top:4px}.examples-row select{flex:1;min-width:0}.examples-row button{flex-shrink:0}.examples-blocked{margin:6px 0 0}.icon-button{width:26px;height:26px;padding:0;border-radius:50%;font-style:italic;font-family:Georgia,Times New Roman,serif;font-size:14px;line-height:1}.example-more p{margin:6px 0;line-height:1.5}
backend/static/index.html CHANGED
@@ -19,8 +19,8 @@
19
  property="og:description"
20
  content="Score a text corpus against validated psychological questionnaires. Operated by the Culture and Morality Lab, University of Massachusetts Amherst."
21
  />
22
- <script type="module" crossorigin src="/assets/index-CrSc0N6z.js"></script>
23
- <link rel="stylesheet" crossorigin href="/assets/index-DIYdnO4u.css">
24
  </head>
25
  <body>
26
  <noscript>
 
19
  property="og:description"
20
  content="Score a text corpus against validated psychological questionnaires. Operated by the Culture and Morality Lab, University of Massachusetts Amherst."
21
  />
22
+ <script type="module" crossorigin src="/assets/index-5w0TZmkt.js"></script>
23
+ <link rel="stylesheet" crossorigin href="/assets/index-DBdfew4A.css">
24
  </head>
25
  <body>
26
  <noscript>
backend/tests/test_example_corpora.py CHANGED
@@ -51,10 +51,24 @@ def test_storage_location_is_not_exposed_to_clients():
51
  assert "storage_key" not in row
52
 
53
 
54
- def test_exactly_one_default_example():
55
- defaults = [e for e in example_corpora.EXAMPLES if e.default]
56
- assert len(defaults) == 1 and defaults[0].id == "camel_sample"
57
- assert example_corpora.listed()[0]["id"] == "camel_sample", "default is listed first"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
 
60
  def test_endpoint_lists_examples(client):
 
51
  assert "storage_key" not in row
52
 
53
 
54
+ def test_exactly_one_preselected_example_and_it_is_listed_first():
55
+ picked = [e for e in example_corpora.EXAMPLES if e.preselected]
56
+ assert len(picked) == 1 and picked[0].id == "camel_sample"
57
+ assert example_corpora.listed()[0]["id"] == "camel_sample"
58
+
59
+
60
+ def test_a_corpus_larger_than_the_row_ceiling_is_listed_but_not_usable():
61
+ """Offering a corpus this instance would refuse at ingest is a dead end.
62
+ It stays visible with the reason, and becomes usable when the deployment
63
+ raises CCR_MAX_ROWS - no code change needed."""
64
+ rows = {r["id"]: r for r in example_corpora.listed(50_000)}
65
+ assert rows["camel_sample"]["usable"] is True
66
+ assert rows["camel_full"]["usable"] is False
67
+ assert "50,000" in rows["camel_full"]["blocked_reason"]
68
+
69
+ raised = {r["id"]: r for r in example_corpora.listed(100_000)}
70
+ assert raised["camel_full"]["usable"] is True
71
+ assert raised["camel_full"]["blocked_reason"] == ""
72
 
73
 
74
  def test_endpoint_lists_examples(client):
docs/specs/0010-example-corpora.md CHANGED
@@ -69,6 +69,22 @@ stranger can push into the server; this is a file we ship at a size we chose,
69
  and letting a visitor try the platform without their own data is the point of
70
  the feature. The per-day run cap still applies.
71
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  ## Non-goals
73
 
74
  - Serving the full 57k corpus from the platform. It stays on Hugging Face,
 
69
  and letting a visitor try the platform without their own data is the point of
70
  the feature. The per-day run cap still applies.
71
 
72
+ ## Presentation (revised 2026-09-07)
73
+
74
+ The first cut rendered every example as an expanded card, which took most of
75
+ Step 1 and pushed the upload control below the fold - the wrong hierarchy, since
76
+ most researchers bring their own data. It also presented CAMEL twice, as two
77
+ "corpora", when it is one dataset in two sizes.
78
+
79
+ Now: one labelled row holding a dropdown, an info button and the action. Sizes
80
+ are options in that dropdown, so CAMEL reads as one dataset. Provenance
81
+ (description, text column, preprocessing, citation, dataset link) moved into a
82
+ modal behind the info button, where it is available without occupying the step.
83
+
84
+ The entry that loads first is `preselected`, not `default`: the PI asked for
85
+ CAMEL to be "available and ready-to-use", which is a starting point rather than
86
+ a claim that this is THE corpus, and nothing is badged as a default.
87
+
88
  ## Non-goals
89
 
90
  - Serving the full 57k corpus from the platform. It stays on Hugging Face,
frontend/src/Workspace.jsx CHANGED
@@ -7,7 +7,10 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
7
  const [corpora, setCorpora] = useState([]);
8
  const [constructs, setConstructs] = useState([]);
9
  const [examples, setExamples] = useState([]);
 
 
10
  const [loadingExample, setLoadingExample] = useState("");
 
11
  // Set after an ANONYMOUS visitor saves a construct: their work is scoped to
12
  // this browser session and expires with it, so say so at the moment it is
13
  // saved rather than letting them discover it later.
@@ -97,7 +100,16 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
97
  useEffect(() => {
98
  api.listCorpora(project.id).then(setCorpora).catch((e) => setError(e.message));
99
  api.listConstructs().then(setConstructs).catch((e) => setError(e.message));
100
- api.exampleCorpora().then(setExamples).catch(() => {});
 
 
 
 
 
 
 
 
 
101
  api
102
  .models()
103
  .then((m) => {
@@ -296,53 +308,81 @@ export default function Workspace({ project, auth, onAuthRefresh, onProjectChang
296
  </p>
297
  {examples.length > 0 && (
298
  <div className="examples">
299
- <div className="small muted examples-label">
300
- No data of your own? Start from an example corpus:
301
- </div>
302
- {examples.map((ex) => (
303
- <div key={ex.id} className="example-card">
304
- <div className="grow">
305
- <div className="example-name">
306
- {ex.name}
307
- {ex.default && <span className="badge-default">Default</span>}
308
- <span className="small muted">
309
- {" "}
310
- · {ex.n_rows.toLocaleString()} texts · {languageName(ex.language)}
311
- </span>
312
- </div>
313
- {ex.detail && <p className="small muted example-desc">{ex.detail}</p>}
314
- <details className="example-more">
315
- <summary className="small">About this dataset</summary>
316
- <p className="small">{ex.description}</p>
317
- <p className="small">
318
- <b>Text column:</b> {ex.text_column} · <b>Language:</b>{" "}
319
- {languageName(ex.language)} · <b>Texts:</b>{" "}
320
- {ex.n_rows.toLocaleString()}
321
- </p>
322
- <p className="small">
323
- <b>Preprocessing:</b> {ex.preprocessing}
324
- </p>
325
- <p className="small example-cite">
326
- <b>Cite as:</b> {ex.citation}
327
- </p>
328
- <p className="small">
329
- Recorded in the run metadata of anything you analyse with it.{" "}
330
- <a href={ex.source_url} target="_blank" rel="noreferrer">
331
- Dataset page
332
- </a>
333
- </p>
334
- </details>
335
- </div>
336
  <button
337
  type="button"
338
- className={ex.default ? "primary" : ""}
339
- onClick={() => useExample(ex)}
340
- disabled={!!loadingExample}
 
341
  >
342
- {loadingExample === ex.id ? "Loading…" : "Use this corpus"}
 
 
 
 
 
 
 
343
  </button>
344
  </div>
345
- ))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  </div>
347
  )}
348
  <div className="row">
 
7
  const [corpora, setCorpora] = useState([]);
8
  const [constructs, setConstructs] = useState([]);
9
  const [examples, setExamples] = useState([]);
10
+ const [exampleId, setExampleId] = useState("");
11
+ const [showExampleInfo, setShowExampleInfo] = useState(false);
12
  const [loadingExample, setLoadingExample] = useState("");
13
+ const selectedExample = examples.find((e) => e.id === exampleId) || null;
14
  // Set after an ANONYMOUS visitor saves a construct: their work is scoped to
15
  // this browser session and expires with it, so say so at the moment it is
16
  // saved rather than letting them discover it later.
 
100
  useEffect(() => {
101
  api.listCorpora(project.id).then(setCorpora).catch((e) => setError(e.message));
102
  api.listConstructs().then(setConstructs).catch((e) => setError(e.message));
103
+ api
104
+ .exampleCorpora()
105
+ .then((rows) => {
106
+ setExamples(rows);
107
+ // The preselected entry is a starting point, not a claim that this is
108
+ // THE corpus; the list is ordered so it lands first.
109
+ const first = rows.find((r) => r.preselected) || rows[0];
110
+ if (first) setExampleId(first.id);
111
+ })
112
+ .catch(() => {});
113
  api
114
  .models()
115
  .then((m) => {
 
308
  </p>
309
  {examples.length > 0 && (
310
  <div className="examples">
311
+ <label className="field examples-pick">
312
+ Or use a ready-to-use corpus
313
+ <div className="row examples-row">
314
+ <select
315
+ value={exampleId}
316
+ onChange={(e) => setExampleId(e.target.value)}
317
+ disabled={!!loadingExample}
318
+ >
319
+ {examples.map((ex) => (
320
+ <option key={ex.id} value={ex.id} disabled={!ex.usable}>
321
+ {ex.name} · {ex.n_rows.toLocaleString()} texts
322
+ {ex.usable ? "" : " · unavailable here"}
323
+ </option>
324
+ ))}
325
+ </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  <button
327
  type="button"
328
+ className="icon-button"
329
+ aria-label="About this corpus"
330
+ title="About this corpus"
331
+ onClick={() => setShowExampleInfo(true)}
332
  >
333
+ i
334
+ </button>
335
+ <button
336
+ type="button"
337
+ onClick={() => selectedExample && useExample(selectedExample)}
338
+ disabled={!!loadingExample || !selectedExample?.usable}
339
+ >
340
+ {loadingExample ? "Loading…" : "Use this corpus"}
341
  </button>
342
  </div>
343
+ </label>
344
+ {selectedExample && !selectedExample.usable && (
345
+ <p className="small muted examples-blocked">
346
+ {selectedExample.blocked_reason}
347
+ </p>
348
+ )}
349
+ </div>
350
+ )}
351
+ {showExampleInfo && selectedExample && (
352
+ <div className="modal-backdrop" onMouseDown={() => setShowExampleInfo(false)}>
353
+ <div
354
+ className="modal"
355
+ role="dialog"
356
+ aria-modal="true"
357
+ aria-label={`About ${selectedExample.name}`}
358
+ onMouseDown={(e) => e.stopPropagation()}
359
+ >
360
+ <h3>{selectedExample.name}</h3>
361
+ <p>{selectedExample.description}</p>
362
+ <p className="small">
363
+ <b>Texts:</b> {selectedExample.n_rows.toLocaleString()} ·{" "}
364
+ <b>Text column:</b> {selectedExample.text_column} ·{" "}
365
+ <b>Language:</b> {languageName(selectedExample.language)}
366
+ </p>
367
+ <p className="small">
368
+ <b>Preprocessing:</b> {selectedExample.preprocessing}
369
+ </p>
370
+ <p className="small">
371
+ <b>Cite as:</b> {selectedExample.citation}
372
+ </p>
373
+ <p className="small muted">
374
+ This citation is written into the run metadata of anything you analyse
375
+ with it, so the attribution travels with your results.{" "}
376
+ <a href={selectedExample.source_url} target="_blank" rel="noreferrer">
377
+ Dataset page
378
+ </a>
379
+ </p>
380
+ <div className="modal-actions">
381
+ <button type="button" onClick={() => setShowExampleInfo(false)}>
382
+ Close
383
+ </button>
384
+ </div>
385
+ </div>
386
  </div>
387
  )}
388
  <div className="row">
frontend/src/styles.css CHANGED
@@ -774,26 +774,18 @@ details[open] > summary::before { transform: rotate(90deg); }
774
  color: var(--brand); text-decoration: underline; font: inherit;
775
  }
776
 
777
- /* ---------- example corpora (Step 1) ---------- */
778
- .examples { margin: 0 0 16px; }
779
- .examples-label { margin-bottom: 8px; }
780
- .example-card {
781
- display: flex; align-items: flex-start; gap: 14px;
782
- padding: 12px 14px; border: 1px solid var(--border); border-radius: 10px;
783
- background: var(--accent-soft);
784
- }
785
- .example-card + .example-card { margin-top: 8px; }
786
- .example-card button { flex-shrink: 0; }
787
- .example-name { font-weight: 600; font-size: 14px; }
788
- .example-desc { margin: 4px 0 0; line-height: 1.5; }
789
- /* Citation is small but never hidden: results run on this corpus have to be
790
- attributable, and the run metadata records the same string. */
791
- .example-cite { margin: 6px 0 0; line-height: 1.45; opacity: 0.85; }
792
- .badge-default {
793
- display: inline-block; margin-left: 8px; padding: 1px 7px; border-radius: 999px;
794
- background: var(--brand); color: #fff; font-size: 10.5px; font-weight: 600;
795
- text-transform: uppercase; letter-spacing: 0.5px; vertical-align: 1px;
796
- }
797
- .example-more { margin-top: 6px; }
798
- .example-more summary { cursor: pointer; color: var(--brand); }
799
  .example-more p { margin: 6px 0; line-height: 1.5; }
 
774
  color: var(--brand); text-decoration: underline; font: inherit;
775
  }
776
 
777
+ /* ---------- example corpora (Step 1) ----------
778
+ One compact row, not a pile of cards: most researchers upload their own data,
779
+ so the example must not push the upload control below the fold. Detail lives
780
+ behind the info button. */
781
+ .examples { margin: 0 0 14px; max-width: 620px; }
782
+ .examples-row { align-items: center; gap: 8px; margin-top: 4px; }
783
+ .examples-row select { flex: 1; min-width: 0; }
784
+ .examples-row button { flex-shrink: 0; }
785
+ .examples-blocked { margin: 6px 0 0; }
786
+ .icon-button {
787
+ width: 26px; height: 26px; padding: 0; border-radius: 50%;
788
+ font-style: italic; font-family: Georgia, "Times New Roman", serif;
789
+ font-size: 14px; line-height: 1;
790
+ }
 
 
 
 
 
 
 
 
791
  .example-more p { margin: 6px 0; line-height: 1.5; }