@runcontext/site 0.3.5 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -12,354 +12,486 @@ var HEAD = `<!DOCTYPE html>
12
12
  <title><%= pageTitle %> \u2014 <%= siteTitle %></title>
13
13
  <link rel="preconnect" href="https://fonts.googleapis.com">
14
14
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
15
- <link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;500;600;700&family=DM+Sans:wght@300;400;500;600&family=JetBrains+Mono:wght@300;400;500&display=swap" rel="stylesheet">
15
+ <link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@300;400;500;600&family=Plus+Jakarta+Sans:wght@300;400;500;600;700&display=swap" rel="stylesheet">
16
16
  <style>
17
17
  :root {
18
- --gold: #D4A855;
19
- --gold-light: #E8C878;
20
- --gold-dark: #A07D3A;
21
- --gold-glow: rgba(212, 168, 85, 0.15);
22
- --bg: #0A0A0C;
23
- --bg-card: #111114;
24
- --bg-elevated: #18181C;
25
- --text: #E8E6E1;
26
- --text-muted: #8A8880;
27
- --text-dim: #5A5850;
28
- --border: #2A2A2E;
29
- --green: #4ADE80;
30
- --green-dim: rgba(74, 222, 128, 0.12);
31
- --blue: #60A5FA;
32
- --red: #F87171;
33
- --serif: 'Cormorant Garamond', Georgia, serif;
34
- --sans: 'DM Sans', system-ui, sans-serif;
35
- --mono: 'JetBrains Mono', monospace;
18
+ --accent: #c9a55a;
19
+ --accent-light: #e0be6a;
20
+ --accent-dim: rgba(201, 165, 90, 0.12);
21
+ --accent-border: rgba(201, 165, 90, 0.25);
22
+ --bg: #0a0908;
23
+ --bg-sidebar: #0f0e0d;
24
+ --bg-card: #141312;
25
+ --bg-hover: #1a1918;
26
+ --bg-code: #111010;
27
+ --text: #e8e6e1;
28
+ --text-secondary: #a09d94;
29
+ --text-dim: #6a675e;
30
+ --border: #252320;
31
+ --border-light: #302d28;
32
+ --green: #4aba6a;
33
+ --green-dim: rgba(74, 186, 106, 0.1);
34
+ --red: #d45050;
35
+ --red-dim: rgba(212, 80, 80, 0.08);
36
+ --blue: #5b9cf5;
37
+ --blue-dim: rgba(91, 156, 245, 0.1);
38
+ --purple: #a78bfa;
39
+ --cyan: #22d3ee;
40
+ --orange: #f59e0b;
41
+ --sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, sans-serif;
42
+ --mono: 'Geist Mono', 'SF Mono', 'Consolas', monospace;
43
+ --sidebar-w: 260px;
44
+ --topbar-h: 48px;
36
45
  }
46
+
37
47
  * { margin: 0; padding: 0; box-sizing: border-box; }
38
48
  html { scroll-behavior: smooth; }
49
+
39
50
  body {
40
51
  font-family: var(--sans);
41
52
  background: var(--bg);
42
53
  color: var(--text);
43
54
  line-height: 1.6;
44
- overflow-x: hidden;
45
55
  }
46
- a { color: var(--gold); text-decoration: none; }
56
+
57
+ a { color: var(--accent); text-decoration: none; }
47
58
  a:hover { text-decoration: underline; }
48
59
 
49
- /* === GRAIN OVERLAY === */
50
- body::before {
51
- content: '';
60
+ /* === TOPBAR === */
61
+ .topbar {
52
62
  position: fixed;
53
- inset: 0;
54
- background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
55
- pointer-events: none;
56
- z-index: 9999;
63
+ top: 0;
64
+ left: 0;
65
+ right: 0;
66
+ height: var(--topbar-h);
67
+ background: var(--bg-sidebar);
68
+ border-bottom: 1px solid var(--border);
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ padding: 0 1.25rem;
73
+ z-index: 200;
74
+ }
75
+ .topbar-left {
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 0.75rem;
57
79
  }
80
+ .topbar-logo {
81
+ font-family: var(--sans);
82
+ font-size: 0.9rem;
83
+ font-weight: 700;
84
+ color: var(--accent);
85
+ text-decoration: none;
86
+ letter-spacing: -0.02em;
87
+ }
88
+ .topbar-logo:hover { text-decoration: none; }
89
+ .topbar-sep {
90
+ color: var(--border-light);
91
+ font-size: 1.1rem;
92
+ font-weight: 300;
93
+ }
94
+ .topbar-page {
95
+ font-size: 0.8rem;
96
+ color: var(--text-secondary);
97
+ }
98
+ .topbar-right {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 1rem;
102
+ }
103
+ .topbar-link {
104
+ font-size: 0.78rem;
105
+ color: var(--text-dim);
106
+ text-decoration: none;
107
+ transition: color 0.15s;
108
+ }
109
+ .topbar-link:hover { color: var(--text); text-decoration: none; }
110
+ .topbar-docs {
111
+ font-size: 0.72rem;
112
+ font-weight: 500;
113
+ color: var(--accent);
114
+ border: 1px solid var(--accent-border);
115
+ padding: 0.3rem 0.65rem;
116
+ border-radius: 5px;
117
+ text-decoration: none;
118
+ transition: background 0.15s;
119
+ }
120
+ .topbar-docs:hover { background: var(--accent-dim); text-decoration: none; }
58
121
 
59
- /* === LAYOUT === */
60
- .page { max-width: 1200px; margin: 0 auto; padding: 3rem 2rem 6rem; }
61
- .section { margin-bottom: 3rem; }
62
- .section-label {
122
+ .menu-toggle {
123
+ display: none;
124
+ background: none;
125
+ border: none;
126
+ color: var(--text-secondary);
127
+ font-size: 1.3rem;
128
+ cursor: pointer;
129
+ padding: 0.25rem;
130
+ }
131
+
132
+ /* === SIDEBAR === */
133
+ .sidebar {
134
+ position: fixed;
135
+ top: var(--topbar-h);
136
+ left: 0;
137
+ bottom: 0;
138
+ width: var(--sidebar-w);
139
+ background: var(--bg-sidebar);
140
+ border-right: 1px solid var(--border);
141
+ overflow-y: auto;
142
+ padding: 1rem 0;
143
+ z-index: 100;
144
+ }
145
+ .sidebar::-webkit-scrollbar { width: 4px; }
146
+ .sidebar::-webkit-scrollbar-track { background: transparent; }
147
+ .sidebar::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 2px; }
148
+
149
+ .sidebar-section {
150
+ padding: 0 0.75rem;
151
+ margin-bottom: 1.25rem;
152
+ }
153
+ .sidebar-heading {
63
154
  font-family: var(--mono);
64
- font-size: 0.65rem;
65
- letter-spacing: 0.3em;
155
+ font-size: 0.6rem;
156
+ font-weight: 600;
157
+ letter-spacing: 0.15em;
66
158
  text-transform: uppercase;
67
- color: var(--gold-dark);
68
- margin-bottom: 0.75rem;
159
+ color: var(--text-dim);
160
+ padding: 0 0.5rem;
161
+ margin-bottom: 0.4rem;
69
162
  }
70
- .section h2, h2.section-title {
71
- font-family: var(--serif);
72
- font-weight: 400;
73
- font-size: clamp(1.6rem, 3vw, 2.4rem);
74
- letter-spacing: -0.01em;
75
- margin-bottom: 1rem;
163
+ .sidebar-item {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 0.5rem;
167
+ padding: 0.35rem 0.5rem;
168
+ border-radius: 5px;
169
+ font-size: 0.82rem;
170
+ color: var(--text-secondary);
171
+ text-decoration: none;
172
+ transition: background 0.12s, color 0.12s;
173
+ }
174
+ .sidebar-item:hover {
175
+ background: var(--bg-hover);
76
176
  color: var(--text);
177
+ text-decoration: none;
77
178
  }
78
- .section-intro {
79
- color: var(--text-muted);
80
- font-size: 1rem;
81
- max-width: 600px;
82
- margin-bottom: 2rem;
83
- font-weight: 300;
179
+ .sidebar-item.active {
180
+ background: var(--accent-dim);
181
+ color: var(--accent-light);
84
182
  }
85
- .divider {
86
- width: 100%;
87
- height: 1px;
88
- background: var(--border);
89
- margin: 0 auto;
183
+ .sidebar-badge {
184
+ font-family: var(--mono);
185
+ font-size: 0.55rem;
186
+ font-weight: 600;
187
+ letter-spacing: 0.08em;
188
+ text-transform: uppercase;
189
+ padding: 0.1rem 0.35rem;
190
+ border-radius: 3px;
191
+ margin-left: auto;
90
192
  }
193
+ .sidebar-badge-bronze { color: #b87a4a; background: rgba(184, 122, 74, 0.12); }
194
+ .sidebar-badge-silver { color: #a0a8b8; background: rgba(160, 168, 184, 0.1); }
195
+ .sidebar-badge-gold { color: var(--accent); background: var(--accent-dim); }
196
+ .sidebar-badge-none { color: var(--text-dim); background: rgba(106, 103, 94, 0.1); }
91
197
 
92
- /* === HERO (index page) === */
93
- .hero {
94
- text-align: center;
95
- padding: 5rem 2rem 4rem;
96
- position: relative;
198
+ /* === MAIN CONTENT === */
199
+ .main {
200
+ margin-left: var(--sidebar-w);
201
+ margin-top: var(--topbar-h);
202
+ min-height: calc(100vh - var(--topbar-h));
203
+ padding: 2rem 2.5rem 4rem;
204
+ max-width: calc(900px + var(--sidebar-w));
97
205
  }
98
- .hero::before {
99
- content: '';
100
- position: absolute;
101
- top: -30%;
102
- left: 50%;
103
- transform: translateX(-50%);
104
- width: 700px;
105
- height: 700px;
106
- background: radial-gradient(circle, var(--gold-glow) 0%, transparent 70%);
107
- pointer-events: none;
108
- }
109
- .hero-eyebrow {
110
- font-family: var(--mono);
111
- font-size: 0.7rem;
112
- letter-spacing: 0.3em;
113
- text-transform: uppercase;
114
- color: var(--gold);
206
+
207
+ /* === BREADCRUMB === */
208
+ .breadcrumb {
209
+ display: flex;
210
+ align-items: center;
211
+ gap: 0.35rem;
212
+ font-size: 0.78rem;
213
+ color: var(--text-dim);
115
214
  margin-bottom: 1.5rem;
116
- position: relative;
117
215
  }
118
- .hero h1 {
119
- font-family: var(--serif);
120
- font-weight: 300;
121
- font-size: clamp(2.5rem, 6vw, 5rem);
122
- line-height: 1.1;
123
- letter-spacing: -0.02em;
216
+ .breadcrumb a {
217
+ color: var(--text-secondary);
218
+ text-decoration: none;
219
+ }
220
+ .breadcrumb a:hover { color: var(--text); text-decoration: none; }
221
+ .breadcrumb-sep { color: var(--border-light); }
222
+
223
+ /* === PAGE HEADER === */
224
+ .page-header { margin-bottom: 2rem; }
225
+ .page-header h1 {
226
+ font-size: clamp(1.5rem, 3vw, 2rem);
227
+ font-weight: 700;
228
+ letter-spacing: -0.025em;
124
229
  color: var(--text);
125
- position: relative;
126
- }
127
- .hero h1 em { font-style: italic; color: var(--gold); }
128
- .hero-sub {
129
- font-size: 1.05rem;
130
- color: var(--text-muted);
131
- max-width: 520px;
132
- margin: 1.5rem auto 0;
230
+ line-height: 1.2;
231
+ }
232
+ .page-header .subtitle {
233
+ font-size: 0.95rem;
234
+ color: var(--text-secondary);
235
+ margin-top: 0.4rem;
133
236
  font-weight: 300;
134
- position: relative;
237
+ max-width: 600px;
135
238
  }
136
239
 
137
- /* === STATS === */
138
- .stats {
139
- display: grid;
140
- grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
240
+ /* === STATS ROW === */
241
+ .stats-row {
242
+ display: flex;
141
243
  gap: 1px;
142
244
  background: var(--border);
143
245
  border: 1px solid var(--border);
144
- border-radius: 12px;
246
+ border-radius: 8px;
145
247
  overflow: hidden;
146
- margin-bottom: 3rem;
248
+ margin-bottom: 2rem;
147
249
  }
148
- .stat {
250
+ .stat-item {
251
+ flex: 1;
149
252
  background: var(--bg-card);
150
- padding: 1.5rem;
253
+ padding: 1rem 1.25rem;
151
254
  text-align: center;
255
+ min-width: 80px;
152
256
  }
153
- .stat-value {
154
- font-family: var(--serif);
155
- font-size: 2.4rem;
156
- font-weight: 300;
157
- color: var(--gold);
257
+ .stat-val {
258
+ font-family: var(--mono);
259
+ font-size: 1.5rem;
260
+ font-weight: 600;
261
+ color: var(--accent);
158
262
  line-height: 1;
159
263
  }
160
- .stat-label {
161
- font-size: 0.7rem;
162
- color: var(--text-muted);
163
- margin-top: 0.4rem;
264
+ .stat-lbl {
265
+ font-size: 0.65rem;
266
+ color: var(--text-dim);
267
+ margin-top: 0.3rem;
164
268
  text-transform: uppercase;
165
269
  letter-spacing: 0.1em;
166
270
  }
167
271
 
272
+ /* === SECTION === */
273
+ .section { margin-bottom: 2.5rem; }
274
+ .section-label {
275
+ font-family: var(--mono);
276
+ font-size: 0.6rem;
277
+ font-weight: 600;
278
+ letter-spacing: 0.2em;
279
+ text-transform: uppercase;
280
+ color: var(--text-dim);
281
+ margin-bottom: 0.5rem;
282
+ }
283
+ .section-title {
284
+ font-size: 1.15rem;
285
+ font-weight: 600;
286
+ letter-spacing: -0.015em;
287
+ margin-bottom: 1rem;
288
+ color: var(--text);
289
+ }
290
+
168
291
  /* === CARDS === */
169
292
  .card {
170
293
  border: 1px solid var(--border);
171
- border-radius: 12px;
172
- padding: 1.5rem;
294
+ border-radius: 8px;
295
+ padding: 1rem 1.25rem;
173
296
  background: var(--bg-card);
174
- transition: border-color 0.3s, box-shadow 0.3s;
297
+ transition: border-color 0.15s;
175
298
  }
176
- .card:hover {
177
- border-color: var(--gold-dark);
178
- box-shadow: 0 0 40px rgba(212,168,85,0.06);
299
+ .card:hover { border-color: var(--border-light); }
300
+ .card-link {
301
+ text-decoration: none;
302
+ display: block;
179
303
  }
180
- .card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 1.5rem; }
181
- .card-grid-sm { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 1rem; }
304
+ .card-link:hover { text-decoration: none; }
305
+ .card-link:hover .card { border-color: var(--accent-border); }
306
+ .card-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 0.75rem; }
182
307
 
183
308
  /* === TAGS === */
184
309
  .tag {
185
- display: inline-block;
310
+ display: inline-flex;
311
+ align-items: center;
186
312
  font-family: var(--mono);
187
313
  font-size: 0.6rem;
188
- letter-spacing: 0.05em;
314
+ font-weight: 500;
315
+ letter-spacing: 0.04em;
189
316
  text-transform: uppercase;
190
- padding: 0.2rem 0.55rem;
191
- border-radius: 4px;
192
- background: var(--bg);
193
- border: 1px solid var(--border);
194
- color: var(--text-muted);
195
- }
196
- .tag-gold { color: var(--gold); border-color: rgba(212,168,85,0.3); background: rgba(212,168,85,0.08); }
197
- .tag-silver { color: #C0C0C0; border-color: rgba(192,192,192,0.3); background: rgba(192,192,192,0.08); }
198
- .tag-bronze { color: #CD7F32; border-color: rgba(205,127,50,0.3); background: rgba(205,127,50,0.08); }
199
- .tag-none { color: var(--text-dim); border-color: var(--border); }
200
- .tag-green { color: var(--green); border-color: rgba(74,222,128,0.3); background: var(--green-dim); }
201
- .tag-red { color: var(--red); border-color: rgba(248,113,113,0.3); background: rgba(248,113,113,0.08); }
202
- .tag-blue { color: var(--blue); border-color: rgba(96,165,250,0.3); background: rgba(96,165,250,0.08); }
203
- .tag-nav {
204
- padding: 0.35rem 0.75rem;
205
- font-size: 0.65rem;
206
- border-radius: 6px;
207
- text-decoration: none;
208
- transition: border-color 0.2s;
209
- }
210
- .tag-nav:hover { border-color: var(--gold); text-decoration: none; }
211
-
212
- /* === TABLES (dark) === */
213
- .table-dark { width: 100%; border-collapse: collapse; }
214
- .table-dark th {
317
+ padding: 0.15rem 0.45rem;
318
+ border-radius: 3px;
319
+ background: rgba(106, 103, 94, 0.1);
320
+ color: var(--text-dim);
321
+ border: 1px solid transparent;
322
+ }
323
+ .tag-gold { color: var(--accent); background: var(--accent-dim); }
324
+ .tag-silver { color: #a0a8b8; background: rgba(160, 168, 184, 0.08); }
325
+ .tag-bronze { color: #b87a4a; background: rgba(184, 122, 74, 0.1); }
326
+ .tag-green { color: var(--green); background: var(--green-dim); }
327
+ .tag-red { color: var(--red); background: var(--red-dim); }
328
+ .tag-blue { color: var(--blue); background: var(--blue-dim); }
329
+ .tag-purple { color: var(--purple); background: rgba(167, 139, 250, 0.1); }
330
+
331
+ /* === TABLES === */
332
+ .data-table { width: 100%; border-collapse: collapse; }
333
+ .data-table th {
215
334
  font-family: var(--mono);
216
- font-size: 0.65rem;
217
- letter-spacing: 0.15em;
335
+ font-size: 0.6rem;
336
+ font-weight: 600;
337
+ letter-spacing: 0.12em;
218
338
  text-transform: uppercase;
219
339
  color: var(--text-dim);
220
340
  text-align: left;
221
- padding: 0.75rem 1rem;
341
+ padding: 0.6rem 0.75rem;
222
342
  border-bottom: 1px solid var(--border);
223
343
  }
224
- .table-dark td {
225
- padding: 0.75rem 1rem;
226
- border-bottom: 1px solid rgba(42,42,46,0.5);
227
- font-size: 0.85rem;
344
+ .data-table td {
345
+ padding: 0.55rem 0.75rem;
346
+ border-bottom: 1px solid rgba(37, 35, 32, 0.6);
347
+ font-size: 0.82rem;
228
348
  vertical-align: top;
229
349
  }
230
- .table-dark tr:hover td { background: rgba(212,168,85,0.02); }
231
- .mono { font-family: var(--mono); font-size: 0.82rem; }
350
+ .data-table tr:hover td { background: rgba(201, 165, 90, 0.02); }
351
+ .mono { font-family: var(--mono); }
232
352
 
233
- /* === SEMANTIC ROLE PILLS === */
234
- .role-identifier { color: var(--gold); border-color: rgba(212,168,85,0.3); background: rgba(212,168,85,0.08); }
235
- .role-metric { color: #06B6D4; border-color: rgba(6,182,212,0.3); background: rgba(6,182,212,0.08); }
236
- .role-dimension { color: #818CF8; border-color: rgba(129,140,248,0.3); background: rgba(129,140,248,0.08); }
237
- .role-date { color: var(--green); border-color: rgba(74,222,128,0.3); background: var(--green-dim); }
238
- .role-attribute { color: var(--text-muted); border-color: var(--border); }
353
+ /* === SEMANTIC ROLE TAGS === */
354
+ .role-identifier { color: var(--accent); background: var(--accent-dim); }
355
+ .role-metric { color: var(--cyan); background: rgba(34, 211, 238, 0.08); }
356
+ .role-dimension { color: var(--purple); background: rgba(167, 139, 250, 0.08); }
357
+ .role-date { color: var(--green); background: var(--green-dim); }
358
+ .role-attribute { color: var(--text-dim); background: rgba(106, 103, 94, 0.08); }
239
359
 
240
- /* === DS TYPE PILLS === */
241
- .ds-type {
360
+ /* === DS TYPE TAGS === */
361
+ .ds-fact { color: var(--purple); background: rgba(167, 139, 250, 0.08); }
362
+ .ds-dimension { color: var(--green); background: var(--green-dim); }
363
+ .ds-event { color: var(--orange); background: rgba(245, 158, 11, 0.08); }
364
+ .ds-view { color: var(--blue); background: var(--blue-dim); }
365
+
366
+ /* === GOV GRID === */
367
+ .gov-grid {
368
+ display: grid;
369
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
370
+ gap: 1px;
371
+ background: var(--border);
372
+ border: 1px solid var(--border);
373
+ border-radius: 8px;
374
+ overflow: hidden;
375
+ }
376
+ .gov-cell {
377
+ background: var(--bg-card);
378
+ padding: 0.75rem 1rem;
379
+ }
380
+ .gov-label {
242
381
  font-family: var(--mono);
243
- font-size: 0.65rem;
382
+ font-size: 0.55rem;
383
+ font-weight: 600;
384
+ letter-spacing: 0.12em;
244
385
  text-transform: uppercase;
245
- letter-spacing: 0.08em;
246
- padding: 0.15rem 0.45rem;
247
- border-radius: 4px;
248
- display: inline-block;
386
+ color: var(--text-dim);
387
+ margin-bottom: 0.2rem;
249
388
  }
250
- .ds-type-fact { color: #818CF8; background: rgba(129,140,248,0.1); }
251
- .ds-type-dimension { color: #34D399; background: rgba(52,211,153,0.1); }
252
- .ds-type-event { color: #FB923C; background: rgba(251,146,60,0.1); }
253
- .ds-type-view { color: #A78BFA; background: rgba(167,139,250,0.1); }
389
+ .gov-value { font-size: 0.85rem; color: var(--text); }
254
390
 
255
- /* === METRIC CARDS === */
256
- .metric-name {
257
- font-family: var(--mono);
258
- font-size: 0.9rem;
259
- color: var(--gold-light);
260
- margin-bottom: 0.5rem;
261
- }
262
- .metric-desc {
263
- color: var(--text-muted);
264
- font-size: 0.85rem;
265
- line-height: 1.6;
266
- margin-bottom: 1rem;
391
+ /* === EXPANDABLE === */
392
+ .expandable-header {
393
+ cursor: pointer;
394
+ display: flex;
395
+ align-items: center;
396
+ justify-content: space-between;
397
+ user-select: none;
267
398
  }
268
- .metric-formula {
399
+ .expand-icon {
269
400
  font-family: var(--mono);
270
- font-size: 0.72rem;
401
+ font-size: 0.85rem;
271
402
  color: var(--text-dim);
272
- background: var(--bg);
273
- border: 1px solid var(--border);
274
- border-radius: 6px;
275
- padding: 0.6rem 0.8rem;
276
- line-height: 1.5;
277
- overflow-x: auto;
403
+ width: 18px;
404
+ text-align: center;
405
+ transition: transform 0.15s;
406
+ }
407
+ .expandable-content {
408
+ max-height: 0;
409
+ overflow: hidden;
410
+ transition: max-height 0.25s ease;
278
411
  }
279
412
 
280
- /* === QUERY CARDS === */
413
+ /* === QUERY CARD === */
281
414
  .query-card {
282
415
  border: 1px solid var(--border);
283
- border-radius: 12px;
416
+ border-radius: 8px;
284
417
  overflow: hidden;
285
- margin-bottom: 1.5rem;
286
418
  background: var(--bg-card);
287
- transition: border-color 0.3s;
419
+ margin-bottom: 1rem;
420
+ transition: border-color 0.15s;
288
421
  }
289
- .query-card:hover { border-color: var(--gold-dark); }
290
- .query-question {
291
- padding: 1.25rem 1.5rem;
292
- font-family: var(--serif);
293
- font-size: 1.1rem;
294
- font-weight: 400;
422
+ .query-card:hover { border-color: var(--border-light); }
423
+ .query-q {
424
+ padding: 1rem 1.25rem;
425
+ font-size: 0.95rem;
426
+ font-weight: 500;
295
427
  font-style: italic;
296
428
  color: var(--text);
297
429
  border-bottom: 1px solid var(--border);
298
430
  display: flex;
299
431
  align-items: flex-start;
300
- gap: 0.6rem;
432
+ gap: 0.5rem;
301
433
  }
302
- .query-question::before {
303
- content: 'Q';
434
+ .query-q-badge {
304
435
  font-family: var(--mono);
305
- font-size: 0.6rem;
436
+ font-size: 0.55rem;
306
437
  font-style: normal;
307
- letter-spacing: 0.1em;
308
- color: var(--gold);
309
- background: rgba(212,168,85,0.1);
310
- border: 1px solid rgba(212,168,85,0.2);
311
- padding: 0.15rem 0.35rem;
438
+ font-weight: 600;
439
+ color: var(--accent);
440
+ background: var(--accent-dim);
441
+ border: 1px solid var(--accent-border);
442
+ padding: 0.1rem 0.3rem;
312
443
  border-radius: 3px;
313
444
  flex-shrink: 0;
314
- margin-top: 0.3rem;
445
+ margin-top: 0.2rem;
315
446
  }
316
447
  .query-sql {
317
- padding: 1rem 1.5rem;
448
+ padding: 0.85rem 1.25rem;
318
449
  font-family: var(--mono);
319
- font-size: 0.78rem;
320
- color: var(--text-muted);
321
- line-height: 1.6;
322
- background: var(--bg);
450
+ font-size: 0.75rem;
451
+ color: var(--text-secondary);
452
+ line-height: 1.7;
453
+ background: var(--bg-code);
323
454
  overflow-x: auto;
324
455
  white-space: pre-wrap;
325
456
  }
326
457
  .query-meta {
327
- padding: 0.6rem 1.5rem;
458
+ padding: 0.5rem 1.25rem;
328
459
  display: flex;
329
- gap: 1rem;
330
- font-size: 0.7rem;
460
+ gap: 0.75rem;
461
+ font-size: 0.65rem;
331
462
  color: var(--text-dim);
332
463
  border-top: 1px solid var(--border);
333
464
  }
334
465
 
335
- /* === GUARDRAILS === */
466
+ /* === GUARDRAIL === */
336
467
  .guardrail {
337
- border: 1px solid rgba(248, 113, 113, 0.2);
338
- border-radius: 12px;
339
- padding: 1.5rem;
340
- background: rgba(248, 113, 113, 0.03);
341
- margin-bottom: 1rem;
468
+ border: 1px solid rgba(212, 80, 80, 0.15);
469
+ border-radius: 8px;
470
+ padding: 1rem 1.25rem;
471
+ background: var(--red-dim);
472
+ margin-bottom: 0.75rem;
342
473
  }
343
474
  .guardrail-name {
344
475
  font-family: var(--mono);
345
- font-size: 0.82rem;
476
+ font-size: 0.8rem;
346
477
  color: var(--red);
347
- margin-bottom: 0.4rem;
478
+ margin-bottom: 0.35rem;
479
+ font-weight: 500;
348
480
  }
349
481
  .guardrail-filter {
350
482
  font-family: var(--mono);
351
- font-size: 0.78rem;
483
+ font-size: 0.75rem;
352
484
  color: var(--text);
353
- background: var(--bg);
354
- padding: 0.4rem 0.65rem;
485
+ background: var(--bg-code);
486
+ padding: 0.35rem 0.6rem;
355
487
  border-radius: 4px;
356
488
  border: 1px solid var(--border);
357
489
  display: inline-block;
358
- margin-bottom: 0.6rem;
490
+ margin-bottom: 0.4rem;
359
491
  }
360
492
  .guardrail-reason {
361
- font-size: 0.82rem;
362
- color: var(--text-muted);
493
+ font-size: 0.8rem;
494
+ color: var(--text-secondary);
363
495
  font-weight: 300;
364
496
  }
365
497
 
@@ -369,48 +501,33 @@ var HEAD = `<!DOCTYPE html>
369
501
  align-items: stretch;
