kpi_assembler 0.5.0

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.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +83 -0
  4. data/app/controllers/kpi_assembler/api/v1/workspace_controller.rb +67 -0
  5. data/app/controllers/kpi_assembler/application_controller.rb +19 -0
  6. data/app/controllers/kpi_assembler/workspace_controller.rb +17 -0
  7. data/bin/kpi_assembler +77 -0
  8. data/bin/kpi_assembler_web +19 -0
  9. data/config/routes.rb +17 -0
  10. data/docs/integration.md +89 -0
  11. data/docs/rails-engine.md +92 -0
  12. data/lib/generators/kpi_assembler/install_generator.rb +25 -0
  13. data/lib/generators/kpi_assembler/templates/kpi_assembler.rb +47 -0
  14. data/lib/kpi_assembler/asset_server.rb +42 -0
  15. data/lib/kpi_assembler/briefing_generator.rb +108 -0
  16. data/lib/kpi_assembler/candidate_generator.rb +421 -0
  17. data/lib/kpi_assembler/certification_engine.rb +125 -0
  18. data/lib/kpi_assembler/configuration.rb +115 -0
  19. data/lib/kpi_assembler/connection.rb +140 -0
  20. data/lib/kpi_assembler/engine.rb +18 -0
  21. data/lib/kpi_assembler/env.rb +40 -0
  22. data/lib/kpi_assembler/html_report.rb +177 -0
  23. data/lib/kpi_assembler/location_directory.rb +32 -0
  24. data/lib/kpi_assembler/metric_evaluator.rb +62 -0
  25. data/lib/kpi_assembler/pack_builder.rb +53 -0
  26. data/lib/kpi_assembler/sample_database.rb +191 -0
  27. data/lib/kpi_assembler/schema_inspector.rb +197 -0
  28. data/lib/kpi_assembler/schema_kpi_proposer.rb +323 -0
  29. data/lib/kpi_assembler/version.rb +5 -0
  30. data/lib/kpi_assembler/web_app.rb +141 -0
  31. data/lib/kpi_assembler/web_service.rb +138 -0
  32. data/lib/kpi_assembler.rb +162 -0
  33. data/public/app.css +458 -0
  34. data/public/app.js +372 -0
  35. data/public/index.html +240 -0
  36. data/public/kpi-assembler.js +75 -0
  37. metadata +101 -0
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "kpi_assembler/version"
5
+ require_relative "kpi_assembler/env"
6
+
7
+ KPIAssembler::Env.load_defaults!
8
+
9
+ module KPIAssembler
10
+ class Error < StandardError; end
11
+
12
+ autoload :SampleDatabase, File.expand_path("kpi_assembler/sample_database", __dir__)
13
+ end
14
+
15
+ require_relative "kpi_assembler/connection"
16
+ require_relative "kpi_assembler/schema_inspector"
17
+ require_relative "kpi_assembler/schema_kpi_proposer"
18
+ require_relative "kpi_assembler/candidate_generator"
19
+ require_relative "kpi_assembler/certification_engine"
20
+ require_relative "kpi_assembler/metric_evaluator"
21
+ require_relative "kpi_assembler/location_directory"
22
+ require_relative "kpi_assembler/briefing_generator"
23
+ require_relative "kpi_assembler/pack_builder"
24
+ require_relative "kpi_assembler/html_report"
25
+ require_relative "kpi_assembler/web_service"
26
+ require_relative "kpi_assembler/asset_server"
27
+ require_relative "kpi_assembler/configuration"
28
+
29
+ module KPIAssembler
30
+ DEMO_ACCEPT = %w[
31
+ lead_conversion_rate
32
+ tour_show_rate
33
+ paid_mrr
34
+ avg_revenue_per_member
35
+ revenue_per_lead_unsafe
36
+ ].freeze
37
+
38
+ class Pipeline
39
+ attr_reader :db_connection, :options, :now
40
+
41
+ def initialize(db_connection:, options: {})
42
+ @db_connection = Connection.wrap(db_connection)
43
+ @options = options
44
+ @now = options[:now] || Time.now.utc
45
+ end
46
+
47
+ def run
48
+ puts "==> [1/6] Discover — introspect schema and profile tables..."
49
+ schema = SchemaInspector.new(db_connection, inspector_options).introspect
50
+ schema.each do |name, meta|
51
+ puts " #{name}: #{meta[:row_count]} rows, FKs=#{meta[:foreign_keys].size}"
52
+ end
53
+
54
+ puts "==> [2/6] Propose — candidate KPI recipes from this schema..."
55
+ generator = CandidateGenerator.new(schema, generator_options)
56
+ candidates = generator.generate
57
+ puts " proposer: #{generator.meta[:proposer]}" +
58
+ (generator.meta[:fallback_reason] ? " (#{generator.meta[:fallback_reason]})" : "")
59
+ unless generator.meta[:ranked_tables].empty?
60
+ puts " ranked tables: #{generator.meta[:ranked_tables].join(", ")}"
61
+ end
62
+ candidates.each { |kpi| puts " • #{kpi[:id]} — #{kpi[:name]} [#{kpi[:source]}]" }
63
+
64
+ puts "==> [3/6] Accept — human gate on catalog entries..."
65
+ accepted = apply_human_gate(candidates)
66
+ puts " Accepted #{accepted.size} of #{candidates.size} candidates."
67
+
68
+ puts "==> [4/6] Certify — deterministic replay (LLM never certifies)..."
69
+ certified, drafts = CertificationEngine.new(
70
+ db_connection,
71
+ schema,
72
+ now: now,
73
+ tenant_column: options[:tenant_column],
74
+ tenant_id: options[:tenant_id]
75
+ ).certify(accepted)
76
+ puts " Certified: #{certified.size} | Draft (failed): #{drafts.size}"
77
+ drafts.each { |kpi| puts " ✗ #{kpi[:id]}: #{Array(kpi[:reasons]).join("; ")}" }
78
+
79
+ puts "==> [5/6] Evaluate role views (exec + site)..."
80
+ evaluations = evaluate_views(certified)
81
+
82
+ puts "==> [6/6] Publish pack + briefing..."
83
+ briefing = BriefingGenerator.new(
84
+ db_connection,
85
+ certified,
86
+ now: now,
87
+ tenant_id: options[:tenant_id]
88
+ ).generate
89
+ puts " #{briefing[:headline]}"
90
+
91
+ PackBuilder.new(
92
+ certified,
93
+ drafts: drafts,
94
+ briefing: briefing,
95
+ evaluations: evaluations,
96
+ schema: schema
97
+ ).build
98
+ end
99
+
100
+ def write_outputs!(pack, directory)
101
+ require "fileutils"
102
+ FileUtils.mkdir_p(directory)
103
+ json_path = File.join(directory, "kpi_pack.json")
104
+ html_path = File.join(directory, "kpi_pack.html")
105
+ File.write(json_path, JSON.pretty_generate(pack))
106
+ html = HtmlReport.new(pack, locations: load_locations).render
107
+ File.write(html_path, html)
108
+ [json_path, html_path]
109
+ end
110
+
111
+ private
112
+
113
+ def inspector_options
114
+ {
115
+ include_tables: Array(options[:include_tables]),
116
+ max_tables: options[:max_tables],
117
+ schema_name: options[:schema_name]
118
+ }
119
+ end
120
+
121
+ def generator_options
122
+ options.merge(dialect: db_connection.dialect)
123
+ end
124
+
125
+ def apply_human_gate(candidates)
126
+ ids = Array(options[:accept_ids]).map(&:to_s)
127
+ if ids.empty? && options[:demo] != false
128
+ demo_ids = candidates.map { |kpi| kpi[:id].to_s } & DEMO_ACCEPT
129
+ ids = demo_ids.empty? ? candidates.map { |kpi| kpi[:id].to_s } : demo_ids
130
+ end
131
+ return candidates if options[:accept_all] || ids.empty?
132
+
133
+ selected = candidates.select { |kpi| ids.include?(kpi[:id].to_s) }
134
+ missing = ids - selected.map { |kpi| kpi[:id].to_s }
135
+ warn " Unknown accept ids: #{missing.join(", ")}" unless missing.empty?
136
+ selected
137
+ end
138
+
139
+ def evaluate_views(certified)
140
+ evaluator = MetricEvaluator.new(db_connection)
141
+ from = now - (30 * 86_400)
142
+ to = now
143
+ result = { "all" => values_for(evaluator, certified, from, to, nil) }
144
+ load_locations.each do |loc|
145
+ result[loc[:id].to_s] = values_for(evaluator, certified, from, to, loc[:id])
146
+ end
147
+ result
148
+ end
149
+
150
+ def values_for(evaluator, certified, from, to, location_id)
151
+ certified.each_with_object({}) do |kpi, acc|
152
+ acc[kpi[:id]] = evaluator.value(kpi, from: from, to: to, location_id: location_id)
153
+ end
154
+ end
155
+
156
+ def load_locations
157
+ LocationDirectory.new(db_connection, tenant_id: options[:tenant_id]).all
158
+ end
159
+ end
160
+ end
161
+
162
+ require_relative "kpi_assembler/engine" if defined?(Rails::Engine)
data/public/app.css ADDED
@@ -0,0 +1,458 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=Manrope:wght@400;500;600;700;800&display=swap");
2
+
3
+ :root {
4
+ --ink: #15162c;
5
+ --muted: #70748a;
6
+ --soft: #f4f5f9;
7
+ --line: #e4e5ec;
8
+ --panel: #ffffff;
9
+ --navy: #191a36;
10
+ --violet: #6854e8;
11
+ --violet-dark: #4f3cd1;
12
+ --violet-soft: #f0edff;
13
+ --green: #1a9b69;
14
+ --green-soft: #e8f7f1;
15
+ --amber: #c57a16;
16
+ --amber-soft: #fff5df;
17
+ --red: #c84b57;
18
+ --red-soft: #fff0f1;
19
+ --shadow: 0 18px 50px rgba(34, 31, 76, 0.1);
20
+ }
21
+
22
+ * { box-sizing: border-box; }
23
+
24
+ body {
25
+ margin: 0;
26
+ color: var(--ink);
27
+ background: #fafafd;
28
+ font-family: "Manrope", sans-serif;
29
+ font-size: 14px;
30
+ -webkit-font-smoothing: antialiased;
31
+ }
32
+
33
+ button, input { font: inherit; }
34
+ button { color: inherit; }
35
+ .hidden { display: none !important; }
36
+
37
+ .app-shell {
38
+ display: grid;
39
+ grid-template-columns: 252px minmax(0, 1fr);
40
+ min-height: 100vh;
41
+ }
42
+
43
+ .sidebar {
44
+ position: sticky;
45
+ top: 0;
46
+ display: flex;
47
+ flex-direction: column;
48
+ height: 100vh;
49
+ padding: 28px 18px 22px;
50
+ color: #fff;
51
+ background:
52
+ radial-gradient(circle at 0 80%, rgba(104, 84, 232, .25), transparent 32%),
53
+ var(--navy);
54
+ }
55
+
56
+ .brand {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: 10px;
60
+ padding: 0 10px 34px;
61
+ color: #fff;
62
+ font-size: 18px;
63
+ font-weight: 800;
64
+ letter-spacing: -.5px;
65
+ text-decoration: none;
66
+ }
67
+
68
+ .brand > span:last-child span { color: #a89cff; }
69
+ .brand-mark { display: grid; grid-template-columns: repeat(3, 5px); align-items: end; gap: 3px; width: 22px; height: 22px; }
70
+ .brand-mark span { display: block; border-radius: 2px; background: #8b7af6; }
71
+ .brand-mark span:nth-child(1) { height: 9px; }
72
+ .brand-mark span:nth-child(2) { height: 17px; }
73
+ .brand-mark span:nth-child(3) { height: 13px; }
74
+
75
+ .nav-label {
76
+ margin: 0 10px 10px;
77
+ color: #757993;
78
+ font-size: 10px;
79
+ font-weight: 800;
80
+ letter-spacing: 1.5px;
81
+ text-transform: uppercase;
82
+ }
83
+
84
+ .nav-label.secondary { margin-top: 28px; }
85
+
86
+ .nav-item {
87
+ display: flex;
88
+ align-items: center;
89
+ width: 100%;
90
+ gap: 11px;
91
+ margin: 3px 0;
92
+ padding: 10px;
93
+ border: 0;
94
+ border-radius: 9px;
95
+ color: #a9acbf;
96
+ text-align: left;
97
+ background: transparent;
98
+ cursor: pointer;
99
+ transition: .2s ease;
100
+ }
101
+
102
+ .nav-item:hover, .nav-item.active { color: #fff; background: rgba(255, 255, 255, .075); }
103
+ .nav-item.active .nav-number { color: #fff; background: var(--violet); border-color: var(--violet); }
104
+ .nav-number {
105
+ display: grid;
106
+ place-items: center;
107
+ flex: 0 0 27px;
108
+ height: 27px;
109
+ border: 1px solid #4d5068;
110
+ border-radius: 50%;
111
+ color: #8e91a8;
112
+ font-family: "DM Mono", monospace;
113
+ font-size: 11px;
114
+ }
115
+ .nav-icon { width: 27px; color: #8b7af6; font-family: "DM Mono", monospace; font-size: 12px; text-align: center; }
116
+ .nav-item strong, .nav-item small { display: block; }
117
+ .nav-item strong { font-size: 12px; font-weight: 700; }
118
+ .nav-item small { margin-top: 2px; color: #74788f; font-size: 10px; }
119
+
120
+ .trust-note {
121
+ display: flex;
122
+ align-items: flex-start;
123
+ gap: 10px;
124
+ margin-top: auto;
125
+ padding: 14px;
126
+ border: 1px solid rgba(139, 122, 246, .25);
127
+ border-radius: 10px;
128
+ background: rgba(104, 84, 232, .08);
129
+ }
130
+ .trust-note strong, .trust-note small { display: block; }
131
+ .trust-note strong { color: #d8d9e6; font-size: 11px; }
132
+ .trust-note small { margin-top: 4px; color: #7f839b; font-size: 10px; line-height: 1.5; }
133
+ .shield {
134
+ display: inline-grid;
135
+ place-items: center;
136
+ flex: 0 0 22px;
137
+ height: 22px;
138
+ border-radius: 7px 7px 10px 10px;
139
+ color: #fff;
140
+ background: var(--violet);
141
+ font-size: 11px;
142
+ }
143
+ .shield.large { width: 31px; height: 31px; font-size: 14px; }
144
+
145
+ .page { min-width: 0; }
146
+ .topbar {
147
+ display: flex;
148
+ align-items: center;
149
+ justify-content: space-between;
150
+ height: 82px;
151
+ padding: 0 42px;
152
+ border-bottom: 1px solid var(--line);
153
+ background: rgba(255, 255, 255, .88);
154
+ backdrop-filter: blur(14px);
155
+ }
156
+ .topbar h1 { margin: 3px 0 0; font-size: 17px; letter-spacing: -.3px; }
157
+ .eyebrow { margin: 0; color: #999cab; font-size: 10px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; }
158
+ .top-actions { display: flex; align-items: center; gap: 16px; }
159
+ .api-status { color: #7f8293; font-size: 11px; }
160
+ .api-status i { display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: #a4a6b0; }
161
+ .api-status.online i { background: #30b77d; box-shadow: 0 0 0 4px #e6f7ef; }
162
+ .api-status.error i { background: var(--red); }
163
+
164
+ main { max-width: 1260px; margin: 0 auto; padding: 0 42px 70px; }
165
+ .view { display: none; }
166
+ .view.active { display: block; animation: reveal .3s ease; }
167
+ @keyframes reveal { from { opacity: 0; transform: translateY(4px); } }
168
+
169
+ .hero {
170
+ display: grid;
171
+ grid-template-columns: 1.05fr .95fr;
172
+ align-items: center;
173
+ min-height: 480px;
174
+ gap: 58px;
175
+ padding: 62px 0;
176
+ }
177
+ .kicker, .step-tag {
178
+ display: inline-flex;
179
+ color: var(--violet);
180
+ font-size: 10px;
181
+ font-weight: 800;
182
+ letter-spacing: 1.35px;
183
+ text-transform: uppercase;
184
+ }
185
+ .hero h2, .integration-hero h2 {
186
+ margin: 18px 0;
187
+ font-size: clamp(36px, 4.2vw, 58px);
188
+ line-height: 1.06;
189
+ letter-spacing: -2.8px;
190
+ }
191
+ .hero h2 em { color: var(--violet); font-style: normal; }
192
+ .hero > div:first-child > p, .integration-hero > p {
193
+ max-width: 590px;
194
+ margin: 0;
195
+ color: var(--muted);
196
+ font-size: 15px;
197
+ line-height: 1.75;
198
+ }
199
+ .hero-actions { display: flex; align-items: center; gap: 20px; margin-top: 30px; }
200
+ .privacy { color: #9295a5; font-size: 10px; }
201
+ .privacy span { color: var(--green); font-size: 8px; }
202
+
203
+ .button {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: 9px;
208
+ min-height: 42px;
209
+ padding: 0 19px;
210
+ border: 0;
211
+ border-radius: 8px;
212
+ font-size: 11px;
213
+ font-weight: 800;
214
+ cursor: pointer;
215
+ transition: .2s ease;
216
+ }
217
+ .button.primary { color: #fff; background: var(--violet); box-shadow: 0 8px 20px rgba(104, 84, 232, .2); }
218
+ .button.primary:hover { background: var(--violet-dark); transform: translateY(-1px); }
219
+ .button.primary:disabled { opacity: .5; cursor: not-allowed; transform: none; }
220
+ .button.ghost { border: 1px solid var(--line); color: #65687a; background: #fff; box-shadow: none; }
221
+ .button.ghost:hover { border-color: #c9c7e9; color: var(--violet); }
222
+ .button.compact { min-height: 35px; padding: 0 14px; }
223
+ .button-icon { font-size: 17px; font-weight: 400; }
224
+
225
+ .pipeline-visual {
226
+ position: relative;
227
+ display: grid;
228
+ grid-template-columns: 1fr 42px 1fr 42px 1fr;
229
+ align-items: center;
230
+ min-height: 250px;
231
+ padding: 24px;
232
+ border: 1px solid #ececf2;
233
+ border-radius: 18px;
234
+ background:
235
+ linear-gradient(rgba(104, 84, 232, .04) 1px, transparent 1px),
236
+ linear-gradient(90deg, rgba(104, 84, 232, .04) 1px, transparent 1px),
237
+ #fff;
238
+ background-size: 22px 22px;
239
+ box-shadow: var(--shadow);
240
+ }
241
+ .pipeline-node {
242
+ display: flex;
243
+ flex-direction: column;
244
+ align-items: center;
245
+ justify-content: center;
246
+ min-height: 114px;
247
+ padding: 15px 8px;
248
+ border: 1px solid var(--line);
249
+ border-radius: 12px;
250
+ text-align: center;
251
+ background: #fff;
252
+ }
253
+ .pipeline-node.model { border-color: #d9d3ff; background: #f9f7ff; }
254
+ .pipeline-node.certified { border-color: #c8eadc; background: #f7fcfa; }
255
+ .node-icon { display: grid; place-items: center; width: 34px; height: 34px; margin-bottom: 9px; border-radius: 9px; color: var(--violet); background: var(--violet-soft); font-size: 16px; }
256
+ .certified .node-icon { color: var(--green); background: var(--green-soft); }
257
+ .pipeline-node strong { font-size: 10px; }
258
+ .pipeline-node small { margin-top: 4px; color: #9698a7; font-size: 8px; }
259
+ .pipeline-line { height: 1px; background: #dcdce6; }
260
+ .pipeline-line span { display: block; width: 5px; height: 5px; margin: -2px auto 0; border-radius: 50%; background: var(--violet); animation: pulse 1.8s infinite; }
261
+ @keyframes pulse { 50% { box-shadow: 0 0 0 5px rgba(104, 84, 232, .12); } }
262
+
263
+ .results { padding-bottom: 40px; }
264
+ .section-heading, .dashboard-heading, .catalog-heading {
265
+ display: flex;
266
+ align-items: flex-end;
267
+ justify-content: space-between;
268
+ gap: 24px;
269
+ margin: 4px 0 20px;
270
+ }
271
+ .section-heading h2, .dashboard-heading h2 { margin: 8px 0 3px; font-size: 24px; letter-spacing: -.8px; }
272
+ .section-heading p, .dashboard-heading p, .catalog-heading p { margin: 0; color: var(--muted); font-size: 11px; }
273
+ .step-tag { padding: 5px 8px; border-radius: 5px; background: var(--violet-soft); }
274
+ .step-tag.violet { color: #7d62db; }
275
+ .step-tag.green { color: var(--green); background: var(--green-soft); }
276
+
277
+ .discovery-notice {
278
+ margin-bottom: 18px;
279
+ padding: 13px 16px;
280
+ border: 1px solid rgba(233, 165, 71, 0.42);
281
+ border-left-width: 3px;
282
+ border-radius: 8px;
283
+ background: rgba(233, 165, 71, 0.09);
284
+ color: #e2b479;
285
+ font-size: 13px;
286
+ line-height: 1.55;
287
+ }
288
+ .discovery-notice strong { color: #f4d3a6; }
289
+ .discovery-notice code {
290
+ padding: 1px 5px;
291
+ border-radius: 4px;
292
+ background: rgba(0, 0, 0, 0.28);
293
+ font-size: 12px;
294
+ }
295
+
296
+ .schema-grid {
297
+ display: grid;
298
+ grid-template-columns: repeat(5, minmax(0, 1fr));
299
+ gap: 10px;
300
+ margin-bottom: 54px;
301
+ }
302
+ .schema-card {
303
+ padding: 15px;
304
+ border: 1px solid var(--line);
305
+ border-radius: 10px;
306
+ background: #fff;
307
+ }
308
+ .schema-card.ranked {
309
+ border-color: rgba(167, 139, 250, 0.55);
310
+ box-shadow: inset 0 0 0 1px rgba(167, 139, 250, 0.18);
311
+ }
312
+ .schema-card .schema-icon { color: var(--violet); font-family: "DM Mono", monospace; font-size: 13px; }
313
+ .schema-card strong { display: block; margin: 10px 0 4px; font-size: 12px; }
314
+ .schema-card small { color: #9598a7; font-size: 9px; }
315
+ .schema-card .column-list { margin-top: 10px; color: #858899; font-family: "DM Mono", monospace; font-size: 8px; line-height: 1.7; }
316
+
317
+ .candidates-heading { margin-top: 20px; }
318
+ .selection-count { padding: 8px 12px; border-radius: 7px; color: #77798b; background: var(--soft); font-size: 10px; }
319
+ .selection-count strong { color: var(--violet); font-size: 14px; }
320
+ .candidate-list { display: grid; gap: 9px; }
321
+ .candidate {
322
+ display: grid;
323
+ grid-template-columns: 24px minmax(180px, .8fr) minmax(280px, 1.5fr) 90px 26px;
324
+ align-items: center;
325
+ gap: 16px;
326
+ padding: 15px 17px;
327
+ border: 1px solid var(--line);
328
+ border-radius: 10px;
329
+ background: #fff;
330
+ cursor: pointer;
331
+ transition: .18s ease;
332
+ }
333
+ .candidate:hover { border-color: #cbc7ed; }
334
+ .candidate.selected { border-color: #aa9ff4; box-shadow: 0 0 0 2px rgba(104, 84, 232, .07); }
335
+ .candidate input { width: 16px; height: 16px; accent-color: var(--violet); }
336
+ .candidate-name strong, .candidate-name small { display: block; }
337
+ .candidate-name strong { font-size: 12px; }
338
+ .candidate-name small { margin-top: 4px; color: #999baa; font-size: 9px; }
339
+ .candidate-definition { color: #676a7b; font-size: 10px; line-height: 1.55; }
340
+ .grain { justify-self: start; padding: 4px 7px; border-radius: 5px; color: #6e7283; background: var(--soft); font-size: 8px; text-transform: uppercase; }
341
+ .sql-toggle { display: grid; place-items: center; width: 25px; height: 25px; border: 0; border-radius: 6px; color: var(--violet); background: var(--violet-soft); cursor: pointer; }
342
+ .candidate-sql { display: none; grid-column: 2 / -1; margin: 0; padding: 13px; border-radius: 7px; color: #d9d8e8; background: #202138; font-family: "DM Mono", monospace; font-size: 9px; line-height: 1.65; white-space: pre-wrap; }
343
+ .candidate.open .candidate-sql { display: block; }
344
+
345
+ .certify-bar {
346
+ display: flex;
347
+ align-items: center;
348
+ justify-content: space-between;
349
+ gap: 20px;
350
+ margin-top: 18px;
351
+ padding: 17px 19px;
352
+ border: 1px solid #dcd8fa;
353
+ border-radius: 11px;
354
+ background: #f8f7ff;
355
+ }
356
+ .certify-bar > div { display: flex; align-items: center; gap: 12px; }
357
+ .certify-bar strong, .certify-bar small { display: block; }
358
+ .certify-bar strong { font-size: 11px; }
359
+ .certify-bar small { margin-top: 3px; color: #85889a; font-size: 9px; }
360
+
361
+ .dashboard-heading { padding-top: 42px; }
362
+ .empty-state { display: grid; justify-items: center; max-width: 500px; margin: 90px auto; padding: 50px; text-align: center; border: 1px dashed #d8d9e2; border-radius: 14px; }
363
+ .empty-state > span { color: var(--violet); font-size: 36px; }
364
+ .empty-state h3 { margin: 12px 0 5px; }
365
+ .empty-state p { margin: 0 0 22px; color: var(--muted); font-size: 11px; }
366
+ .briefing-card { display: flex; gap: 15px; margin: 28px 0 20px; padding: 20px; border: 1px solid #d9d4fa; border-radius: 12px; background: linear-gradient(120deg, #faf9ff, #f4f1ff); }
367
+ .briefing-icon { display: grid; place-items: center; flex: 0 0 37px; height: 37px; border-radius: 9px; color: #fff; background: var(--violet); }
368
+ .briefing-card span { color: var(--violet); font-size: 8px; font-weight: 800; letter-spacing: 1px; }
369
+ .briefing-card p { margin: 7px 0 5px; font-size: 13px; line-height: 1.55; }
370
+ .briefing-card small { color: #7e8192; font-size: 9px; }
371
+ .role-tabs { display: flex; gap: 5px; margin: 26px 0 12px; padding-bottom: 8px; overflow-x: auto; }
372
+ .role-tab { flex: 0 0 auto; padding: 8px 13px; border: 1px solid var(--line); border-radius: 7px; color: #818495; background: #fff; font-size: 9px; font-weight: 700; cursor: pointer; }
373
+ .role-tab.active { color: #fff; border-color: var(--ink); background: var(--ink); }
374
+ .metric-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; }
375
+ .metric-card { position: relative; min-height: 142px; padding: 17px; border: 1px solid var(--line); border-radius: 11px; background: #fff; overflow: hidden; }
376
+ .metric-card::after { position: absolute; right: -12px; bottom: -25px; width: 80px; height: 80px; border-radius: 50%; background: var(--violet-soft); content: ""; }
377
+ .metric-card .metric-label { display: flex; justify-content: space-between; gap: 8px; color: #75788b; font-size: 9px; font-weight: 600; }
378
+ .status-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--green); box-shadow: 0 0 0 4px var(--green-soft); }
379
+ .metric-value { margin: 21px 0 6px; font-size: 29px; font-weight: 800; letter-spacing: -1.3px; }
380
+ .metric-card small { position: relative; z-index: 1; color: #9a9cab; font-size: 8px; }
381
+ .catalog-heading { margin-top: 46px; }
382
+ .catalog-heading h3 { margin: 0 0 4px; font-size: 18px; }
383
+ .certification-report { display: grid; gap: 8px; }
384
+ .report-row { display: grid; grid-template-columns: 95px 1fr 1.5fr; gap: 15px; padding: 13px 15px; border: 1px solid var(--line); border-radius: 9px; background: #fff; font-size: 10px; }
385
+ .report-status { justify-self: start; padding: 4px 7px; border-radius: 5px; color: var(--green); background: var(--green-soft); font-size: 8px; font-weight: 800; text-transform: uppercase; }
386
+ .report-status.draft { color: var(--red); background: var(--red-soft); }
387
+ .report-row strong { font-size: 10px; }
388
+ .report-row p { margin: 0; color: #7a7d8f; line-height: 1.5; }
389
+
390
+ .integration-hero { padding: 58px 0 38px; }
391
+ .integration-hero h2 { margin: 15px 0; font-size: clamp(34px, 4vw, 52px); }
392
+ .integration-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
393
+ .integration-card { padding: 23px; border: 1px solid var(--line); border-radius: 13px; background: #fff; }
394
+ .integration-card.featured { border-color: #d4cff7; }
395
+ .integration-type { color: var(--violet); font-size: 8px; font-weight: 800; letter-spacing: 1.2px; }
396
+ .integration-card h3 { margin: 9px 0 7px; font-size: 17px; }
397
+ .integration-card > p { min-height: 43px; margin: 0 0 16px; color: var(--muted); font-size: 10px; line-height: 1.6; }
398
+ .code-block { position: relative; border-radius: 9px; color: #deddeb; background: #1e1f35; overflow: hidden; }
399
+ .code-block pre { min-height: 170px; margin: 0; padding: 20px; overflow: auto; font-family: "DM Mono", monospace; font-size: 9px; line-height: 1.8; }
400
+ .copy-button { position: absolute; top: 8px; right: 8px; padding: 5px 8px; border: 1px solid #3b3d58; border-radius: 5px; color: #a7a9bb; background: #292a43; font-size: 8px; cursor: pointer; }
401
+ .integration-steps { position: relative; margin-top: 15px; padding: 24px; border: 1px solid var(--line); border-radius: 13px; background: #fff; }
402
+ .integration-steps h3 { margin: 0 0 18px; font-size: 16px; }
403
+ .integration-steps ol { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; max-width: 75%; margin: 0; padding: 0; list-style: none; }
404
+ .integration-steps li { display: flex; align-items: center; gap: 10px; }
405
+ .integration-steps li > span { display: grid; place-items: center; flex: 0 0 25px; height: 25px; border-radius: 50%; color: var(--violet); background: var(--violet-soft); font-size: 9px; font-weight: 800; }
406
+ .integration-steps li strong, .integration-steps li code { display: block; }
407
+ .integration-steps li strong { font-size: 10px; }
408
+ .integration-steps li code { margin-top: 3px; color: #838697; font-family: "DM Mono", monospace; font-size: 8px; }
409
+ .integration-steps aside { position: absolute; top: 24px; right: 24px; width: 21%; padding: 13px; border-radius: 8px; background: var(--amber-soft); }
410
+ .integration-steps aside span { color: var(--amber); font-size: 8px; font-weight: 800; text-transform: uppercase; }
411
+ .integration-steps aside p { margin: 5px 0 0; color: #80643a; font-size: 8px; line-height: 1.55; }
412
+
413
+ .toast { position: fixed; right: 24px; bottom: 24px; z-index: 20; max-width: 360px; padding: 12px 16px; border-radius: 8px; color: #fff; background: var(--ink); box-shadow: var(--shadow); font-size: 10px; opacity: 0; transform: translateY(10px); pointer-events: none; transition: .25s ease; }
414
+ .toast.show { opacity: 1; transform: translateY(0); }
415
+ .toast.error { background: var(--red); }
416
+ .loading { position: relative; color: transparent !important; pointer-events: none; }
417
+ .loading::after { position: absolute; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,.4); border-top-color: #fff; border-radius: 50%; content: ""; animation: spin .7s linear infinite; }
418
+ @keyframes spin { to { transform: rotate(360deg); } }
419
+
420
+ body.embed .sidebar, body.embed .topbar { display: none; }
421
+ body.embed .app-shell { display: block; }
422
+ body.embed main { max-width: none; padding-inline: 28px; }
423
+ body.embed .hero { min-height: 390px; }
424
+
425
+ @media (max-width: 980px) {
426
+ .app-shell { grid-template-columns: 82px minmax(0, 1fr); }
427
+ .sidebar { padding-inline: 10px; }
428
+ .brand { justify-content: center; padding-inline: 0; font-size: 0; }
429
+ .brand > span:last-child, .nav-label, .nav-item span:last-child, .trust-note div { display: none; }
430
+ .nav-item { justify-content: center; }
431
+ .trust-note { justify-content: center; padding: 10px; }
432
+ .hero { grid-template-columns: 1fr; gap: 20px; }
433
+ .pipeline-visual { min-height: 200px; }
434
+ .schema-grid { grid-template-columns: repeat(3, 1fr); }
435
+ .metric-grid { grid-template-columns: repeat(2, 1fr); }
436
+ .integration-steps ol { max-width: 100%; }
437
+ .integration-steps aside { position: static; width: auto; margin-top: 18px; }
438
+ }
439
+
440
+ @media (max-width: 680px) {
441
+ .app-shell { display: block; }
442
+ .sidebar { position: static; display: none; }
443
+ .topbar { height: auto; padding: 18px 20px; }
444
+ .top-actions .button, .api-status { display: none; }
445
+ main { padding: 0 20px 45px; }
446
+ .hero { min-height: auto; padding: 42px 0; }
447
+ .hero h2 { font-size: 38px; letter-spacing: -2px; }
448
+ .hero-actions { align-items: flex-start; flex-direction: column; }
449
+ .pipeline-visual { grid-template-columns: 1fr; gap: 8px; }
450
+ .pipeline-line { width: 1px; height: 22px; margin: auto; }
451
+ .schema-grid, .metric-grid, .integration-grid, .integration-steps ol { grid-template-columns: 1fr; }
452
+ .candidate { grid-template-columns: 22px 1fr 26px; }
453
+ .candidate-definition, .candidate .grain { grid-column: 2 / -1; }
454
+ .candidate-sql { grid-column: 1 / -1; }
455
+ .certify-bar { align-items: stretch; flex-direction: column; }
456
+ .report-row { grid-template-columns: 85px 1fr; }
457
+ .report-row p { grid-column: 1 / -1; }
458
+ }