370
502
  gap: 0;
371
503
  overflow-x: auto;
372
- padding: 1.5rem 0;
373
- }
374
- .lineage-col {
375
- display: flex;
376
- flex-direction: column;
377
- gap: 0.75rem;
378
- min-width: 220px;
504
+ padding: 1rem 0;
379
505
  }
506
+ .lineage-col { display: flex; flex-direction: column; gap: 0.5rem; min-width: 200px; }
380
507
  .lineage-col-label {
381
508
  font-family: var(--mono);
382
- font-size: 0.6rem;
383
- letter-spacing: 0.2em;
509
+ font-size: 0.55rem;
510
+ letter-spacing: 0.15em;
384
511
  text-transform: uppercase;
385
512
  color: var(--text-dim);
386
- margin-bottom: 0.25rem;
513
+ margin-bottom: 0.15rem;
387
514
  padding-left: 0.5rem;
388
515
  }
389
516
  .lineage-node {
390
517
  border: 1px solid var(--border);
391
- border-radius: 8px;
392
- padding: 0.75rem 1rem;
518
+ border-radius: 6px;
519
+ padding: 0.6rem 0.85rem;
393
520
  background: var(--bg-card);
394
- transition: border-color 0.3s;
395
- }
396
- .lineage-node:hover { border-color: var(--gold-dark); }
397
- .lineage-node-name {
398
- font-family: var(--mono);
399
- font-size: 0.78rem;
400
- color: var(--text);
401
- margin-bottom: 0.15rem;
402
- }
403
- .lineage-node-detail {
404
- font-size: 0.7rem;
405
- color: var(--text-dim);
406
521
  }
522
+ .lineage-node-name { font-family: var(--mono); font-size: 0.75rem; color: var(--text); }
523
+ .lineage-node-detail { font-size: 0.65rem; color: var(--text-dim); margin-top: 0.1rem; }
407
524
  .lineage-arrow {
408
525
  display: flex;
409
526
  align-items: center;
410
527
  justify-content: center;
411
- min-width: 50px;
412
- color: var(--gold-dark);
413
- font-size: 1.3rem;
528
+ min-width: 40px;
529
+ color: var(--text-dim);
530
+ font-size: 1rem;
414
531
  }
415
532
 
416
533
  /* === SCORECARD === */
@@ -420,230 +537,242 @@ var HEAD = `<!DOCTYPE html>
420
537
  gap: 1px;
421
538
  background: var(--border);
422
539
  border: 1px solid var(--border);
423
- border-radius: 12px;
540
+ border-radius: 8px;
424
541
  overflow: hidden;
425
542
  }
426
543
  @media (max-width: 768px) { .scorecard { grid-template-columns: 1fr; } }
427
- .scorecard-tier {
428
- background: var(--bg-card);
429
- padding: 1.5rem;
430
- }
431
- .scorecard-tier-header {
544
+ .sc-tier { background: var(--bg-card); padding: 1.25rem; }
545
+ .sc-tier-head {
432
546
  display: flex;
433
547
  align-items: center;
434
548
  justify-content: space-between;
435
- margin-bottom: 1rem;
436
- }
437
- .scorecard-tier-name {
438
- font-family: var(--serif);
439
- font-size: 1.3rem;
440
- font-weight: 500;
441
- text-transform: capitalize;
549
+ margin-bottom: 0.75rem;
442
550
  }
443
- .scorecard-tier-name.bronze { color: #CD7F32; }
444
- .scorecard-tier-name.silver { color: #C0C0C0; }
445
- .scorecard-tier-name.gold { color: var(--gold); }
446
- .scorecard-pass {
551
+ .sc-tier-name { font-size: 0.9rem; font-weight: 600; text-transform: capitalize; }
552
+ .sc-tier-name.bronze { color: #b87a4a; }
553
+ .sc-tier-name.silver { color: #a0a8b8; }
554
+ .sc-tier-name.gold { color: var(--accent); }
555
+ .sc-status {
447
556
  font-family: var(--mono);
448
- font-size: 0.6rem;
449
- letter-spacing: 0.1em;
557
+ font-size: 0.55rem;
558
+ font-weight: 600;
559
+ letter-spacing: 0.08em;
450
560
  text-transform: uppercase;
451
- padding: 0.15rem 0.5rem;
452
- border-radius: 4px;
561
+ padding: 0.12rem 0.4rem;
562
+ border-radius: 3px;
453
563
  }
454
- .scorecard-pass.passed { color: var(--green); background: var(--green-dim); }
455
- .scorecard-pass.failed { color: var(--red); background: rgba(248,113,113,0.1); }
564
+ .sc-status.pass { color: var(--green); background: var(--green-dim); }
565
+ .sc-status.fail { color: var(--red); background: var(--red-dim); }
456
566
  .check-list { list-style: none; }
457
567
  .check-item {
458
568
  display: flex;
459
569
  align-items: flex-start;
460
- gap: 0.4rem;
461
- padding: 0.3rem 0;
462
- font-size: 0.75rem;
463
- color: var(--text-muted);
570
+ gap: 0.35rem;
571
+ padding: 0.2rem 0;
572
+ font-size: 0.72rem;
573
+ color: var(--text-secondary);
464
574
  line-height: 1.4;
465
575
  }
466
- .check-icon { flex-shrink: 0; margin-top: 1px; font-size: 0.8rem; }
576
+ .check-icon { flex-shrink: 0; margin-top: 1px; font-size: 0.75rem; }
467
577
  .check-icon.pass { color: var(--green); }
468
578
  .check-icon.fail { color: var(--red); }
469
579
 
470
580
  /* === GLOSSARY === */
471
581
  .glossary-grid {
472
582
  display: grid;
473
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
474
- gap: 1.5rem;
475
- }
476
- .glossary-card {
477
- border: 1px solid var(--border);
478
- border-radius: 12px;
479
- padding: 1.5rem;
480
- background: var(--bg-card);
481
- position: relative;
482
- overflow: hidden;
483
- transition: border-color 0.3s;
583
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
584
+ gap: 0.75rem;
484
585
  }
485
- .glossary-card:hover { border-color: var(--gold-dark); }
586
+ .glossary-card { position: relative; overflow: hidden; }
486
587
  .glossary-card::before {
487
588
  content: '';
488
589
  position: absolute;
489
- top: 0; left: 0; right: 0;
490
- height: 2px;
491
- background: linear-gradient(90deg, var(--gold-dark), transparent);
492
- }
493
- .glossary-term {
494
- font-family: var(--serif);
495
- font-size: 1.3rem;
496
- font-weight: 500;
497
- margin-bottom: 0.5rem;
498
- color: var(--text);
499
- }
500
- .glossary-def {
501
- font-size: 0.85rem;
502
- color: var(--text-muted);
503
- line-height: 1.7;
504
- font-weight: 300;
505
- }
506
-
507
- /* === EXPANDABLE === */
508
- .expandable-header {
509
- cursor: pointer;
510
- display: flex;
511
- align-items: center;
512
- justify-content: space-between;
513
- user-select: none;
514
- }
515
- .expand-icon {
516
- font-family: var(--mono);
517
- font-size: 0.9rem;
518
- color: var(--text-dim);
519
- transition: transform 0.2s;
520
- width: 20px;
521
- text-align: center;
590
+ top: 0; left: 0; right: 0;
591
+ height: 2px;
592
+ background: linear-gradient(90deg, var(--accent-border), transparent);
522
593
  }
523
- .expandable-content {
524
- max-height: 0;
525
- overflow: hidden;
526
- transition: max-height 0.3s ease;
594
+ .glossary-term {
595
+ font-size: 1rem;
596
+ font-weight: 600;
597
+ margin-bottom: 0.35rem;
598
+ color: var(--text);
599
+ }
600
+ .glossary-def {
601
+ font-size: 0.82rem;
602
+ color: var(--text-secondary);
603
+ line-height: 1.6;
604
+ font-weight: 300;
527
605
  }
528
606
 
529
- /* === SEARCH === */
607
+ /* === SEARCH INPUT === */
530
608
  .search-input {
531
609
  width: 100%;
532
610
  background: var(--bg-card);
533
611
  border: 1px solid var(--border);
534
- border-radius: 8px;
535
- padding: 0.8rem 1.2rem;
612
+ border-radius: 6px;
613
+ padding: 0.6rem 1rem;
536
614
  font-family: var(--sans);
537
- font-size: 1rem;
615
+ font-size: 0.9rem;
538
616
  color: var(--text);
539
617
  outline: none;
540
- transition: border-color 0.2s, box-shadow 0.2s;
618
+ transition: border-color 0.15s, box-shadow 0.15s;
541
619
  }
542
620
  .search-input::placeholder { color: var(--text-dim); }
543
621
  .search-input:focus {
544
- border-color: var(--gold-dark);
545
- box-shadow: 0 0 0 3px var(--gold-glow);
622
+ border-color: var(--accent-border);
623
+ box-shadow: 0 0 0 3px var(--accent-dim);
546
624
  }
547
625
 
548
- /* === BACK LINK === */
549
- .back-link {
550
- font-size: 0.85rem;
551
- color: var(--text-muted);
552
- display: inline-flex;
553
- align-items: center;
554
- gap: 0.3rem;
555
- margin-bottom: 1.5rem;
556
- text-decoration: none;
557
- }
558
- .back-link:hover { color: var(--gold); text-decoration: none; }
626
+ /* === SQL HIGHLIGHT === */
627
+ .sql-kw { color: var(--purple); }
628
+ .sql-fn { color: var(--green); }
629
+ .sql-str { color: var(--orange); }
630
+ .sql-num { color: var(--orange); }
559
631
 
560
- /* === GOVERNANCE GRID === */
561
- .gov-grid {
562
- display: grid;
563
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
564
- gap: 1px;
565
- background: var(--border);
566
- border: 1px solid var(--border);
567
- border-radius: 8px;
568
- overflow: hidden;
569
- }
570
- .gov-item {
571
- background: var(--bg-card);
572
- padding: 1rem 1.25rem;
632
+ /* === METRIC === */
633
+ .metric-name {
634
+ font-family: var(--mono);
635
+ font-size: 0.85rem;
636
+ font-weight: 500;
637
+ color: var(--accent-light);
638
+ margin-bottom: 0.35rem;
573
639
  }
574
- .gov-label {
640
+ .metric-desc { color: var(--text-secondary); font-size: 0.82rem; line-height: 1.6; margin-bottom: 0.75rem; }
641
+ .metric-formula {
575
642
  font-family: var(--mono);
576
- font-size: 0.6rem;
577
- letter-spacing: 0.15em;
578
- text-transform: uppercase;
643
+ font-size: 0.7rem;
579
644
  color: var(--text-dim);
580
- margin-bottom: 0.3rem;
581
- }
582
- .gov-value {
583
- font-size: 0.9rem;
584
- color: var(--text);
645
+ background: var(--bg-code);
646
+ border: 1px solid var(--border);
647
+ border-radius: 5px;
648
+ padding: 0.5rem 0.7rem;
649
+ overflow-x: auto;
585
650
  }
586
651
 
587
- /* === ANIMATIONS === */
588
- @keyframes fadeUp {
589
- from { opacity: 0; transform: translateY(20px); }
590
- to { opacity: 1; transform: translateY(0); }
591
- }
592
- @keyframes pulse {
593
- 0%, 100% { opacity: 1; }
594
- 50% { opacity: 0.4; }
595
- }
596
- .reveal {
597
- opacity: 0;
598
- transform: translateY(25px);
599
- transition: opacity 0.6s ease, transform 0.6s ease;
600
- }
601
- .reveal.visible {
602
- opacity: 1;
603
- transform: translateY(0);
652
+ /* === FOOTER === */
653
+ .site-footer {
654
+ margin-left: var(--sidebar-w);
655
+ text-align: center;
656
+ padding: 2rem 1.5rem;
657
+ color: var(--text-dim);
658
+ font-size: 0.72rem;
659
+ border-top: 1px solid var(--border);
604
660
  }
661
+ .site-footer a { color: var(--text-dim); }
662
+ .site-footer a:hover { color: var(--accent); }
605
663
 
606
- /* === SQL HIGHLIGHT === */
607
- .sql-kw { color: #818CF8; }
608
- .sql-fn { color: #34D399; }
609
- .sql-str { color: #FCD34D; }
610
- .sql-num { color: #FB923C; }
611
- .sql-cm { color: var(--text-dim); font-style: italic; }
664
+ /* === RESPONSIVE === */
665
+ @media (max-width: 860px) {
666
+ .sidebar {
667
+ transform: translateX(-100%);
668
+ transition: transform 0.25s ease;
669
+ z-index: 300;
670
+ }
671
+ .sidebar.open { transform: translateX(0); }
672
+ .main { margin-left: 0; padding: 1.5rem 1.25rem 3rem; }
673
+ .site-footer { margin-left: 0; }
674
+ .menu-toggle { display: block; }
675
+ .stats-row { flex-wrap: wrap; }
676
+ .stat-item { min-width: 60px; }
677
+ .overlay {
678
+ position: fixed;
679
+ inset: 0;
680
+ background: rgba(0,0,0,0.5);
681
+ z-index: 250;
682
+ display: none;
683
+ }
684
+ .overlay.open { display: block; }
685
+ }
612
686
  </style>
687
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
688
+ <style>
689
+ .edit-btn { background: none; border: 1px solid #c9a55a; color: #c9a55a; border-radius: 4px; padding: 2px 8px; font-size: 12px; cursor: pointer; margin-left: 8px; opacity: 0.6; transition: opacity 0.2s; }
690
+ .edit-btn:hover { opacity: 1; }
691
+ .editable { cursor: text; border-bottom: 1px dashed #c9a55a40; transition: border-color 0.2s; }
692
+ .editable:hover { border-bottom-color: #c9a55a; }
693
+ .editable:focus { outline: none; border-bottom: 2px solid #c9a55a; background: #c9a55a10; }
694
+ .staged-bar { position: fixed; bottom: 0; left: 0; right: 0; background: #1a1a2e; border-top: 2px solid #c9a55a; padding: 12px 24px; display: flex; align-items: center; justify-content: space-between; z-index: 1000; box-shadow: 0 -4px 12px rgba(0,0,0,0.3); }
695
+ .staged-btn { border: none; border-radius: 6px; padding: 8px 20px; font-size: 14px; cursor: pointer; font-weight: 500; }
696
+ .staged-btn.primary { background: #c9a55a; color: #0a0a0f; }
697
+ .staged-btn.primary:hover { background: #d4b06a; }
698
+ .staged-btn.secondary { background: transparent; border: 1px solid #666; color: #999; }
699
+ .staged-btn.secondary:hover { border-color: #999; color: #fff; }
700
+ .diff-modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 2000; display: flex; align-items: center; justify-content: center; }
701
+ .diff-modal-content { background: #1a1a2e; border: 1px solid #333; border-radius: 12px; padding: 24px; max-width: 800px; width: 90%; max-height: 80vh; overflow-y: auto; }
702
+ .diff-modal-content h2 { margin: 0 0 16px; color: #c9a55a; }
703
+ .diff-file { margin: 12px 0; padding: 12px; background: #0a0a0f; border-radius: 8px; font-family: monospace; font-size: 13px; white-space: pre-wrap; }
704
+ .diff-file-name { color: #888; font-size: 12px; margin-bottom: 8px; }
705
+ .diff-add { color: #4ade80; }
706
+ .diff-del { color: #f87171; }
707
+ .diff-actions { display: flex; gap: 12px; justify-content: flex-end; margin-top: 16px; }
708
+ .toast { position: fixed; top: 20px; right: 20px; background: #1a1a2e; border: 1px solid #c9a55a; color: #e0e0e0; padding: 12px 20px; border-radius: 8px; z-index: 3000; animation: toast-in 0.3s ease-out; }
709
+ @keyframes toast-in { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
710
+ .studio-add-btn { background: none; border: 1px dashed #c9a55a60; color: #c9a55a; border-radius: 8px; padding: 12px; width: 100%; cursor: pointer; font-size: 14px; transition: all 0.2s; }
711
+ .studio-add-btn:hover { border-color: #c9a55a; background: #c9a55a10; }
712
+ </style>
713
+ <% } %>
613
714
  </head>`;
614
- var NAV = `<nav style="position:sticky;top:0;z-index:100;background:var(--bg-card);border-bottom:1px solid var(--border);padding:0.75rem 2rem;display:flex;align-items:center;gap:2rem;">
615
- <a href="<%= basePath %>/index.html" style="font-family:var(--serif);font-size:1.25rem;font-weight:600;color:var(--gold);text-decoration:none;"><%- siteTitle %></a>
616
- <div style="display:flex;gap:1.5rem;font-size:0.82rem;">
617
- <a href="<%= basePath %>/index.html" style="color:var(--text-muted);text-decoration:none;">Models</a>
618
- <a href="<%= basePath %>/glossary.html" style="color:var(--text-muted);text-decoration:none;">Glossary</a>
619
- <a href="<%= basePath %>/search.html" style="color:var(--text-muted);text-decoration:none;">Search</a>
715
+ var SIDEBAR_DATA = `<%
716
+ var _sidebarModels = typeof models !== 'undefined' ? Object.keys(models) : (typeof model !== 'undefined' ? [model.name] : []);
717
+ var _sidebarTiers = typeof tiers !== 'undefined' ? tiers : (typeof tier !== 'undefined' && typeof model !== 'undefined' ? (function(){ var t = {}; t[model.name] = tier; return t; })() : {});
718
+ %>`;
719
+ var TOPBAR = `<div class="topbar">
720
+ <div class="topbar-left">
721
+ <button class="menu-toggle" onclick="toggleSidebar()" aria-label="Toggle menu">&#9776;</button>
722
+ <a href="<%= basePath %>/index.html" class="topbar-logo"><%- siteTitle %></a>
723
+ <span class="topbar-sep">/</span>
724
+ <span class="topbar-page"><%= pageTitle %></span>
725
+ </div>
726
+ <div class="topbar-right">
727
+ <a href="<%= basePath %>/search.html" class="topbar-link">Search</a>
728
+ <a href="https://contextkit.dev" class="topbar-docs" target="_blank" rel="noopener">Docs &nearr;</a>
729
+ </div>
730
+ </div>`;
731
+ var SIDEBAR = `<div class="overlay" id="sidebar-overlay" onclick="toggleSidebar()"></div>
732
+ <nav class="sidebar" id="sidebar">
733
+ <div class="sidebar-section">
734
+ <div class="sidebar-heading">Navigation</div>
735
+ <a href="<%= basePath %>/index.html" class="sidebar-item<%= pageTitle === 'Home' ? ' active' : '' %>">
736
+ <span>Home</span>
737
+ </a>
738
+ <a href="<%= basePath %>/glossary.html" class="sidebar-item<%= pageTitle === 'Glossary' ? ' active' : '' %>">
739
+ <span>Glossary</span>
740
+ </a>
741
+ <a href="<%= basePath %>/search.html" class="sidebar-item<%= pageTitle === 'Search' ? ' active' : '' %>">
742
+ <span>Search</span>
743
+ </a>
620
744
  </div>
745
+ <% if (_sidebarModels.length > 0) { %>
746
+ <div class="sidebar-section">
747
+ <div class="sidebar-heading">Models</div>
748
+ <% for (var _sm of _sidebarModels) { %>
749
+ <a href="<%= basePath %>/models/<%= _sm %>.html" class="sidebar-item<%= (typeof model !== 'undefined' && model.name === _sm) ? ' active' : '' %>">
750
+ <span class="mono" style="font-size:0.78rem;"><%= _sm %></span>
751
+ <% if (_sidebarTiers[_sm]) { %>
752
+ <span class="sidebar-badge sidebar-badge-<%= _sidebarTiers[_sm].tier || 'none' %>"><%= _sidebarTiers[_sm].tier || '\\u2014' %></span>
753
+ <% } %>
754
+ </a>
755
+ <% } %>
756
+ </div>
757
+ <% } %>
621
758
  </nav>`;
622
- var FOOTER = `<footer style="text-align:center;padding:3rem 2rem;color:var(--text-dim);font-size:0.78rem;border-top:1px solid var(--border);margin-top:4rem;">
623
- Generated by <a href="https://github.com/erickittelson/ContextKit" style="color:var(--gold-dark);">ContextKit</a>
759
+ var FOOTER = `<footer class="site-footer">
760
+ Generated by <a href="https://github.com/erickittelson/ContextKit">ContextKit</a>
761
+ &nbsp;&middot;&nbsp;
762
+ <a href="https://contextkit.dev" target="_blank" rel="noopener">Documentation</a>
624
763
  </footer>`;
625
764
  var TIER_BADGE = `<% function tierBadge(tier) {
626
- var cls = { none: 'tag-none', bronze: 'tag-bronze', silver: 'tag-silver', gold: 'tag-gold' };
627
- var c = cls[tier] || cls.none;
628
- return '<span class="tag ' + c + '">' + tier + '</span>';
765
+ var cls = { none: '', bronze: 'tag-bronze', silver: 'tag-silver', gold: 'tag-gold' };
766
+ var c = cls[tier] || '';
767
+ return '<span class="tag ' + c + '">' + (tier || 'none') + '</span>';
629
768
  } %>`;
630
769
  var SCRIPTS = `<script>
631
770
  (function() {
632
- // Scroll-reveal observer
633
- var reveals = document.querySelectorAll('.reveal');
634
- if (reveals.length > 0 && 'IntersectionObserver' in window) {
635
- var observer = new IntersectionObserver(function(entries) {
636
- entries.forEach(function(e) {
637
- if (e.isIntersecting) {
638
- e.target.classList.add('visible');
639
- observer.unobserve(e.target);
640
- }
641
- });
642
- }, { threshold: 0.1 });
643
- reveals.forEach(function(el) { observer.observe(el); });
644
- } else {
645
- reveals.forEach(function(el) { el.classList.add('visible'); });
646
- }
771
+ // Sidebar toggle (mobile)
772
+ window.toggleSidebar = function() {
773
+ document.getElementById('sidebar').classList.toggle('open');
774
+ document.getElementById('sidebar-overlay').classList.toggle('open');
775
+ };
647
776
 
648
777
  // Expandable sections
649
778
  window.toggleExpand = function(id) {
@@ -659,20 +788,25 @@ var SCRIPTS = `<script>
659
788
  }
660
789
  };
661
790
 
662
- // Simple SQL syntax highlighter \u2014 operates on trusted template-rendered content
791
+ // SQL syntax highlighter \u2014 operates on trusted, template-rendered content only.
792
+ // The SQL blocks contain server-rendered code snippets from the user's own
793
+ // golden queries / business rules, not arbitrary user input.
663
794
  window.highlightSQL = function() {
664
795
  var blocks = document.querySelectorAll('.sql-highlight');
665
796
  var kw = /\\b(SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|CROSS|ON|AND|OR|NOT|IN|AS|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|UNION|ALL|DISTINCT|CASE|WHEN|THEN|ELSE|END|IS|NULL|BETWEEN|LIKE|EXISTS|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|SET|VALUES|INTO|WITH|DESC|ASC|OVER|PARTITION|WINDOW|FILTER|LATERAL|UNNEST|TRUE|FALSE)\\b/gi;
666
797
  var fn = /\\b(SUM|COUNT|AVG|MIN|MAX|ROUND|COALESCE|CAST|NULLIF|ABS|UPPER|LOWER|LENGTH|TRIM|SUBSTRING|CONCAT|ROW_NUMBER|RANK|DENSE_RANK|LAG|LEAD|FIRST_VALUE|LAST_VALUE|NTILE|PERCENTILE_CONT|STRING_AGG|ARRAY_AGG|LIST|STRUCT_PACK)\\b/gi;
667
798
  var str = /('(?:[^'\\\\]|\\\\.)*')/g;
668
- var num = /\\b(\\d+\\.?\\d*)\\b/g;
669
-
670
799
  blocks.forEach(function(block) {
671
800
  var text = block.textContent || '';
801
+ // Escape HTML entities first to prevent injection
802
+ var div = document.createElement('div');
803
+ div.appendChild(document.createTextNode(text));
804
+ text = div.textContent || '';
672
805
  text = text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
673
806
  text = text.replace(str, '<span class="sql-str">$1</span>');
674
807
  text = text.replace(kw, '<span class="sql-kw">$&</span>');
675
808
  text = text.replace(fn, '<span class="sql-fn">$&</span>');
809
+ // Safe: content is from trusted template-rendered golden queries
676
810
  block.innerHTML = text;
677
811
  });
678
812
  };
@@ -682,23 +816,236 @@ var SCRIPTS = `<script>
682
816
  window.highlightSQL();
683
817
  }
684
818
  })();
685
- </script>`;
819
+ </script>
820
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
821
+ <div class="staged-bar" id="staged-bar" style="display:none;">
822
+ <span id="staged-count" style="color:#c9a55a;font-weight:500;">0 changes staged</span>
823
+ <div>
824
+ <button onclick="previewAndSave()" class="staged-btn primary">Preview &amp; Save</button>
825
+ <button onclick="discardEdits()" class="staged-btn secondary" style="margin-left:8px;">Discard</button>
826
+ </div>
827
+ </div>
828
+ <div class="diff-modal" id="diff-modal" style="display:none;">
829
+ <div class="diff-modal-content">
830
+ <h2>Review Changes</h2>
831
+ <div id="diff-container"></div>
832
+ <div class="diff-actions">
833
+ <button onclick="confirmSave()" class="staged-btn primary">Save All</button>
834
+ <button onclick="closeDiffModal()" class="staged-btn secondary">Cancel</button>
835
+ </div>
836
+ </div>
837
+ </div>
838
+ <script>
839
+ window.studioState = { edits: [] };
840
+
841
+ function stageEdit(file, path, value, label) {
842
+ window.studioState.edits = window.studioState.edits.filter(
843
+ e => !(e.file === file && e.path === path)
844
+ );
845
+ window.studioState.edits.push({ file, path, value, label });
846
+ updateStagedBar();
847
+ }
848
+
849
+ function updateStagedBar() {
850
+ const bar = document.getElementById('staged-bar');
851
+ const count = document.getElementById('staged-count');
852
+ if (!bar || !count) return;
853
+ const n = window.studioState.edits.length;
854
+ bar.style.display = n > 0 ? 'flex' : 'none';
855
+ count.textContent = n + ' change' + (n !== 1 ? 's' : '') + ' staged';
856
+ }
857
+
858
+ function discardEdits() {
859
+ window.studioState.edits = [];
860
+ updateStagedBar();
861
+ document.querySelectorAll('.editable[contenteditable="true"]').forEach(el => {
862
+ el.contentEditable = 'false';
863
+ if (el.dataset.original) el.textContent = el.dataset.original;
864
+ });
865
+ }
866
+
867
+ async function previewAndSave() {
868
+ const edits = window.studioState.edits;
869
+ if (edits.length === 0) return;
870
+
871
+ const fileGroups = {};
872
+ for (const edit of edits) {
873
+ if (!fileGroups[edit.file]) fileGroups[edit.file] = [];
874
+ fileGroups[edit.file].push(edit);
875
+ }
876
+
877
+ const previews = [];
878
+ for (const [file, fileEdits] of Object.entries(fileGroups)) {
879
+ try {
880
+ const res = await fetch('/api/preview', {
881
+ method: 'POST',
882
+ headers: { 'Content-Type': 'application/json' },
883
+ body: JSON.stringify({ file: fileEdits[0].file, path: fileEdits[0].path, value: fileEdits[0].value }),
884
+ });
885
+ const data = await res.json();
886
+ previews.push({ file, edits: fileEdits, ...data });
887
+ } catch (err) {
888
+ previews.push({ file, edits: fileEdits, error: err.message });
889
+ }
890
+ }
891
+
892
+ showDiffModal(previews);
893
+ }
894
+
895
+ function showDiffModal(previews) {
896
+ const modal = document.getElementById('diff-modal');
897
+ const container = document.getElementById('diff-container');
898
+ if (!modal || !container) return;
899
+
900
+ container.innerHTML = previews.map(p => {
901
+ if (p.error) {
902
+ return '<div class="diff-file"><div class="diff-file-name">' + p.file + '</div><span class="diff-del">Error: ' + p.error + '</span></div>';
903
+ }
904
+ const editList = p.edits.map(e => '<div class="diff-add"> ' + e.label + ': ' + JSON.stringify(e.value) + '</div>').join('');
905
+ return '<div class="diff-file"><div class="diff-file-name">' + p.file + '</div>' + editList + '</div>';
906
+ }).join('');
907
+
908
+ modal.style.display = 'flex';
909
+ }
910
+
911
+ function closeDiffModal() {
912
+ const modal = document.getElementById('diff-modal');
913
+ if (modal) modal.style.display = 'none';
914
+ }
915
+
916
+ async function confirmSave() {
917
+ const edits = window.studioState.edits;
918
+ if (edits.length === 0) return;
919
+
920
+ try {
921
+ const res = await fetch('/api/save', {
922
+ method: 'POST',
923
+ headers: { 'Content-Type': 'application/json' },
924
+ body: JSON.stringify({ edits }),
925
+ });
926
+ const data = await res.json();
927
+ const ok = data.results.filter(r => r.ok).length;
928
+ showToast(ok + ' file(s) saved');
929
+ window.studioState.edits = [];
930
+ updateStagedBar();
931
+ closeDiffModal();
932
+ } catch (err) {
933
+ showToast('Save failed: ' + err.message);
934
+ }
935
+ }
936
+
937
+ function showToast(msg) {
938
+ const existing = document.querySelector('.toast');
939
+ if (existing) existing.remove();
940
+ const toast = document.createElement('div');
941
+ toast.className = 'toast';
942
+ toast.textContent = msg;
943
+ document.body.appendChild(toast);
944
+ setTimeout(() => toast.remove(), 3000);
945
+ }
946
+
947
+ function initSSE() {
948
+ const es = new EventSource('/api/events');
949
+ es.addEventListener('update', function(e) {
950
+ try {
951
+ const data = JSON.parse(e.data);
952
+ showToast('Recompiled \u2014 ' + data.diagnosticCount + ' diagnostics');
953
+ setTimeout(() => location.reload(), 500);
954
+ } catch {}
955
+ });
956
+ }
957
+
958
+ function makeEditable(el) {
959
+ el.addEventListener('click', function() {
960
+ if (el.contentEditable === 'true') return;
961
+ el.dataset.original = el.textContent;
962
+ el.contentEditable = 'true';
963
+ el.focus();
964
+ });
965
+ el.addEventListener('blur', function() {
966
+ el.contentEditable = 'false';
967
+ const newVal = el.textContent.trim();
968
+ if (newVal !== el.dataset.original) {
969
+ stageEdit(el.dataset.file, el.dataset.path, newVal, el.dataset.label || el.dataset.path);
970
+ el.style.borderBottomColor = '#4ade80';
971
+ setTimeout(() => { el.style.borderBottomColor = ''; }, 1000);
972
+ }
973
+ });
974
+ el.addEventListener('keydown', function(e) {
975
+ if (e.key === 'Enter' && !e.shiftKey) {
976
+ e.preventDefault();
977
+ el.blur();
978
+ }
979
+ if (e.key === 'Escape') {
980
+ el.textContent = el.dataset.original;
981
+ el.contentEditable = 'false';
982
+ }
983
+ });
984
+ }
985
+
986
+ function makeDropdown(el) {
987
+ el.addEventListener('click', function(e) {
988
+ document.querySelectorAll('.studio-dropdown').forEach(d => d.remove());
989
+
990
+ const options = (el.dataset.options || '').split(',');
991
+ const dropdown = document.createElement('div');
992
+ dropdown.className = 'studio-dropdown';
993
+ dropdown.style.cssText = 'position:absolute;background:#1a1a2e;border:1px solid #c9a55a;border-radius:6px;padding:4px 0;z-index:500;min-width:120px;box-shadow:0 4px 12px rgba(0,0,0,0.4);';
994
+
995
+ options.forEach(opt => {
996
+ const item = document.createElement('div');
997
+ item.textContent = opt.trim();
998
+ item.style.cssText = 'padding:6px 16px;cursor:pointer;color:#e0e0e0;font-size:13px;';
999
+ item.addEventListener('mouseenter', () => { item.style.background = '#c9a55a20'; });
1000
+ item.addEventListener('mouseleave', () => { item.style.background = 'none'; });
1001
+ item.addEventListener('click', (ev) => {
1002
+ ev.stopPropagation();
1003
+ const val = opt.trim();
1004
+ el.textContent = val;
1005
+ stageEdit(el.dataset.file, el.dataset.path, val, el.dataset.label || el.dataset.path);
1006
+ dropdown.remove();
1007
+ el.style.borderBottomColor = '#4ade80';
1008
+ setTimeout(() => { el.style.borderBottomColor = ''; }, 1000);
1009
+ });
1010
+ dropdown.appendChild(item);
1011
+ });
1012
+
1013
+ const rect = el.getBoundingClientRect();
1014
+ dropdown.style.top = (rect.bottom + window.scrollY + 4) + 'px';
1015
+ dropdown.style.left = (rect.left + window.scrollX) + 'px';
1016
+ document.body.appendChild(dropdown);
1017
+
1018
+ setTimeout(() => {
1019
+ document.addEventListener('click', function closer() {
1020
+ dropdown.remove();
1021
+ document.removeEventListener('click', closer);
1022
+ }, { once: true });
1023
+ }, 0);
1024
+ });
1025
+ }
1026
+
1027
+ document.addEventListener('DOMContentLoaded', function() {
1028
+ document.querySelectorAll('.editable').forEach(makeEditable);
1029
+ document.querySelectorAll('.dropdown-editable').forEach(makeDropdown);
1030
+ initSSE();
1031
+ });
1032
+ </script>
1033
+ <% } %>`;
686
1034
 
687
1035
  // src/templates/index.ts
688
1036
  var indexTemplate = `${HEAD}
689
1037
  <body>
690
- ${NAV}
1038
+ ${TOPBAR}
1039
+ ${SIDEBAR_DATA}
1040
+ ${SIDEBAR}
691
1041
  ${TIER_BADGE}
692
1042
 
693
- <section class="hero">
694
- <div class="hero-eyebrow"><%- siteTitle %></div>
695
- <h1>Metadata<br><em>Catalog</em></h1>
696
- <p class="hero-sub">Explore semantic models, governed datasets, business glossary, and data quality tiers.</p>
697
- </section>
698
-
699
- <div class="divider"></div>
1043
+ <div class="main">
1044
+ <div class="page-header">
1045
+ <h1>Metadata Catalog</h1>
1046
+ <p class="subtitle">Explore semantic models, governed datasets, business glossary, and data quality tiers.</p>
1047
+ </div>
700
1048
 
701
- <main class="page">
702
1049
  <%
703
1050
  var modelNames = Object.keys(models);
704
1051
  var totalDatasets = 0;
@@ -716,83 +1063,85 @@ ${TIER_BADGE}
716
1063
  var totalOwners = Object.keys(owners).length;
717
1064
  %>
718
1065
 
719
- <div class="stats reveal">
720
- <div class="stat">
721
- <div class="stat-value"><%= modelNames.length %></div>
722
- <div class="stat-label">Models</div>
1066
+ <div class="stats-row">
1067
+ <div class="stat-item">
1068
+ <div class="stat-val"><%= modelNames.length %></div>
1069
+ <div class="stat-lbl">Models</div>
723
1070
  </div>
724
- <div class="stat">
725
- <div class="stat-value"><%= totalDatasets %></div>
726
- <div class="stat-label">Datasets</div>
1071
+ <div class="stat-item">
1072
+ <div class="stat-val"><%= totalDatasets %></div>
1073
+ <div class="stat-lbl">Datasets</div>
727
1074
  </div>
728
- <div class="stat">
729
- <div class="stat-value"><%= totalFields %></div>
730
- <div class="stat-label">Fields</div>
1075
+ <div class="stat-item">
1076
+ <div class="stat-val"><%= totalFields %></div>
1077
+ <div class="stat-lbl">Fields</div>
731
1078
  </div>
732
- <div class="stat">
733
- <div class="stat-value"><%= totalTerms %></div>
734
- <div class="stat-label">Terms</div>
1079
+ <div class="stat-item">
1080
+ <div class="stat-val"><%= totalTerms %></div>
1081
+ <div class="stat-lbl">Terms</div>
735
1082
  </div>
736
- <div class="stat">
737
- <div class="stat-value"><%= totalOwners %></div>
738
- <div class="stat-label">Owners</div>
1083
+ <div class="stat-item">
1084
+ <div class="stat-val"><%= totalOwners %></div>
1085
+ <div class="stat-lbl">Owners</div>
739
1086
  </div>
740
1087
  </div>
741
1088
 
742
- <div class="section reveal">
1089
+ <div class="section">
743
1090
  <div class="section-label">Semantic Models</div>
744
1091
  <h2 class="section-title">Models</h2>
745
1092
  <% if (modelNames.length === 0) { %>
746
- <p style="color:var(--text-muted);">No models found.</p>
1093
+ <p style="color:var(--text-secondary);">No models found. Run <code style="font-family:var(--mono);background:var(--bg-card);padding:0.15rem 0.4rem;border-radius:3px;">context introspect</code> to get started.</p>
747
1094
  <% } else { %>
748
1095
  <div class="card-grid">
749
1096
  <% for (var name of modelNames) { %>
750
- <div class="card">
751
- <div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.6rem;">
752
- <a href="<%= basePath %>/models/<%= name %>.html" style="font-family:var(--mono);font-size:0.95rem;color:var(--gold-light);text-decoration:none;">
753
- <%= name %>
754
- </a>
755
- <% if (tiers[name]) { %><%- tierBadge(tiers[name].tier) %><% } %>
756
- </div>
757
- <% if (models[name].description) { %>
758
- <p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:0.75rem;font-weight:300;"><%= models[name].description %></p>
759
- <% } %>
760
- <% if (governance[name]) { %>
761
- <div style="display:flex;gap:0.5rem;flex-wrap:wrap;align-items:center;">
762
- <% if (governance[name].owner) { %>
763
- <a href="<%= basePath %>/owners/<%= governance[name].owner %>.html" class="tag tag-nav" style="text-decoration:none;"><%= governance[name].owner %></a>
764
- <% } %>
765
- <% if (governance[name].trust) { %>
766
- <span class="tag tag-green"><%= governance[name].trust %></span>
767
- <% } %>
768
- <% if (governance[name].tags) { for (var t of governance[name].tags) { %>
769
- <span class="tag"><%= t %></span>
770
- <% } } %>
1097
+ <a href="<%= basePath %>/models/<%= name %>.html" class="card-link">
1098
+ <div class="card">
1099
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.4rem;">
1100
+ <span class="mono" style="font-size:0.88rem;color:var(--accent-light);"><%= name %></span>
1101
+ <% if (tiers[name]) { %><%- tierBadge(tiers[name].tier) %><% } %>
771
1102
  </div>
772
- <% } %>
773
- </div>
1103
+ <% if (models[name].description) { %>
1104
+ <p style="color:var(--text-secondary);font-size:0.82rem;margin-bottom:0.5rem;font-weight:300;"><%= models[name].description %></p>
1105
+ <% } %>
1106
+ <% if (governance[name]) { %>
1107
+ <div style="display:flex;gap:0.35rem;flex-wrap:wrap;align-items:center;">
1108
+ <% if (governance[name].owner) { %>
1109
+ <span class="tag"><%= governance[name].owner %></span>
1110
+ <% } %>
1111
+ <% if (governance[name].trust) { %>
1112
+ <span class="tag tag-green"><%= governance[name].trust %></span>
1113
+ <% } %>
1114
+ <% if (governance[name].tags) { for (var t of governance[name].tags) { %>
1115
+ <span class="tag"><%= t %></span>
1116
+ <% } } %>
1117
+ </div>
1118
+ <% } %>
1119
+ </div>
1120
+ </a>
774
1121
  <% } %>
775
1122
  </div>
776
1123
  <% } %>
777
1124
  </div>
778
1125
 
779
1126
  <% if (Object.keys(owners).length > 0) { %>
780
- <div class="section reveal">
1127
+ <div class="section">
781
1128
  <div class="section-label">Data Stewardship</div>
782
1129
  <h2 class="section-title">Owners</h2>
783
- <div class="card-grid-sm">
1130
+ <div class="card-grid">
784
1131
  <% for (var oid of Object.keys(owners)) { %>
785
- <a href="<%= basePath %>/owners/<%= oid %>.html" class="card" style="text-decoration:none;">
786
- <div style="font-size:1rem;font-weight:500;color:var(--text);margin-bottom:0.25rem;"><%= owners[oid].display_name %></div>
787
- <% if (owners[oid].team) { %>
788
- <div style="font-size:0.78rem;color:var(--text-dim);"><%= owners[oid].team %></div>
789
- <% } %>
1132
+ <a href="<%= basePath %>/owners/<%= oid %>.html" class="card-link">
1133
+ <div class="card">
1134
+ <div style="font-size:0.9rem;font-weight:500;color:var(--text);margin-bottom:0.15rem;"><%= owners[oid].display_name %></div>
1135
+ <% if (owners[oid].team) { %>
1136
+ <div style="font-size:0.75rem;color:var(--text-dim);"><%= owners[oid].team %></div>
1137
+ <% } %>
1138
+ </div>
790
1139
  </a>
791
1140
  <% } %>
792
1141
  </div>
793
1142
  </div>
794
1143
  <% } %>
795
- </main>
1144
+ </div>
796
1145
 
797
1146
  ${FOOTER}
798
1147
  ${SCRIPTS}
@@ -802,49 +1151,93 @@ ${SCRIPTS}
802
1151
  // src/templates/model.ts
803
1152
  var modelTemplate = `${HEAD}
804
1153
  <body>
805
- ${NAV}
1154
+ ${TOPBAR}
1155
+ ${SIDEBAR_DATA}
1156
+ ${SIDEBAR}
806
1157
  ${TIER_BADGE}
807
1158
 
808
- <main class="page">
809
- <a href="<%= basePath %>/" class="back-link">&larr; All Models</a>
1159
+ <div class="main">
1160
+ <div class="breadcrumb">
1161
+ <a href="<%= basePath %>/index.html">Home</a>
1162
+ <span class="breadcrumb-sep">/</span>
1163
+ <span><%= model.name %></span>
1164
+ </div>
1165
+
1166
+ <div class="page-header">
1167
+ <div style="display:flex;align-items:center;gap:0.6rem;">
1168
+ <h1><%= model.name %></h1>
1169
+ <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>
1170
+ </div>
1171
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1172
+ <p class="subtitle"><span class="editable" data-file="context/models/<%= model.name %>.osi.yaml" data-path="semantic_model.0.description" data-label="Model description"><%= model.description || 'Add description...' %></span></p>
1173
+ <% } else { %>
1174
+ <% if (model.description) { %>
1175
+ <p class="subtitle"><%= model.description %></p>
1176
+ <% } %>
1177
+ <% } %>
1178
+ </div>
810
1179
 
811
- <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:0.5rem;">
812
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);"><%= model.name %></h1>
813
- <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>
1180
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1181
+ <div class="section">
1182
+ <div class="section-label">AI Context</div>
1183
+ <div class="card" style="margin-bottom:1rem;">
1184
+ <span class="editable" data-file="context/models/<%= model.name %>.osi.yaml" data-path="semantic_model.0.ai_context" data-label="AI context"><%= (typeof model.ai_context === 'string' ? model.ai_context : (model.ai_context ? JSON.stringify(model.ai_context) : 'Add AI context...')) %></span>
1185
+ </div>
814
1186
  </div>
815
- <% if (model.description) { %>
816
- <p style="color:var(--text-muted);font-size:1rem;margin-bottom:1.5rem;max-width:700px;font-weight:300;"><%= model.description %></p>
1187
+ <% } else { %>
1188
+ <% if (model.ai_context) { %>
1189
+ <div class="section">
1190
+ <div class="section-label">AI Context</div>
1191
+ <div class="card" style="margin-bottom:1rem;">
1192
+ <p style="font-size:0.85rem;color:var(--text-secondary);font-weight:300;line-height:1.6;"><%= typeof model.ai_context === 'string' ? model.ai_context : JSON.stringify(model.ai_context) %></p>
1193
+ </div>
1194
+ </div>
1195
+ <% } %>
817
1196
  <% } %>
818
1197
 
819
- <div style="display:flex;gap:0.6rem;margin-bottom:2.5rem;">
820
- <a href="<%= basePath %>/models/<%= model.name %>/schema.html" class="tag tag-nav tag-blue">Schema Browser</a>
821
- <a href="<%= basePath %>/models/<%= model.name %>/rules.html" class="tag tag-nav tag-gold">Rules &amp; Queries</a>
1198
+ <div style="display:flex;gap:0.5rem;margin-bottom:2rem;">
1199
+ <a href="<%= basePath %>/models/<%= model.name %>/schema.html" class="tag tag-blue" style="padding:0.3rem 0.6rem;font-size:0.65rem;text-decoration:none;">Schema Browser</a>
1200
+ <a href="<%= basePath %>/models/<%= model.name %>/rules.html" class="tag tag-gold" style="padding:0.3rem 0.6rem;font-size:0.65rem;text-decoration:none;">Rules &amp; Queries</a>
822
1201
  </div>
823
1202
 
824
1203
  <% if (gov) { %>
825
- <div class="section reveal">
1204
+ <div class="section">
826
1205
  <div class="section-label">Governance</div>
827
1206
  <div class="gov-grid">
828
- <div class="gov-item">
1207
+ <div class="gov-cell">
829
1208
  <div class="gov-label">Owner</div>
830
1209
  <div class="gov-value"><a href="<%= basePath %>/owners/<%= gov.owner %>.html"><%= gov.owner %></a></div>
831
1210
  </div>
832
- <% if (gov.trust) { %>
833
- <div class="gov-item">
1211
+ <div class="gov-cell">
834
1212
  <div class="gov-label">Trust</div>
835
- <div class="gov-value"><%= gov.trust %></div>
1213
+ <div class="gov-value">
1214
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1215
+ <span class="dropdown-editable" data-file="context/governance/<%= model.name %>.governance.yaml" data-path="trust" data-label="Trust" data-options="draft,reviewed,endorsed,certified"><%= gov.trust || 'Select...' %></span>
1216
+ <% } else { %>
1217
+ <%= gov.trust || '' %>
1218
+ <% } %>
1219
+ </div>
836
1220
  </div>
837
- <% } %>
838
1221
  <% if (gov.security) { %>
839
- <div class="gov-item">
1222
+ <div class="gov-cell">
840
1223
  <div class="gov-label">Security</div>
841
1224
  <div class="gov-value"><%= gov.security %></div>
842
1225
  </div>
843
1226
  <% } %>
1227
+ <div class="gov-cell">
1228
+ <div class="gov-label">Refresh Cadence</div>
1229
+ <div class="gov-value">
1230
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1231
+ <span class="editable" data-file="context/governance/<%= model.name %>.governance.yaml" data-path="refresh" data-label="Refresh cadence"><%= gov.refresh || 'Add refresh cadence...' %></span>
1232
+ <% } else { %>
1233
+ <%= gov.refresh || '' %>
1234
+ <% } %>
1235
+ </div>
1236
+ </div>
844
1237
  <% if (gov.tags && gov.tags.length > 0) { %>
845
- <div class="gov-item">
1238
+ <div class="gov-cell">
846
1239
  <div class="gov-label">Tags</div>
847
- <div class="gov-value" style="display:flex;gap:0.4rem;flex-wrap:wrap;">
1240
+ <div class="gov-value" style="display:flex;gap:0.3rem;flex-wrap:wrap;">
848
1241
  <% for (var t of gov.tags) { %><span class="tag"><%= t %></span><% } %>
849
1242
  </div>
850
1243
  </div>
@@ -854,44 +1247,37 @@ ${TIER_BADGE}
854
1247
  <% } %>
855
1248
 
856
1249
  <% if (model.datasets && model.datasets.length > 0) { %>
857
- <div class="section reveal">
1250
+ <div class="section">
858
1251
  <div class="section-label">Data Explorer</div>
859
1252
  <h2 class="section-title">Datasets</h2>
860
- <p style="color:var(--text-muted);font-size:0.85rem;margin-bottom:1.5rem;font-weight:300;">Click a dataset to explore its fields, governance, and metadata.</p>
861
- <div style="display:flex;flex-direction:column;gap:1rem;">
1253
+ <div style="display:flex;flex-direction:column;gap:0.75rem;">
862
1254
  <% for (var i = 0; i < model.datasets.length; i++) { var ds = model.datasets[i]; %>
863
1255
  <% var dsGov = gov && gov.datasets && gov.datasets[ds.name]; %>
864
1256
  <div class="card">
865
1257
  <div class="expandable-header" onclick="toggleExpand('ds-<%= i %>')">
866
1258
  <div>
867
- <div style="display:flex;align-items:center;gap:0.6rem;margin-bottom:0.25rem;">
868
- <span class="mono" style="color:var(--text);"><%= ds.name %></span>
1259
+ <div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.15rem;">
1260
+ <span class="mono" style="font-size:0.85rem;color:var(--text);"><%= ds.name %></span>
869
1261
  <% if (dsGov && dsGov.table_type) { %>
870
- <span class="ds-type ds-type-<%= dsGov.table_type %>"><%= dsGov.table_type %></span>
1262
+ <span class="tag ds-<%= dsGov.table_type %>"><%= dsGov.table_type %></span>
871
1263
  <% } %>
872
1264
  <% if (ds.fields) { %><span class="tag"><%= ds.fields.length %> fields</span><% } %>
873
1265
  </div>
874
- <div style="font-size:0.75rem;color:var(--text-dim);font-family:var(--mono);">
1266
+ <div style="font-size:0.72rem;color:var(--text-dim);font-family:var(--mono);">
875
1267
  <%= ds.source %>
876
- <% if (dsGov && dsGov.grain) { %> &middot; <span style="color:var(--text-muted);font-family:var(--sans);"><%= dsGov.grain %></span><% } %>
877
- <% if (dsGov && dsGov.refresh) { %> &middot; <span style="color:var(--text-muted);font-family:var(--sans);"><%= dsGov.refresh %></span><% } %>
1268
+ <% if (dsGov && dsGov.grain) { %> &middot; <span style="font-family:var(--sans);color:var(--text-secondary);"><%= dsGov.grain %></span><% } %>
878
1269
  </div>
879
1270
  <% if (ds.description) { %>
880
- <p style="font-size:0.82rem;color:var(--text-muted);margin-top:0.4rem;font-weight:300;"><%= ds.description %></p>
1271
+ <p style="font-size:0.8rem;color:var(--text-secondary);margin-top:0.3rem;font-weight:300;"><%= ds.description %></p>
881
1272
  <% } %>
882
1273
  </div>
883
1274
  <span class="expand-icon" id="ds-<%= i %>-icon">+</span>
884
1275
  </div>
885
1276
  <div class="expandable-content" id="ds-<%= i %>">
886
1277
  <% if (ds.fields && ds.fields.length > 0) { %>
887
- <table class="table-dark" style="margin-top:1rem;">
1278
+ <table class="data-table" style="margin-top:0.75rem;">
888
1279
  <thead>
889
- <tr>
890
- <th>Field</th>
891
- <th>Description</th>
892
- <th>Role</th>
893
- <th>Aggregation</th>
894
- </tr>
1280
+ <tr><th>Field</th><th>Description</th><th>Role</th><th>Aggregation</th></tr>
895
1281
  </thead>
896
1282
  <tbody>
897
1283
  <% for (var field of ds.fields) { %>
@@ -899,10 +1285,10 @@ ${TIER_BADGE}
899
1285
  <% var fGov = gov && gov.fields && gov.fields[fKey]; %>
900
1286
  <% var role = fGov && fGov.semantic_role ? fGov.semantic_role : ''; %>
901
1287
  <tr>
902
- <td class="mono" style="font-size:0.78rem;"><%= field.name %></td>
903
- <td style="color:var(--text-muted);font-size:0.82rem;"><%= field.description || '' %></td>
1288
+ <td class="mono" style="font-size:0.75rem;"><%= field.name %></td>
1289
+ <td style="color:var(--text-secondary);font-size:0.8rem;"><%= field.description || '' %></td>
904
1290
  <td><% if (role) { %><span class="tag role-<%= role %>"><%= role %></span><% } %></td>
905
- <td style="font-family:var(--mono);font-size:0.78rem;color:var(--text-dim);"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>
1291
+ <td class="mono" style="font-size:0.75rem;color:var(--text-dim);"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>
906
1292
  </tr>
907
1293
  <% } %>
908
1294
  </tbody>
@@ -916,20 +1302,20 @@ ${TIER_BADGE}
916
1302
  <% } %>
917
1303
 
918
1304
  <% if (model.relationships && model.relationships.length > 0) { %>
919
- <div class="section reveal">
1305
+ <div class="section">
920
1306
  <div class="section-label">Data Model</div>
921
1307
  <h2 class="section-title">Relationships</h2>
922
- <table class="table-dark">
1308
+ <table class="data-table">
923
1309
  <thead>
924
1310
  <tr><th>Name</th><th>From</th><th></th><th>To</th></tr>
925
1311
  </thead>
926
1312
  <tbody>
927
1313
  <% for (var rel of model.relationships) { %>
928
1314
  <tr>
929
- <td style="color:var(--text-muted);font-size:0.82rem;"><%= rel.name %></td>
930
- <td class="mono" style="font-size:0.78rem;"><%= rel.from %></td>
931
- <td style="color:var(--gold-dark);text-align:center;">&rarr;</td>
932
- <td class="mono" style="font-size:0.78rem;"><%= rel.to %></td>
1315
+ <td style="color:var(--text-secondary);font-size:0.8rem;"><%= rel.name %></td>
1316
+ <td class="mono" style="font-size:0.75rem;"><%= rel.from %></td>
1317
+ <td style="color:var(--text-dim);text-align:center;">&rarr;</td>
1318
+ <td class="mono" style="font-size:0.75rem;"><%= rel.to %></td>
933
1319
  </tr>
934
1320
  <% } %>
935
1321
  </tbody>
@@ -938,7 +1324,7 @@ ${TIER_BADGE}
938
1324
  <% } %>
939
1325
 
940
1326
  <% if (model.metrics && model.metrics.length > 0) { %>
941
- <div class="section reveal">
1327
+ <div class="section">
942
1328
  <div class="section-label">Computed Metrics</div>
943
1329
  <h2 class="section-title">Metrics</h2>
944
1330
  <div class="card-grid">
@@ -955,8 +1341,229 @@ ${TIER_BADGE}
955
1341
  </div>
956
1342
  <% } %>
957
1343
 
1344
+ <% if (rules && rules.golden_queries && rules.golden_queries.length > 0) { %>
1345
+ <div class="section">
1346
+ <div class="section-label">Golden Queries</div>
1347
+ <h2 class="section-title">Pre-validated Queries</h2>
1348
+ <% for (var gq of rules.golden_queries) { %>
1349
+ <div class="query-card">
1350
+ <div class="query-q">
1351
+ <span class="query-q-badge">Q</span>
1352
+ <span><%= gq.question %></span>
1353
+ </div>
1354
+ <div class="query-sql sql-highlight"><%= gq.sql %></div>
1355
+ <% if (gq.description) { %>
1356
+ <div class="query-meta"><span><%= gq.description %></span></div>
1357
+ <% } %>
1358
+ </div>
1359
+ <% } %>
1360
+ </div>
1361
+ <% } %>
1362
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1363
+ <div class="section" id="golden-queries-studio">
1364
+ <% if (!(rules && rules.golden_queries && rules.golden_queries.length > 0)) { %>
1365
+ <div class="section-label">Golden Queries</div>
1366
+ <h2 class="section-title">Pre-validated Queries</h2>
1367
+ <% } %>
1368
+ <div id="golden-query-forms"></div>
1369
+ <button class="studio-add-btn" onclick="addGoldenQuery('<%= model.name %>')">+ Add Golden Query</button>
1370
+ </div>
1371
+ <script>
1372
+ function addGoldenQuery(modelName) {
1373
+ var container = document.getElementById('golden-query-forms');
1374
+ var card = document.createElement('div');
1375
+ card.className = 'card';
1376
+ card.style.marginBottom = '1rem';
1377
+
1378
+ var questionLabel = document.createElement('label');
1379
+ questionLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1380
+ questionLabel.textContent = 'Question';
1381
+ var questionInput = document.createElement('input');
1382
+ questionInput.type = 'text';
1383
+ questionInput.className = 'search-input gq-question';
1384
+ questionInput.placeholder = 'e.g. What is the total revenue by region?';
1385
+ questionInput.style.fontSize = '0.85rem';
1386
+ var questionDiv = document.createElement('div');
1387
+ questionDiv.style.marginBottom = '0.75rem';
1388
+ questionDiv.appendChild(questionLabel);
1389
+ questionDiv.appendChild(questionInput);
1390
+
1391
+ var sqlLabel = document.createElement('label');
1392
+ sqlLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1393
+ sqlLabel.textContent = 'SQL';
1394
+ var sqlInput = document.createElement('textarea');
1395
+ sqlInput.className = 'search-input gq-sql';
1396
+ sqlInput.rows = 4;
1397
+ sqlInput.placeholder = 'SELECT ...';
1398
+ sqlInput.style.cssText = 'font-family:var(--mono);font-size:0.8rem;resize:vertical;';
1399
+ var sqlDiv = document.createElement('div');
1400
+ sqlDiv.style.marginBottom = '0.75rem';
1401
+ sqlDiv.appendChild(sqlLabel);
1402
+ sqlDiv.appendChild(sqlInput);
1403
+
1404
+ var descLabel = document.createElement('label');
1405
+ descLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1406
+ descLabel.textContent = 'Description';
1407
+ var descInput = document.createElement('input');
1408
+ descInput.type = 'text';
1409
+ descInput.className = 'search-input gq-desc';
1410
+ descInput.placeholder = 'Optional description';
1411
+ descInput.style.fontSize = '0.85rem';
1412
+ var descDiv = document.createElement('div');
1413
+ descDiv.style.marginBottom = '0.75rem';
1414
+ descDiv.appendChild(descLabel);
1415
+ descDiv.appendChild(descInput);
1416
+
1417
+ var stageBtn = document.createElement('button');
1418
+ stageBtn.className = 'staged-btn primary';
1419
+ stageBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1420
+ stageBtn.textContent = 'Stage';
1421
+ stageBtn.addEventListener('click', function() { stageGoldenQuery(stageBtn, modelName); });
1422
+
1423
+ var cancelBtn = document.createElement('button');
1424
+ cancelBtn.className = 'staged-btn secondary';
1425
+ cancelBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1426
+ cancelBtn.textContent = 'Cancel';
1427
+ cancelBtn.addEventListener('click', function() { card.remove(); });
1428
+
1429
+ var btnDiv = document.createElement('div');
1430
+ btnDiv.style.cssText = 'display:flex;gap:0.5rem;';
1431
+ btnDiv.appendChild(stageBtn);
1432
+ btnDiv.appendChild(cancelBtn);
1433
+
1434
+ card.appendChild(questionDiv);
1435
+ card.appendChild(sqlDiv);
1436
+ card.appendChild(descDiv);
1437
+ card.appendChild(btnDiv);
1438
+ container.appendChild(card);
1439
+ }
1440
+
1441
+ function stageGoldenQuery(btn, modelName) {
1442
+ var card = btn.closest('.card');
1443
+ var question = card.querySelector('.gq-question').value.trim();
1444
+ var sql = card.querySelector('.gq-sql').value.trim();
1445
+ var desc = card.querySelector('.gq-desc').value.trim();
1446
+ if (!question || !sql) { showToast('Question and SQL are required'); return; }
1447
+ var entry = { question: question, sql: sql };
1448
+ if (desc) entry.description = desc;
1449
+ stageEdit('context/rules/' + modelName + '.rules.yaml', 'golden_queries.+', entry, 'Add golden query: ' + question);
1450
+ card.style.borderColor = '#4ade80';
1451
+ btn.textContent = 'Staged';
1452
+ btn.disabled = true;
1453
+ }
1454
+ </script>
1455
+ <% } %>
1456
+
1457
+ <% if (rules && rules.guardrail_filters && rules.guardrail_filters.length > 0) { %>
1458
+ <div class="section">
1459
+ <div class="section-label">Safety</div>
1460
+ <h2 class="section-title">Guardrail Filters</h2>
1461
+ <% for (var gf of rules.guardrail_filters) { %>
1462
+ <div class="guardrail">
1463
+ <div class="guardrail-name"><%= gf.name %></div>
1464
+ <div class="guardrail-filter"><%= gf.filter %></div>
1465
+ <div class="guardrail-reason"><%= gf.reason %></div>
1466
+ </div>
1467
+ <% } %>
1468
+ </div>
1469
+ <% } %>
1470
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1471
+ <div class="section" id="guardrails-studio">
1472
+ <% if (!(rules && rules.guardrail_filters && rules.guardrail_filters.length > 0)) { %>
1473
+ <div class="section-label">Safety</div>
1474
+ <h2 class="section-title">Guardrail Filters</h2>
1475
+ <% } %>
1476
+ <div id="guardrail-forms"></div>
1477
+ <button class="studio-add-btn" onclick="addGuardrail('<%= model.name %>')">+ Add Guardrail</button>
1478
+ </div>
1479
+ <script>
1480
+ function addGuardrail(modelName) {
1481
+ var container = document.getElementById('guardrail-forms');
1482
+ var card = document.createElement('div');
1483
+ card.className = 'card';
1484
+ card.style.marginBottom = '1rem';
1485
+
1486
+ var nameLabel = document.createElement('label');
1487
+ nameLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1488
+ nameLabel.textContent = 'Name';
1489
+ var nameInput = document.createElement('input');
1490
+ nameInput.type = 'text';
1491
+ nameInput.className = 'search-input gr-name';
1492
+ nameInput.placeholder = 'e.g. pii_filter';
1493
+ nameInput.style.fontSize = '0.85rem';
1494
+ var nameDiv = document.createElement('div');
1495
+ nameDiv.style.marginBottom = '0.75rem';
1496
+ nameDiv.appendChild(nameLabel);
1497
+ nameDiv.appendChild(nameInput);
1498
+
1499
+ var filterLabel = document.createElement('label');
1500
+ filterLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1501
+ filterLabel.textContent = 'Filter Expression';
1502
+ var filterInput = document.createElement('input');
1503
+ filterInput.type = 'text';
1504
+ filterInput.className = 'search-input gr-filter';
1505
+ filterInput.placeholder = 'e.g. WHERE email IS NOT NULL';
1506
+ filterInput.style.cssText = 'font-family:var(--mono);font-size:0.85rem;';
1507
+ var filterDiv = document.createElement('div');
1508
+ filterDiv.style.marginBottom = '0.75rem';
1509
+ filterDiv.appendChild(filterLabel);
1510
+ filterDiv.appendChild(filterInput);
1511
+
1512
+ var reasonLabel = document.createElement('label');
1513
+ reasonLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1514
+ reasonLabel.textContent = 'Reason';
1515
+ var reasonInput = document.createElement('input');
1516
+ reasonInput.type = 'text';
1517
+ reasonInput.className = 'search-input gr-reason';
1518
+ reasonInput.placeholder = 'Why this guardrail exists';
1519
+ reasonInput.style.fontSize = '0.85rem';
1520
+ var reasonDiv = document.createElement('div');
1521
+ reasonDiv.style.marginBottom = '0.75rem';
1522
+ reasonDiv.appendChild(reasonLabel);
1523
+ reasonDiv.appendChild(reasonInput);
1524
+
1525
+ var stageBtn = document.createElement('button');
1526
+ stageBtn.className = 'staged-btn primary';
1527
+ stageBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1528
+ stageBtn.textContent = 'Stage';
1529
+ stageBtn.addEventListener('click', function() { stageGuardrail(stageBtn, modelName); });
1530
+
1531
+ var cancelBtn = document.createElement('button');
1532
+ cancelBtn.className = 'staged-btn secondary';
1533
+ cancelBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1534
+ cancelBtn.textContent = 'Cancel';
1535
+ cancelBtn.addEventListener('click', function() { card.remove(); });
1536
+
1537
+ var btnDiv = document.createElement('div');
1538
+ btnDiv.style.cssText = 'display:flex;gap:0.5rem;';
1539
+ btnDiv.appendChild(stageBtn);
1540
+ btnDiv.appendChild(cancelBtn);
1541
+
1542
+ card.appendChild(nameDiv);
1543
+ card.appendChild(filterDiv);
1544
+ card.appendChild(reasonDiv);
1545
+ card.appendChild(btnDiv);
1546
+ container.appendChild(card);
1547
+ }
1548
+
1549
+ function stageGuardrail(btn, modelName) {
1550
+ var card = btn.closest('.card');
1551
+ var name = card.querySelector('.gr-name').value.trim();
1552
+ var filter = card.querySelector('.gr-filter').value.trim();
1553
+ var reason = card.querySelector('.gr-reason').value.trim();
1554
+ if (!name || !filter) { showToast('Name and filter expression are required'); return; }
1555
+ var entry = { name: name, filter: filter };
1556
+ if (reason) entry.reason = reason;
1557
+ stageEdit('context/rules/' + modelName + '.rules.yaml', 'guardrail_filters.+', entry, 'Add guardrail: ' + name);
1558
+ card.style.borderColor = '#4ade80';
1559
+ btn.textContent = 'Staged';
1560
+ btn.disabled = true;
1561
+ }
1562
+ </script>
1563
+ <% } %>
1564
+
958
1565
  <% if (lineage && (lineage.upstream && lineage.upstream.length > 0 || lineage.downstream && lineage.downstream.length > 0)) { %>
959
- <div class="section reveal">
1566
+ <div class="section">
960
1567
  <div class="section-label">Data Lineage</div>
961
1568
  <h2 class="section-title">Lineage</h2>
962
1569
  <div class="lineage-flow">
@@ -974,8 +1581,8 @@ ${TIER_BADGE}
974
1581
  <% } %>
975
1582
  <div class="lineage-col">
976
1583
  <div class="lineage-col-label">This Model</div>
977
- <div class="lineage-node" style="border-color:var(--gold-dark);">
978
- <div class="lineage-node-name" style="color:var(--gold);"><%= model.name %></div>
1584
+ <div class="lineage-node" style="border-color:var(--accent-border);">
1585
+ <div class="lineage-node-name" style="color:var(--accent);"><%= model.name %></div>
979
1586
  </div>
980
1587
  </div>
981
1588
  <% if (lineage.downstream && lineage.downstream.length > 0) { %>
@@ -993,21 +1600,127 @@ ${TIER_BADGE}
993
1600
  </div>
994
1601
  </div>
995
1602
  <% } %>
1603
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1604
+ <div class="section" id="lineage-studio">
1605
+ <% if (!(lineage && (lineage.upstream && lineage.upstream.length > 0 || lineage.downstream && lineage.downstream.length > 0))) { %>
1606
+ <div class="section-label">Data Lineage</div>
1607
+ <h2 class="section-title">Lineage</h2>
1608
+ <% } %>
1609
+ <div id="upstream-forms"></div>
1610
+ <button class="studio-add-btn" onclick="addUpstreamSource('<%= model.name %>')" style="margin-bottom:0.75rem;">+ Add Upstream Source</button>
1611
+ <div id="downstream-forms"></div>
1612
+ <button class="studio-add-btn" onclick="addDownstreamTarget('<%= model.name %>')">+ Add Downstream Target</button>
1613
+ </div>
1614
+ <script>
1615
+ function createLineageForm(container, direction, modelName) {
1616
+ var card = document.createElement('div');
1617
+ card.className = 'card';
1618
+ card.style.marginBottom = '1rem';
1619
+
1620
+ var nameLabel = document.createElement('label');
1621
+ nameLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1622
+ nameLabel.textContent = direction === 'upstream' ? 'Source Name' : 'Target Name';
1623
+ var nameInput = document.createElement('input');
1624
+ nameInput.type = 'text';
1625
+ nameInput.className = 'search-input lin-name';
1626
+ nameInput.placeholder = direction === 'upstream' ? 'e.g. raw_orders_db' : 'e.g. revenue_dashboard';
1627
+ nameInput.style.fontSize = '0.85rem';
1628
+ var nameDiv = document.createElement('div');
1629
+ nameDiv.style.marginBottom = '0.75rem';
1630
+ nameDiv.appendChild(nameLabel);
1631
+ nameDiv.appendChild(nameInput);
1632
+
1633
+ var typeLabel = document.createElement('label');
1634
+ typeLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1635
+ typeLabel.textContent = 'Type';
1636
+ var typeSelect = document.createElement('select');
1637
+ typeSelect.className = 'search-input lin-type';
1638
+ typeSelect.style.fontSize = '0.85rem';
1639
+ ['pipeline', 'dashboard', 'api', 'file', 'derived'].forEach(function(opt) {
1640
+ var option = document.createElement('option');
1641
+ option.value = opt;
1642
+ option.textContent = opt;
1643
+ typeSelect.appendChild(option);
1644
+ });
1645
+ var typeDiv = document.createElement('div');
1646
+ typeDiv.style.marginBottom = '0.75rem';
1647
+ typeDiv.appendChild(typeLabel);
1648
+ typeDiv.appendChild(typeSelect);
1649
+
1650
+ var notesLabel = document.createElement('label');
1651
+ notesLabel.style.cssText = 'display:block;font-size:0.7rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.25rem;font-family:var(--mono);';
1652
+ notesLabel.textContent = 'Notes';
1653
+ var notesInput = document.createElement('input');
1654
+ notesInput.type = 'text';
1655
+ notesInput.className = 'search-input lin-notes';
1656
+ notesInput.placeholder = 'Optional notes';
1657
+ notesInput.style.fontSize = '0.85rem';
1658
+ var notesDiv = document.createElement('div');
1659
+ notesDiv.style.marginBottom = '0.75rem';
1660
+ notesDiv.appendChild(notesLabel);
1661
+ notesDiv.appendChild(notesInput);
1662
+
1663
+ var stageBtn = document.createElement('button');
1664
+ stageBtn.className = 'staged-btn primary';
1665
+ stageBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1666
+ stageBtn.textContent = 'Stage';
1667
+ stageBtn.addEventListener('click', function() {
1668
+ var entryName = card.querySelector('.lin-name').value.trim();
1669
+ var entryType = card.querySelector('.lin-type').value;
1670
+ var entryNotes = card.querySelector('.lin-notes').value.trim();
1671
+ if (!entryName) { showToast((direction === 'upstream' ? 'Source' : 'Target') + ' name is required'); return; }
1672
+ var entry = {};
1673
+ if (direction === 'upstream') { entry.source = entryName; } else { entry.target = entryName; }
1674
+ entry.type = entryType;
1675
+ if (entryNotes) entry.notes = entryNotes;
1676
+ stageEdit('context/lineage/' + modelName + '.lineage.yaml', direction + '.+', entry, 'Add ' + direction + ': ' + entryName);
1677
+ card.style.borderColor = '#4ade80';
1678
+ stageBtn.textContent = 'Staged';
1679
+ stageBtn.disabled = true;
1680
+ });
1681
+
1682
+ var cancelBtn = document.createElement('button');
1683
+ cancelBtn.className = 'staged-btn secondary';
1684
+ cancelBtn.style.cssText = 'font-size:0.8rem;padding:6px 16px;';
1685
+ cancelBtn.textContent = 'Cancel';
1686
+ cancelBtn.addEventListener('click', function() { card.remove(); });
1687
+
1688
+ var btnDiv = document.createElement('div');
1689
+ btnDiv.style.cssText = 'display:flex;gap:0.5rem;';
1690
+ btnDiv.appendChild(stageBtn);
1691
+ btnDiv.appendChild(cancelBtn);
1692
+
1693
+ card.appendChild(nameDiv);
1694
+ card.appendChild(typeDiv);
1695
+ card.appendChild(notesDiv);
1696
+ card.appendChild(btnDiv);
1697
+ container.appendChild(card);
1698
+ }
1699
+
1700
+ function addUpstreamSource(modelName) {
1701
+ createLineageForm(document.getElementById('upstream-forms'), 'upstream', modelName);
1702
+ }
1703
+
1704
+ function addDownstreamTarget(modelName) {
1705
+ createLineageForm(document.getElementById('downstream-forms'), 'downstream', modelName);
1706
+ }
1707
+ </script>
1708
+ <% } %>
996
1709
 
997
1710
  <% if (tier) { %>
998
- <div class="section reveal">
1711
+ <div class="section">
999
1712
  <div class="section-label">Data Quality</div>
1000
1713
  <h2 class="section-title">Tier Scorecard</h2>
1001
1714
  <div class="scorecard">
1002
1715
  <% var tierLevels = ['bronze', 'silver', 'gold']; %>
1003
1716
  <% for (var lvl of tierLevels) { %>
1004
- <div class="scorecard-tier">
1005
- <div class="scorecard-tier-header">
1006
- <span class="scorecard-tier-name <%= lvl %>"><%= lvl %></span>
1717
+ <div class="sc-tier">
1718
+ <div class="sc-tier-head">
1719
+ <span class="sc-tier-name <%= lvl %>"><%= lvl %></span>
1007
1720
  <% if (tier[lvl].passed) { %>
1008
- <span class="scorecard-pass passed">Passed</span>
1721
+ <span class="sc-status pass">Passed</span>
1009
1722
  <% } else { %>
1010
- <span class="scorecard-pass failed">Not passed</span>
1723
+ <span class="sc-status fail">Not passed</span>
1011
1724
  <% } %>
1012
1725
  </div>
1013
1726
  <% if (tier[lvl].checks && tier[lvl].checks.length > 0) { %>
@@ -1025,7 +1738,7 @@ ${TIER_BADGE}
1025
1738
  </div>
1026
1739
  </div>
1027
1740
  <% } %>
1028
- </main>
1741
+ </div>
1029
1742
 
1030
1743
  ${FOOTER}
1031
1744
  ${SCRIPTS}
@@ -1035,77 +1748,102 @@ ${SCRIPTS}
1035
1748
  // src/templates/schema.ts
1036
1749
  var schemaTemplate = `${HEAD}
1037
1750
  <body>
1038
- ${NAV}
1751
+ ${TOPBAR}
1752
+ ${SIDEBAR_DATA}
1753
+ ${SIDEBAR}
1039
1754
  ${TIER_BADGE}
1040
1755
 
1041
- <main class="page">
1042
- <a href="<%= basePath %>/models/<%= model.name %>.html" class="back-link">&larr; Back to <%= model.name %></a>
1756
+ <div class="main">
1757
+ <div class="breadcrumb">
1758
+ <a href="<%= basePath %>/index.html">Home</a>
1759
+ <span class="breadcrumb-sep">/</span>
1760
+ <a href="<%= basePath %>/models/<%= model.name %>.html"><%= model.name %></a>
1761
+ <span class="breadcrumb-sep">/</span>
1762
+ <span>Schema</span>
1763
+ </div>
1043
1764
 
1044
- <div style="display:flex;align-items:center;gap:0.75rem;margin-bottom:2rem;">
1045
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(1.8rem,4vw,2.8rem);color:var(--text);"><%= model.name %></h1>
1046
- <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>
1047
- <span style="font-size:0.85rem;color:var(--text-dim);">Schema Browser</span>
1765
+ <div class="page-header">
1766
+ <div style="display:flex;align-items:center;gap:0.6rem;">
1767
+ <h1><%= model.name %></h1>
1768
+ <% if (tier) { %><%- tierBadge(tier.tier) %><% } %>
1769
+ <span style="font-size:0.8rem;color:var(--text-dim);">Schema Browser</span>
1770
+ </div>
1048
1771
  </div>
1049
1772
 
1050
1773
  <% if (model.datasets && model.datasets.length > 0) { %>
1051
1774
  <% for (var i = 0; i < model.datasets.length; i++) { var ds = model.datasets[i]; %>
1052
1775
  <% var dsGov = gov && gov.datasets && gov.datasets[ds.name]; %>
1053
- <div class="section reveal">
1776
+ <div class="section">
1054
1777
  <div class="card" style="padding:0;overflow:hidden;">
1055
- <div style="padding:1.25rem 1.5rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.5rem;">
1056
- <div style="display:flex;align-items:center;gap:0.6rem;">
1057
- <span class="mono" style="font-size:1rem;color:var(--text);"><%= ds.name %></span>
1778
+ <div style="padding:1rem 1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:0.4rem;">
1779
+ <div style="display:flex;align-items:center;gap:0.5rem;">
1780
+ <span class="mono" style="font-size:0.9rem;color:var(--text);"><%= ds.name %></span>
1058
1781
  <% if (dsGov && dsGov.table_type) { %>
1059
- <span class="ds-type ds-type-<%= dsGov.table_type %>"><%= dsGov.table_type %></span>
1782
+ <span class="tag ds-<%= dsGov.table_type %>"><%= dsGov.table_type %></span>
1060
1783
  <% } %>
1061
1784
  </div>
1062
- <div style="display:flex;gap:0.75rem;flex-wrap:wrap;">
1785
+ <div style="display:flex;gap:0.5rem;flex-wrap:wrap;">
1063
1786
  <% if (dsGov && dsGov.grain) { %><span class="tag"><%= dsGov.grain %></span><% } %>
1064
1787
  <% if (dsGov && dsGov.refresh) { %><span class="tag"><%= dsGov.refresh %></span><% } %>
1065
1788
  <% if (dsGov && dsGov.security) { %><span class="tag"><%= dsGov.security %></span><% } %>
1066
1789
  </div>
1067
1790
  </div>
1068
- <div style="padding:0 1.5rem 0.5rem;font-size:0.75rem;color:var(--text-dim);font-family:var(--mono);padding-top:0.75rem;">
1791
+ <div style="padding:0.5rem 1.25rem;font-size:0.72rem;color:var(--text-dim);font-family:var(--mono);">
1069
1792
  Source: <%= ds.source %>
1070
1793
  </div>
1071
- <% if (ds.description) { %>
1072
- <div style="padding:0 1.5rem 1rem;font-size:0.85rem;color:var(--text-muted);font-weight:300;"><%= ds.description %></div>
1794
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1795
+ <div style="padding:0 1.25rem 0.75rem;font-size:0.82rem;color:var(--text-secondary);font-weight:300;"><span class="editable" data-file="context/models/<%= model.name %>.osi.yaml" data-path="semantic_model.0.datasets.<%= i %>.description" data-label="<%= ds.name %> description"><%= ds.description || 'Add description' %></span></div>
1796
+ <% } else if (ds.description) { %>
1797
+ <div style="padding:0 1.25rem 0.75rem;font-size:0.82rem;color:var(--text-secondary);font-weight:300;"><%= ds.description %></div>
1073
1798
  <% } %>
1074
1799
 
1075
1800
  <% if (ds.fields && ds.fields.length > 0) { %>
1076
- <table class="table-dark">
1801
+ <table class="data-table">
1077
1802
  <thead>
1078
- <tr>
1079
- <th>Field</th>
1080
- <th>Description</th>
1081
- <th>Semantic Role</th>
1082
- <th>Aggregation</th>
1083
- </tr>
1803
+ <tr><th>Field</th><th>Description</th><th>Semantic Role</th><th>Aggregation</th></tr>
1084
1804
  </thead>
1085
1805
  <tbody>
1086
- <% for (var field of ds.fields) { %>
1806
+ <% for (var j = 0; j < ds.fields.length; j++) { var field = ds.fields[j]; %>
1087
1807
  <% var fieldKey = ds.name + '.' + field.name; %>
1088
1808
  <% var fGov = gov && gov.fields && gov.fields[fieldKey]; %>
1089
1809
  <% var role = fGov && fGov.semantic_role ? fGov.semantic_role : ''; %>
1090
1810
  <tr>
1091
- <td class="mono" style="font-size:0.78rem;"><%= field.name %></td>
1092
- <td style="color:var(--text-muted);font-size:0.82rem;"><%= field.description || '' %></td>
1093
- <td><% if (role) { %><span class="tag role-<%= role %>"><%= role %></span><% } %></td>
1094
- <td style="font-family:var(--mono);font-size:0.78rem;color:var(--text-dim);"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %></td>
1811
+ <td class="mono" style="font-size:0.75rem;"><%= field.name %></td>
1812
+ <td style="color:var(--text-secondary);font-size:0.8rem;">
1813
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1814
+ <span class="editable" data-file="context/models/<%= model.name %>.osi.yaml" data-path="semantic_model.0.datasets.<%= i %>.fields.<%= j %>.description" data-label="<%= field.name %> description"><%= field.description || 'Add description' %></span>
1815
+ <% } else { %>
1816
+ <%= field.description || '' %>
1817
+ <% } %>
1818
+ </td>
1819
+ <td>
1820
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1821
+ <span class="dropdown-editable" data-file="context/governance/<%= model.name %>.governance.yaml" data-path="datasets.<%= ds.name %>.fields.<%= field.name %>.semantic_role" data-options="identifier,metric,dimension,date,attribute" data-label="<%= field.name %> semantic role"><%= role || 'Select role' %></span>
1822
+ <% } else { %>
1823
+ <% if (role) { %><span class="tag role-<%= role %>"><%= role %></span><% } %>
1824
+ <% } %>
1825
+ </td>
1826
+ <td class="mono" style="font-size:0.75rem;color:var(--text-dim);">
1827
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1828
+ <span class="dropdown-editable" data-file="context/governance/<%= model.name %>.governance.yaml" data-path="datasets.<%= ds.name %>.fields.<%= field.name %>.default_aggregation" data-options="SUM,AVG,COUNT,MIN,MAX,NONE" data-label="<%= field.name %> aggregation"><%= fGov && fGov.default_aggregation ? fGov.default_aggregation : 'Select' %></span>
1829
+ <% } else { %>
1830
+ <%= fGov && fGov.default_aggregation ? fGov.default_aggregation : '' %>
1831
+ <% } %>
1832
+ </td>
1095
1833
  </tr>
1096
1834
  <% } %>
1097
1835
  </tbody>
1098
1836
  </table>
1099
1837
  <% } else { %>
1100
- <p style="padding:1.5rem;color:var(--text-dim);font-size:0.85rem;">No fields defined.</p>
1838
+ <p style="padding:1.25rem;color:var(--text-dim);font-size:0.82rem;">No fields defined.</p>
1101
1839
  <% } %>
1102
1840
  </div>
1103
1841
  </div>
1104
1842
  <% } %>
1105
1843
  <% } else { %>
1106
- <p style="color:var(--text-muted);">No datasets found.</p>
1844
+ <p style="color:var(--text-secondary);">No datasets found.</p>
1107
1845
  <% } %>
1108
- </main>
1846
+ </div>
1109
1847
 
1110
1848
  ${FOOTER}
1111
1849
  ${SCRIPTS}
@@ -1115,22 +1853,33 @@ ${SCRIPTS}
1115
1853
  // src/templates/rules.ts
1116
1854
  var rulesTemplate = `${HEAD}
1117
1855
  <body>
1118
- ${NAV}
1119
-
1120
- <main class="page">
1121
- <a href="<%= basePath %>/models/<%= modelName %>.html" class="back-link">&larr; Back to <%= modelName %></a>
1856
+ ${TOPBAR}
1857
+ ${SIDEBAR_DATA}
1858
+ ${SIDEBAR}
1859
+
1860
+ <div class="main">
1861
+ <div class="breadcrumb">
1862
+ <a href="<%= basePath %>/index.html">Home</a>
1863
+ <span class="breadcrumb-sep">/</span>
1864
+ <a href="<%= basePath %>/models/<%= modelName %>.html"><%= modelName %></a>
1865
+ <span class="breadcrumb-sep">/</span>
1866
+ <span>Rules</span>
1867
+ </div>
1122
1868
 
1123
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(1.8rem,4vw,2.8rem);color:var(--text);margin-bottom:2rem;">
1124
- <%= modelName %> <span style="color:var(--text-dim);font-weight:300;">&mdash; Rules &amp; Queries</span>
1125
- </h1>
1869
+ <div class="page-header">
1870
+ <h1><%= modelName %> <span style="color:var(--text-dim);font-weight:300;">&mdash; Rules &amp; Queries</span></h1>
1871
+ </div>
1126
1872
 
1127
1873
  <% if (rules && rules.golden_queries && rules.golden_queries.length > 0) { %>
1128
- <div class="section reveal">
1874
+ <div class="section">
1129
1875
  <div class="section-label">Golden Queries</div>
1130
1876
  <h2 class="section-title">Pre-validated questions</h2>
1131
1877
  <% for (var gq of rules.golden_queries) { %>
1132
1878
  <div class="query-card">
1133
- <div class="query-question"><%= gq.question %></div>
1879
+ <div class="query-q">
1880
+ <span class="query-q-badge">Q</span>
1881
+ <span><%= gq.question %></span>
1882
+ </div>
1134
1883
  <div class="query-sql sql-highlight"><%= gq.sql %></div>
1135
1884
  <% if (gq.dialect || (gq.tags && gq.tags.length > 0)) { %>
1136
1885
  <div class="query-meta">
@@ -1144,21 +1893,21 @@ ${NAV}
1144
1893
  <% } %>
1145
1894
 
1146
1895
  <% if (rules && rules.business_rules && rules.business_rules.length > 0) { %>
1147
- <div class="section reveal">
1896
+ <div class="section">
1148
1897
  <div class="section-label">Business Rules</div>
1149
1898
  <h2 class="section-title">Business Rules</h2>
1150
- <div style="display:flex;flex-direction:column;gap:1rem;">
1899
+ <div style="display:flex;flex-direction:column;gap:0.75rem;">
1151
1900
  <% for (var br of rules.business_rules) { %>
1152
1901
  <div class="card">
1153
- <div style="font-family:var(--mono);font-size:0.88rem;color:var(--gold-light);margin-bottom:0.4rem;"><%= br.name %></div>
1154
- <p style="font-size:0.85rem;color:var(--text-muted);line-height:1.6;font-weight:300;"><%= br.definition %></p>
1902
+ <div style="font-family:var(--mono);font-size:0.82rem;color:var(--accent-light);margin-bottom:0.3rem;font-weight:500;"><%= br.name %></div>
1903
+ <p style="font-size:0.82rem;color:var(--text-secondary);line-height:1.6;font-weight:300;"><%= br.definition %></p>
1155
1904
  <% if (br.enforcement && br.enforcement.length > 0) { %>
1156
- <div style="display:flex;gap:0.4rem;margin-top:0.6rem;flex-wrap:wrap;">
1905
+ <div style="display:flex;gap:0.3rem;margin-top:0.5rem;flex-wrap:wrap;">
1157
1906
  <% for (var e of br.enforcement) { %><span class="tag tag-green"><%= e %></span><% } %>
1158
1907
  </div>
1159
1908
  <% } %>
1160
1909
  <% if (br.avoid && br.avoid.length > 0) { %>
1161
- <div style="display:flex;gap:0.4rem;margin-top:0.4rem;flex-wrap:wrap;">
1910
+ <div style="display:flex;gap:0.3rem;margin-top:0.3rem;flex-wrap:wrap;">
1162
1911
  <% for (var a of br.avoid) { %><span class="tag tag-red"><%= a %></span><% } %>
1163
1912
  </div>
1164
1913
  <% } %>
@@ -1169,7 +1918,7 @@ ${NAV}
1169
1918
  <% } %>
1170
1919
 
1171
1920
  <% if (rules && rules.guardrail_filters && rules.guardrail_filters.length > 0) { %>
1172
- <div class="section reveal">
1921
+ <div class="section">
1173
1922
  <div class="section-label">Safety</div>
1174
1923
  <h2 class="section-title">Guardrail Filters</h2>
1175
1924
  <% for (var gf of rules.guardrail_filters) { %>
@@ -1178,7 +1927,7 @@ ${NAV}
1178
1927
  <div class="guardrail-filter"><%= gf.filter %></div>
1179
1928
  <div class="guardrail-reason"><%= gf.reason %></div>
1180
1929
  <% if (gf.tables && gf.tables.length > 0) { %>
1181
- <div style="display:flex;gap:0.4rem;margin-top:0.6rem;">
1930
+ <div style="display:flex;gap:0.3rem;margin-top:0.4rem;">
1182
1931
  <% for (var tb of gf.tables) { %><span class="tag"><%= tb %></span><% } %>
1183
1932
  </div>
1184
1933
  <% } %>
@@ -1188,18 +1937,18 @@ ${NAV}
1188
1937
  <% } %>
1189
1938
 
1190
1939
  <% if (rules && rules.hierarchies && rules.hierarchies.length > 0) { %>
1191
- <div class="section reveal">
1940
+ <div class="section">
1192
1941
  <div class="section-label">Drill Paths</div>
1193
1942
  <h2 class="section-title">Hierarchies</h2>
1194
- <div style="display:flex;flex-direction:column;gap:1rem;">
1943
+ <div style="display:flex;flex-direction:column;gap:0.75rem;">
1195
1944
  <% for (var h of rules.hierarchies) { %>
1196
1945
  <div class="card">
1197
- <div style="font-family:var(--mono);font-size:0.88rem;color:var(--gold-light);margin-bottom:0.4rem;"><%= h.name %></div>
1198
- <div style="font-size:0.82rem;color:var(--text-muted);margin-bottom:0.4rem;">Dataset: <span class="mono"><%= h.dataset %></span></div>
1199
- <div style="display:flex;align-items:center;gap:0.5rem;flex-wrap:wrap;">
1946
+ <div style="font-family:var(--mono);font-size:0.82rem;color:var(--accent-light);margin-bottom:0.3rem;font-weight:500;"><%= h.name %></div>
1947
+ <div style="font-size:0.78rem;color:var(--text-secondary);margin-bottom:0.3rem;">Dataset: <span class="mono"><%= h.dataset %></span></div>
1948
+ <div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap;">
1200
1949
  <% for (var li = 0; li < h.levels.length; li++) { %>
1201
1950
  <span class="tag tag-blue"><%= h.levels[li] %></span>
1202
- <% if (li < h.levels.length - 1) { %><span style="color:var(--gold-dark);">&rarr;</span><% } %>
1951
+ <% if (li < h.levels.length - 1) { %><span style="color:var(--text-dim);">&rarr;</span><% } %>
1203
1952
  <% } %>
1204
1953
  </div>
1205
1954
  </div>
@@ -1209,9 +1958,9 @@ ${NAV}
1209
1958
  <% } %>
1210
1959
 
1211
1960
  <% if (!rules || ((!rules.golden_queries || rules.golden_queries.length === 0) && (!rules.business_rules || rules.business_rules.length === 0) && (!rules.guardrail_filters || rules.guardrail_filters.length === 0) && (!rules.hierarchies || rules.hierarchies.length === 0))) { %>
1212
- <p style="color:var(--text-muted);">No rules or queries defined for this model.</p>
1961
+ <p style="color:var(--text-secondary);">No rules or queries defined for this model.</p>
1213
1962
  <% } %>
1214
- </main>
1963
+ </div>
1215
1964
 
1216
1965
  ${FOOTER}
1217
1966
  ${SCRIPTS}
@@ -1221,47 +1970,90 @@ ${SCRIPTS}
1221
1970
  // src/templates/glossary.ts
1222
1971
  var glossaryTemplate = `${HEAD}
1223
1972
  <body>
1224
- ${NAV}
1225
-
1226
- <main class="page">
1227
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:0.5rem;">Glossary</h1>
1228
- <p style="color:var(--text-muted);font-size:1rem;margin-bottom:2rem;font-weight:300;">Business term definitions and mappings.</p>
1973
+ ${TOPBAR}
1974
+ ${SIDEBAR_DATA}
1975
+ ${SIDEBAR}
1976
+
1977
+ <div class="main">
1978
+ <div class="page-header">
1979
+ <h1>Glossary</h1>
1980
+ <p class="subtitle">Business term definitions and mappings.</p>
1981
+ </div>
1229
1982
 
1230
1983
  <% var termIds = Object.keys(terms).sort(); %>
1231
1984
  <% if (termIds.length === 0) { %>
1232
- <p style="color:var(--text-muted);">No terms defined.</p>
1985
+ <p style="color:var(--text-secondary);">No terms defined.</p>
1233
1986
  <% } else { %>
1234
1987
  <input type="text" id="glossary-filter"
1235
1988
  placeholder="Filter terms..."
1236
1989
  class="search-input"
1237
- style="margin-bottom:2rem;max-width:400px;" />
1990
+ style="margin-bottom:1.5rem;max-width:360px;" />
1238
1991
 
1239
1992
  <div class="glossary-grid" id="glossary-grid">
1240
1993
  <% for (var tid of termIds) { %>
1241
1994
  <% var term = terms[tid]; %>
1242
- <div class="glossary-card" id="term-<%= tid %>" data-term="<%= tid %>">
1995
+ <div class="glossary-card card" id="term-<%= tid %>" data-term="<%= tid %>">
1243
1996
  <div class="glossary-term"><%= tid %></div>
1244
- <div class="glossary-def"><%= term.definition %></div>
1245
- <% if (term.synonyms && term.synonyms.length > 0) { %>
1246
- <div style="display:flex;gap:0.4rem;margin-top:0.75rem;flex-wrap:wrap;">
1997
+ <div class="glossary-def">
1998
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
1999
+ <span class="editable" data-file="context/glossary/<%= tid %>.term.yaml" data-path="definition" data-label="<%= tid %> definition"><%= term.definition || 'Add definition' %></span>
2000
+ <% } else { %>
2001
+ <%= term.definition || '' %>
2002
+ <% } %>
2003
+ </div>
2004
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
2005
+ <div style="display:flex;gap:0.3rem;margin-top:0.5rem;flex-wrap:wrap;">
2006
+ <span class="editable" data-file="context/glossary/<%= tid %>.term.yaml" data-path="synonyms" data-label="<%= tid %> synonyms" style="font-size:0.78rem;color:var(--text-dim);"><%= (term.synonyms && term.synonyms.length > 0) ? term.synonyms.join(', ') : 'Add synonyms (comma-separated)' %></span>
2007
+ </div>
2008
+ <% } else if (term.synonyms && term.synonyms.length > 0) { %>
2009
+ <div style="display:flex;gap:0.3rem;margin-top:0.5rem;flex-wrap:wrap;">
1247
2010
  <% for (var s of term.synonyms) { %><span class="tag"><%= s %></span><% } %>
1248
2011
  </div>
1249
2012
  <% } %>
1250
2013
  <% if (term.maps_to && term.maps_to.length > 0) { %>
1251
- <div style="display:flex;gap:0.4rem;margin-top:0.5rem;flex-wrap:wrap;">
2014
+ <div style="display:flex;gap:0.3rem;margin-top:0.35rem;flex-wrap:wrap;">
1252
2015
  <% for (var m of term.maps_to) { %><span class="tag tag-blue"><%= m %></span><% } %>
1253
2016
  </div>
1254
2017
  <% } %>
1255
2018
  <% if (term.owner) { %>
1256
- <div style="font-size:0.78rem;color:var(--text-dim);margin-top:0.6rem;">
2019
+ <div style="font-size:0.72rem;color:var(--text-dim);margin-top:0.4rem;">
1257
2020
  Owner: <a href="<%= basePath %>/owners/<%= term.owner %>.html"><%= term.owner %></a>
1258
2021
  </div>
1259
2022
  <% } %>
1260
2023
  </div>
1261
2024
  <% } %>
1262
2025
  </div>
2026
+
2027
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
2028
+ <div id="add-term-container" style="margin-top:1.5rem;">
2029
+ <button class="studio-add-btn" id="add-term-btn" onclick="document.getElementById('add-term-form').style.display='block';this.style.display='none';">+ Add Term</button>
2030
+ <div id="add-term-form" class="card" style="display:none;padding:1.25rem;margin-top:0.5rem;">
2031
+ <div style="font-size:0.85rem;color:#c9a55a;margin-bottom:1rem;font-weight:500;">New Glossary Term</div>
2032
+ <div style="margin-bottom:0.75rem;">
2033
+ <label style="display:block;font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem;">Term ID (slug)</label>
2034
+ <input type="text" id="new-term-id" placeholder="e.g. churn_rate" style="width:100%;padding:6px 10px;background:var(--bg-card);border:1px solid #333;border-radius:6px;color:var(--text-primary);font-size:0.85rem;" />
2035
+ </div>
2036
+ <div style="margin-bottom:0.75rem;">
2037
+ <label style="display:block;font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem;">Definition</label>
2038
+ <input type="text" id="new-term-def" placeholder="What this term means" style="width:100%;padding:6px 10px;background:var(--bg-card);border:1px solid #333;border-radius:6px;color:var(--text-primary);font-size:0.85rem;" />
2039
+ </div>
2040
+ <div style="margin-bottom:0.75rem;">
2041
+ <label style="display:block;font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem;">Synonyms (comma-separated)</label>
2042
+ <input type="text" id="new-term-synonyms" placeholder="e.g. attrition, turnover" style="width:100%;padding:6px 10px;background:var(--bg-card);border:1px solid #333;border-radius:6px;color:var(--text-primary);font-size:0.85rem;" />
2043
+ </div>
2044
+ <div style="margin-bottom:0.75rem;">
2045
+ <label style="display:block;font-size:0.75rem;color:var(--text-dim);margin-bottom:0.25rem;">Tags (comma-separated)</label>
2046
+ <input type="text" id="new-term-tags" placeholder="e.g. finance, metrics" style="width:100%;padding:6px 10px;background:var(--bg-card);border:1px solid #333;border-radius:6px;color:var(--text-primary);font-size:0.85rem;" />
2047
+ </div>
2048
+ <div style="display:flex;gap:0.5rem;justify-content:flex-end;">
2049
+ <button onclick="document.getElementById('add-term-form').style.display='none';document.getElementById('add-term-btn').style.display='';" style="background:none;border:1px solid #555;color:var(--text-secondary);border-radius:6px;padding:6px 14px;cursor:pointer;font-size:0.82rem;">Cancel</button>
2050
+ <button onclick="submitNewTerm()" style="background:#c9a55a;border:none;color:#000;border-radius:6px;padding:6px 14px;cursor:pointer;font-size:0.82rem;font-weight:500;">Stage Term</button>
2051
+ </div>
2052
+ </div>
2053
+ </div>
2054
+ <% } %>
1263
2055
  <% } %>
1264
- </main>
2056
+ </div>
1265
2057
 
1266
2058
  ${FOOTER}
1267
2059
  ${SCRIPTS}
@@ -1279,48 +2071,96 @@ ${SCRIPTS}
1279
2071
  });
1280
2072
  })();
1281
2073
  </script>
2074
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
2075
+ <script>
2076
+ function submitNewTerm() {
2077
+ var id = document.getElementById('new-term-id').value.trim();
2078
+ var def = document.getElementById('new-term-def').value.trim();
2079
+ var syns = document.getElementById('new-term-synonyms').value.trim();
2080
+ var tags = document.getElementById('new-term-tags').value.trim();
2081
+ if (!id) { alert('Term ID is required.'); return; }
2082
+ var file = 'context/glossary/' + id + '.term.yaml';
2083
+ if (def) stageEdit(file, 'definition', def, id + ' definition');
2084
+ if (syns) {
2085
+ var synList = syns.split(',').map(function(s){ return s.trim(); }).filter(Boolean);
2086
+ stageEdit(file, 'synonyms', synList, id + ' synonyms');
2087
+ }
2088
+ if (tags) {
2089
+ var tagList = tags.split(',').map(function(t){ return t.trim(); }).filter(Boolean);
2090
+ stageEdit(file, 'tags', tagList, id + ' tags');
2091
+ }
2092
+ document.getElementById('add-term-form').style.display = 'none';
2093
+ document.getElementById('add-term-btn').style.display = '';
2094
+ document.getElementById('new-term-id').value = '';
2095
+ document.getElementById('new-term-def').value = '';
2096
+ document.getElementById('new-term-synonyms').value = '';
2097
+ document.getElementById('new-term-tags').value = '';
2098
+ }
2099
+ </script>
2100
+ <% } %>
1282
2101
  </body>
1283
2102
  </html>`;
1284
2103
 
1285
2104
  // src/templates/owner.ts
1286
2105
  var ownerTemplate = `${HEAD}
1287
2106
  <body>
1288
- ${NAV}
2107
+ ${TOPBAR}
2108
+ ${SIDEBAR_DATA}
2109
+ ${SIDEBAR}
1289
2110
  ${TIER_BADGE}
1290
2111
 
1291
- <main class="page">
1292
- <a href="<%= basePath %>/" class="back-link">&larr; All Models</a>
2112
+ <div class="main">
2113
+ <div class="breadcrumb">
2114
+ <a href="<%= basePath %>/index.html">Home</a>
2115
+ <span class="breadcrumb-sep">/</span>
2116
+ <span><%= owner.display_name %></span>
2117
+ </div>
1293
2118
 
1294
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:0.5rem;"><%= owner.display_name %></h1>
2119
+ <div class="page-header">
2120
+ <h1>
2121
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
2122
+ <span class="editable" data-file="context/owners/<%= owner.id %>.owner.yaml" data-path="display_name" data-label="<%= owner.id %> display name"><%= owner.display_name || 'Add display name' %></span>
2123
+ <% } else { %>
2124
+ <%= owner.display_name %>
2125
+ <% } %>
2126
+ </h1>
2127
+ </div>
1295
2128
 
1296
- <div style="display:flex;gap:1.5rem;font-size:0.85rem;color:var(--text-dim);margin-bottom:1.5rem;">
1297
- <% if (owner.email) { %><span>Email: <span style="color:var(--text-muted);"><%= owner.email %></span></span><% } %>
1298
- <% if (owner.team) { %><span>Team: <span style="color:var(--text-muted);"><%= owner.team %></span></span><% } %>
2129
+ <div style="display:flex;gap:1.25rem;font-size:0.82rem;color:var(--text-dim);margin-bottom:1.5rem;">
2130
+ <% if (typeof studioMode !== 'undefined' && studioMode) { %>
2131
+ <span>Email: <span class="editable" data-file="context/owners/<%= owner.id %>.owner.yaml" data-path="email" data-label="<%= owner.id %> email" style="color:var(--text-secondary);"><%= owner.email || 'Add email' %></span></span>
2132
+ <span>Team: <span class="editable" data-file="context/owners/<%= owner.id %>.owner.yaml" data-path="team" data-label="<%= owner.id %> team" style="color:var(--text-secondary);"><%= owner.team || 'Add team' %></span></span>
2133
+ <% } else { %>
2134
+ <% if (owner.email) { %><span>Email: <span style="color:var(--text-secondary);"><%= owner.email %></span></span><% } %>
2135
+ <% if (owner.team) { %><span>Team: <span style="color:var(--text-secondary);"><%= owner.team %></span></span><% } %>
2136
+ <% } %>
1299
2137
  </div>
1300
2138
 
1301
2139
  <% if (owner.description) { %>
1302
- <p style="color:var(--text-muted);font-size:0.95rem;margin-bottom:2rem;font-weight:300;max-width:600px;"><%= owner.description %></p>
2140
+ <p style="color:var(--text-secondary);font-size:0.9rem;margin-bottom:2rem;font-weight:300;max-width:560px;"><%= owner.description %></p>
1303
2141
  <% } %>
1304
2142
 
1305
2143
  <% if (governedModels.length > 0) { %>
1306
- <div class="section reveal">
2144
+ <div class="section">
1307
2145
  <div class="section-label">Stewardship</div>
1308
2146
  <h2 class="section-title">Governed Models</h2>
1309
- <div class="card-grid-sm">
2147
+ <div class="card-grid">
1310
2148
  <% for (var gm of governedModels) { %>
1311
- <a href="<%= basePath %>/models/<%= gm.name %>.html" class="card" style="text-decoration:none;">
1312
- <div style="display:flex;align-items:center;gap:0.5rem;">
1313
- <span class="mono" style="color:var(--gold-light);"><%= gm.name %></span>
1314
- <% if (gm.tier) { %><%- tierBadge(gm.tier) %><% } %>
2149
+ <a href="<%= basePath %>/models/<%= gm.name %>.html" class="card-link">
2150
+ <div class="card">
2151
+ <div style="display:flex;align-items:center;gap:0.4rem;">
2152
+ <span class="mono" style="font-size:0.85rem;color:var(--accent-light);"><%= gm.name %></span>
2153
+ <% if (gm.tier) { %><%- tierBadge(gm.tier) %><% } %>
2154
+ </div>
1315
2155
  </div>
1316
2156
  </a>
1317
2157
  <% } %>
1318
2158
  </div>
1319
2159
  </div>
1320
2160
  <% } else { %>
1321
- <p style="color:var(--text-muted);">No models governed by this owner.</p>
2161
+ <p style="color:var(--text-secondary);">No models governed by this owner.</p>
1322
2162
  <% } %>
1323
- </main>
2163
+ </div>
1324
2164
 
1325
2165
  ${FOOTER}
1326
2166
  ${SCRIPTS}
@@ -1330,18 +2170,22 @@ ${SCRIPTS}
1330
2170
  // src/templates/search.ts
1331
2171
  var searchTemplate = `${HEAD}
1332
2172
  <body>
1333
- ${NAV}
2173
+ ${TOPBAR}
2174
+ ${SIDEBAR_DATA}
2175
+ ${SIDEBAR}
1334
2176
 
1335
- <main class="page">
1336
- <h1 style="font-family:var(--serif);font-weight:300;font-size:clamp(2rem,5vw,3.5rem);color:var(--text);margin-bottom:2rem;">Search</h1>
2177
+ <div class="main">
2178
+ <div class="page-header">
2179
+ <h1>Search</h1>
2180
+ </div>
1337
2181
 
1338
2182
  <input type="text" id="search-input"
1339
- placeholder="Search models, datasets, terms..."
2183
+ placeholder="Search models, datasets, fields, terms..."
1340
2184
  class="search-input"
1341
- style="margin-bottom:2rem;" />
2185
+ style="margin-bottom:1.5rem;" />
1342
2186
 
1343
- <div id="search-results" style="display:flex;flex-direction:column;gap:0.75rem;"></div>
1344
- </main>
2187
+ <div id="search-results" style="display:flex;flex-direction:column;gap:0.5rem;"></div>
2188
+ </div>
1345
2189
 
1346
2190
  ${FOOTER}
1347
2191
  ${SCRIPTS}
@@ -1372,18 +2216,18 @@ ${SCRIPTS}
1372
2216
  function createResultElement(doc) {
1373
2217
  var wrapper = document.createElement('div');
1374
2218
  wrapper.className = 'card';
1375
- wrapper.style.padding = '1rem 1.25rem';
2219
+ wrapper.style.padding = '0.75rem 1rem';
1376
2220
 
1377
2221
  var top = document.createElement('div');
1378
2222
  top.style.display = 'flex';
1379
2223
  top.style.alignItems = 'center';
1380
- top.style.gap = '0.5rem';
2224
+ top.style.gap = '0.4rem';
1381
2225
 
1382
2226
  var link = document.createElement('a');
1383
2227
  link.href = doc.url;
1384
2228
  link.style.fontFamily = 'var(--mono)';
1385
- link.style.fontSize = '0.88rem';
1386
- link.style.color = 'var(--gold-light)';
2229
+ link.style.fontSize = '0.82rem';
2230
+ link.style.color = 'var(--accent-light)';
1387
2231
  link.textContent = doc.title;
1388
2232
  top.appendChild(link);
1389
2233
 
@@ -1396,9 +2240,9 @@ ${SCRIPTS}
1396
2240
 
1397
2241
  if (doc.description) {
1398
2242
  var desc = document.createElement('p');
1399
- desc.style.fontSize = '0.82rem';
1400
- desc.style.color = 'var(--text-muted)';
1401
- desc.style.marginTop = '0.3rem';
2243
+ desc.style.fontSize = '0.78rem';
2244
+ desc.style.color = 'var(--text-secondary)';
2245
+ desc.style.marginTop = '0.2rem';
1402
2246
  desc.style.fontWeight = '300';
1403
2247
  desc.textContent = doc.description;
1404
2248
  wrapper.appendChild(desc);
@@ -1414,7 +2258,7 @@ ${SCRIPTS}
1414
2258
  var hits = miniSearch.search(query, { prefix: true, fuzzy: 0.2 });
1415
2259
  if (hits.length === 0) {
1416
2260
  var noResults = document.createElement('p');
1417
- noResults.style.color = 'var(--text-muted)';
2261
+ noResults.style.color = 'var(--text-secondary)';
1418
2262
  noResults.textContent = 'No results found.';
1419
2263
  resultsContainer.appendChild(noResults);
1420
2264
  return;
@@ -1493,22 +2337,24 @@ function buildSearchIndex(manifest, basePath) {
1493
2337
  }
1494
2338
 
1495
2339
  // src/generator.ts
1496
- function generateSite(manifest, config) {
2340
+ function generateSite(manifest, config, options) {
1497
2341
  const files = /* @__PURE__ */ new Map();
1498
2342
  const siteTitle = _nullishCoalesce(_optionalChain([config, 'optionalAccess', _ => _.title]), () => ( "ContextKit"));
1499
2343
  const basePath = (_nullishCoalesce(_optionalChain([config, 'optionalAccess', _2 => _2.base_path]), () => ( ""))).replace(/\/+$/, "") || ".";
2344
+ const studioMode = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _3 => _3.studioMode]), () => ( false));
1500
2345
  const commonData = {
1501
2346
  siteTitle,
1502
- basePath
2347
+ basePath,
2348
+ studioMode,
2349
+ models: manifest.models,
2350
+ tiers: manifest.tiers
1503
2351
  };
1504
2352
  files.set(
1505
2353
  "index.html",
1506
2354
  _ejs2.default.render(indexTemplate, {
1507
2355
  ...commonData,
1508
2356
  pageTitle: "Home",
1509
- models: manifest.models,
1510
2357
  governance: manifest.governance,
1511
- tiers: manifest.tiers,
1512
2358
  owners: manifest.owners,
1513
2359
  terms: manifest.terms
1514
2360
  })
@@ -1565,7 +2411,7 @@ function generateSite(manifest, config) {
1565
2411
  const tierScore = manifest.tiers[modelName];
1566
2412
  governedModels.push({
1567
2413
  name: modelName,
1568
- tier: _nullishCoalesce(_optionalChain([tierScore, 'optionalAccess', _3 => _3.tier]), () => ( null))
2414
+ tier: _nullishCoalesce(_optionalChain([tierScore, 'optionalAccess', _4 => _4.tier]), () => ( null))
1569
2415
  });
1570
2416
  }
1571
2417
  }