opencastle 0.16.0 → 0.18.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 (65) hide show
  1. package/dist/cli/convoy/engine.d.ts.map +1 -1
  2. package/dist/cli/convoy/engine.js +47 -11
  3. package/dist/cli/convoy/engine.js.map +1 -1
  4. package/dist/cli/convoy/engine.test.js +104 -1
  5. package/dist/cli/convoy/engine.test.js.map +1 -1
  6. package/dist/cli/convoy/export.d.ts +3 -0
  7. package/dist/cli/convoy/export.d.ts.map +1 -0
  8. package/dist/cli/convoy/export.js +46 -0
  9. package/dist/cli/convoy/export.js.map +1 -0
  10. package/dist/cli/convoy/export.test.d.ts +2 -0
  11. package/dist/cli/convoy/export.test.d.ts.map +1 -0
  12. package/dist/cli/convoy/export.test.js +157 -0
  13. package/dist/cli/convoy/export.test.js.map +1 -0
  14. package/dist/cli/convoy/health.test.js +1 -0
  15. package/dist/cli/convoy/health.test.js.map +1 -1
  16. package/dist/cli/convoy/store.d.ts.map +1 -1
  17. package/dist/cli/convoy/store.js +8 -3
  18. package/dist/cli/convoy/store.js.map +1 -1
  19. package/dist/cli/convoy/store.test.js +83 -3
  20. package/dist/cli/convoy/store.test.js.map +1 -1
  21. package/dist/cli/convoy/types.d.ts +1 -0
  22. package/dist/cli/convoy/types.d.ts.map +1 -1
  23. package/dist/cli/dashboard.d.ts +14 -0
  24. package/dist/cli/dashboard.d.ts.map +1 -1
  25. package/dist/cli/dashboard.js +73 -36
  26. package/dist/cli/dashboard.js.map +1 -1
  27. package/dist/cli/run/adapters/index.d.ts.map +1 -1
  28. package/dist/cli/run/adapters/index.js +2 -1
  29. package/dist/cli/run/adapters/index.js.map +1 -1
  30. package/dist/cli/run/adapters/opencode.d.ts +16 -0
  31. package/dist/cli/run/adapters/opencode.d.ts.map +1 -0
  32. package/dist/cli/run/adapters/opencode.js +75 -0
  33. package/dist/cli/run/adapters/opencode.js.map +1 -0
  34. package/dist/cli/run/schema.d.ts.map +1 -1
  35. package/dist/cli/run/schema.js +11 -0
  36. package/dist/cli/run/schema.js.map +1 -1
  37. package/dist/cli/run/schema.test.js +44 -0
  38. package/dist/cli/run/schema.test.js.map +1 -1
  39. package/dist/cli/run.d.ts +1 -1
  40. package/dist/cli/run.d.ts.map +1 -1
  41. package/dist/cli/run.js +18 -1
  42. package/dist/cli/run.js.map +1 -1
  43. package/dist/cli/types.d.ts +3 -0
  44. package/dist/cli/types.d.ts.map +1 -1
  45. package/package.json +1 -1
  46. package/src/cli/convoy/engine.test.ts +126 -1
  47. package/src/cli/convoy/engine.ts +39 -9
  48. package/src/cli/convoy/export.test.ts +190 -0
  49. package/src/cli/convoy/export.ts +52 -0
  50. package/src/cli/convoy/health.test.ts +1 -0
  51. package/src/cli/convoy/store.test.ts +89 -3
  52. package/src/cli/convoy/store.ts +8 -3
  53. package/src/cli/convoy/types.ts +1 -0
  54. package/src/cli/dashboard.ts +94 -42
  55. package/src/cli/run/adapters/index.ts +2 -1
  56. package/src/cli/run/adapters/opencode.ts +88 -0
  57. package/src/cli/run/schema.test.ts +50 -0
  58. package/src/cli/run/schema.ts +13 -0
  59. package/src/cli/run.ts +19 -1
  60. package/src/cli/types.ts +3 -0
  61. package/src/dashboard/dist/_astro/{index.Bnq19_1M.css → index.DyyaCW8L.css} +1 -1
  62. package/src/dashboard/dist/index.html +145 -6
  63. package/src/dashboard/node_modules/.vite/deps/_metadata.json +6 -6
  64. package/src/dashboard/src/pages/index.astro +160 -4
  65. package/src/dashboard/src/styles/dashboard.css +60 -0
@@ -20,6 +20,7 @@ const MIME_TYPES: Record<string, string> = {
20
20
 
21
21
  const DATA_FILES = [
22
22
  'events.ndjson',
23
+ 'convoys.ndjson',
23
24
  // Legacy individual files — kept for backwards compatibility
24
25
  'sessions.ndjson',
25
26
  'delegations.ndjson',
@@ -32,12 +33,28 @@ interface DashboardArgs {
32
33
  port: number
33
34
  openBrowser: boolean
34
35
  seed: boolean
36
+ convoyId?: string
37
+ }
38
+
39
+ export interface DashboardServerOptions {
40
+ port?: number
41
+ openBrowser?: boolean
42
+ seed?: boolean
43
+ pkgRoot: string
44
+ convoyId?: string
45
+ }
46
+
47
+ export interface DashboardServerResult {
48
+ server: Server
49
+ port: number
50
+ url: string
35
51
  }
36
52
 
37
53
  function parseArgs(args: string[]): DashboardArgs {
38
54
  let port = 4300
39
55
  let openBrowser = true
40
56
  let seed = false
57
+ let convoyId: string | undefined
41
58
 
42
59
  for (let i = 0; i < args.length; i++) {
43
60
  if (args[i] === '--port' && args[i + 1]) {
@@ -47,10 +64,13 @@ function parseArgs(args: string[]): DashboardArgs {
47
64
  openBrowser = false
48
65
  } else if (args[i] === '--seed') {
49
66
  seed = true
67
+ } else if (args[i] === '--convoy' && args[i + 1]) {
68
+ convoyId = args[i + 1]
69
+ i++
50
70
  }
51
71
  }
52
72
 
53
- return { port, openBrowser, seed }
73
+ return { port, openBrowser, seed, convoyId }
54
74
  }
55
75
 
56
76
  function openUrl(url: string): void {
@@ -104,15 +124,17 @@ function tryListen(
104
124
  })
105
125
  }
106
126
 
107
- export default async function dashboard({
108
- pkgRoot,
109
- args,
110
- }: CliContext): Promise<void> {
111
- const { port, openBrowser, seed } = parseArgs(args)
127
+ export async function startDashboardServer(
128
+ options: DashboardServerOptions,
129
+ ): Promise<DashboardServerResult> {
130
+ const port = options.port ?? 4300
131
+ const seed = options.seed ?? false
132
+ const { pkgRoot } = options
112
133
 
113
134
  const distDir = resolve(pkgRoot, 'src', 'dashboard', 'dist')
114
135
  const seedDir = resolve(pkgRoot, 'src', 'dashboard', 'seed-data')
115
136
  const projectRoot = process.cwd()
137
+ const convoyLogsDir = resolve(projectRoot, '.opencastle', 'logs')
116
138
  const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
117
139
 
118
140
  // Check if dist exists
@@ -122,18 +144,6 @@ export default async function dashboard({
122
144
  )
123
145
  }
124
146
 
125
- // Check if any log files exist (for messaging)
126
- let hasLogs = false
127
- if (!seed) {
128
- const checkFiles = ['events.ndjson', ...DATA_FILES]
129
- for (const f of checkFiles) {
130
- if (await fileExists(join(logsDir, f))) {
131
- hasLogs = true
132
- break
133
- }
134
- }
135
- }
136
-
137
147
  const server = createServer(
138
148
  async (req: IncomingMessage, res: ServerResponse) => {
139
149
  try {
@@ -149,22 +159,34 @@ export default async function dashboard({
149
159
  const dataMatch = pathname.match(/^\/data\/(.+\.ndjson)$/)
150
160
  if (dataMatch && DATA_FILES.includes(dataMatch[1])) {
151
161
  const filename = dataMatch[1]
152
- let filePath: string
153
162
 
154
163
  if (seed) {
155
- filePath = join(seedDir, filename)
164
+ const filePath = join(seedDir, filename)
165
+ if (await fileExists(filePath)) {
166
+ const content = await readFile(filePath)
167
+ res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
168
+ res.end(content)
169
+ } else {
170
+ res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
171
+ res.end('')
172
+ }
156
173
  } else {
157
- filePath = join(logsDir, filename)
158
- }
174
+ const convoyPath = join(convoyLogsDir, filename)
175
+ const logsPath = join(logsDir, filename)
176
+ const inConvoy = await fileExists(convoyPath)
177
+ const inLogs = await fileExists(logsPath)
159
178
 
160
- if (await fileExists(filePath)) {
161
- const content = await readFile(filePath)
162
- res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
163
- res.end(content)
164
- } else {
165
- // Graceful fallback — empty body
166
179
  res.writeHead(200, { 'Content-Type': 'application/x-ndjson' })
167
- res.end('')
180
+ if (inConvoy && inLogs) {
181
+ const [c1, c2] = await Promise.all([readFile(convoyPath), readFile(logsPath)])
182
+ res.end(Buffer.concat([c1, c2]))
183
+ } else if (inConvoy) {
184
+ res.end(await readFile(convoyPath))
185
+ } else if (inLogs) {
186
+ res.end(await readFile(logsPath))
187
+ } else {
188
+ res.end('')
189
+ }
168
190
  }
169
191
  return
170
192
  }
@@ -197,23 +219,53 @@ export default async function dashboard({
197
219
  )
198
220
 
199
221
  const actualPort = await tryListen(server, port)
200
- const url = `http://localhost:${actualPort}`
222
+ const resolvedUrl = `http://localhost:${actualPort}`
223
+
224
+ return { server, port: actualPort, url: resolvedUrl }
225
+ }
226
+
227
+ export default async function dashboard({
228
+ pkgRoot,
229
+ args,
230
+ }: CliContext): Promise<void> {
231
+ const { port, openBrowser, seed, convoyId } = parseArgs(args)
232
+
233
+ // Check if any log files exist (for messaging)
234
+ let hasLogs = false
235
+ if (!seed) {
236
+ const projectRoot = process.cwd()
237
+ const logsDir = resolve(projectRoot, '.github', 'customizations', 'logs')
238
+ const checkFiles = ['events.ndjson', ...DATA_FILES]
239
+ for (const f of checkFiles) {
240
+ if (await fileExists(join(logsDir, f))) {
241
+ hasLogs = true
242
+ break
243
+ }
244
+ }
245
+ }
246
+
247
+ const dashResult = await startDashboardServer({ port, seed, pkgRoot, convoyId })
201
248
 
202
249
  console.log('')
203
250
  console.log(' \u{1F3F0} OpenCastle Dashboard')
204
251
  console.log('')
205
- console.log(` \u2192 ${url}`)
206
252
 
207
- if (seed) {
208
- console.log(
209
- ' \u{1F4C2} Showing demo data (use without --seed to read project logs)'
210
- )
211
- } else if (hasLogs) {
212
- console.log(' \u{1F4C2} Reading logs from .github/customizations/logs/')
253
+ if (convoyId) {
254
+ console.log(` \u2192 ${dashResult.url}/?convoy=${convoyId}`)
255
+ console.log(` \u{1F4C2} Watching convoy: ${convoyId}`)
213
256
  } else {
214
- console.log(
215
- ' \u{1F4A1} No agent logs found. Run agents with OpenCastle to generate data, or use --seed for demo data.'
216
- )
257
+ console.log(` \u2192 ${dashResult.url}`)
258
+ if (seed) {
259
+ console.log(
260
+ ' \u{1F4C2} Showing demo data (use without --seed to read project logs)'
261
+ )
262
+ } else if (hasLogs) {
263
+ console.log(' \u{1F4C2} Reading logs from .github/customizations/logs/')
264
+ } else {
265
+ console.log(
266
+ ' \u{1F4A1} No agent logs found. Run agents with OpenCastle to generate data, or use --seed for demo data.'
267
+ )
268
+ }
217
269
  }
218
270
 
219
271
  console.log('')
@@ -221,13 +273,13 @@ export default async function dashboard({
221
273
  console.log('')
222
274
 
223
275
  if (openBrowser) {
224
- openUrl(url)
276
+ openUrl(convoyId ? `${dashResult.url}/?convoy=${convoyId}` : dashResult.url)
225
277
  }
226
278
 
227
279
  // Graceful shutdown
228
280
  process.on('SIGINT', () => {
229
281
  console.log('\n Dashboard stopped.\n')
230
- server.close()
282
+ dashResult.server.close()
231
283
  process.exit(0)
232
284
  })
233
285
 
@@ -7,6 +7,7 @@ const ADAPTERS: Record<string, () => Promise<AgentAdapter>> = {
7
7
  'claude-code': () => import('./claude-code.js') as Promise<AgentAdapter>,
8
8
  copilot: () => import('./copilot.js') as Promise<AgentAdapter>,
9
9
  cursor: () => import('./cursor.js') as Promise<AgentAdapter>,
10
+ opencode: () => import('./opencode.js') as Promise<AgentAdapter>,
10
11
  }
11
12
 
12
13
  /**
@@ -28,7 +29,7 @@ export async function getAdapter(name: string): Promise<AgentAdapter> {
28
29
  * Detection priority order — checked first-to-last.
29
30
  * The first available adapter wins.
30
31
  */
31
- const DETECTION_ORDER = ['copilot', 'claude-code', 'cursor'] as const
32
+ const DETECTION_ORDER = ['copilot', 'claude-code', 'cursor', 'opencode'] as const
32
33
 
33
34
  /**
34
35
  * Auto-detect which adapter CLI is available on the system.
@@ -0,0 +1,88 @@
1
+ import { spawn } from 'node:child_process'
2
+ import type { Task, ExecuteOptions, ExecuteResult } from '../../types.js'
3
+
4
+ /** Adapter name */
5
+ export const name = 'opencode'
6
+
7
+ /**
8
+ * Check if the `opencode` CLI is available on the system PATH.
9
+ */
10
+ export async function isAvailable(): Promise<boolean> {
11
+ return new Promise((resolve) => {
12
+ const proc = spawn('which', ['opencode'], { stdio: 'pipe' })
13
+ proc.on('close', (code) => resolve(code === 0))
14
+ proc.on('error', () => resolve(false))
15
+ })
16
+ }
17
+
18
+ /**
19
+ * Execute a task by invoking the OpenCode CLI in headless mode.
20
+ */
21
+ export async function execute(task: Task, options: ExecuteOptions = {}): Promise<ExecuteResult> {
22
+ let prompt = `You are a ${task.agent}. ${task.prompt}`
23
+
24
+ if (task.files && task.files.length > 0) {
25
+ prompt += `\n\nOnly modify files under: ${task.files.join(', ')}`
26
+ }
27
+
28
+ const args = ['--headless', '-p', prompt]
29
+
30
+ return new Promise((resolve) => {
31
+ const proc = spawn('opencode', args, {
32
+ stdio: ['ignore', 'pipe', 'pipe'],
33
+ env: { ...process.env },
34
+ cwd: options?.cwd ?? process.cwd(),
35
+ })
36
+
37
+ let stdout = ''
38
+ let stderr = ''
39
+
40
+ proc.stdout.on('data', (chunk: Buffer) => {
41
+ stdout += chunk.toString()
42
+ if (options.verbose) {
43
+ process.stdout.write(chunk)
44
+ }
45
+ })
46
+
47
+ proc.stderr.on('data', (chunk: Buffer) => {
48
+ stderr += chunk.toString()
49
+ if (options.verbose) {
50
+ process.stderr.write(chunk)
51
+ }
52
+ })
53
+
54
+ proc.on('close', (code) => {
55
+ const output = [stdout, stderr].filter(Boolean).join('\n')
56
+ resolve({
57
+ success: code === 0,
58
+ output: output.slice(0, 10000), // Cap output size
59
+ exitCode: code ?? -1,
60
+ })
61
+ })
62
+
63
+ proc.on('error', (err) => {
64
+ resolve({
65
+ success: false,
66
+ output: `Failed to spawn opencode: ${err.message}`,
67
+ exitCode: -1,
68
+ })
69
+ })
70
+
71
+ // Store process ref for potential timeout kill
72
+ task._process = proc
73
+ })
74
+ }
75
+
76
+ /**
77
+ * Kill the process associated with a task (used by timeout enforcement).
78
+ */
79
+ export function kill(task: Task): void {
80
+ if (task._process && !task._process.killed) {
81
+ task._process.kill('SIGTERM')
82
+ setTimeout(() => {
83
+ if (task._process && !task._process.killed) {
84
+ task._process.kill('SIGKILL')
85
+ }
86
+ }, 5000)
87
+ }
88
+ }
@@ -529,6 +529,27 @@ describe('validateSpec — branch field', () => {
529
529
  })
530
530
  })
531
531
 
532
+ // ── validateSpec — per-task adapter ──────────────────────────
533
+
534
+ describe('validateSpec — per-task adapter', () => {
535
+ it('task.adapter must be a string', () => {
536
+ const result = validateSpec({
537
+ name: 'test',
538
+ tasks: [{ id: 'a', prompt: 'x', adapter: 123 }],
539
+ })
540
+ expect(result.valid).toBe(false)
541
+ expect(result.errors).toContainEqual(expect.stringContaining('adapter'))
542
+ })
543
+
544
+ it('task.adapter accepts valid string', () => {
545
+ const result = validateSpec({
546
+ name: 'test',
547
+ tasks: [{ id: 'a', prompt: 'x', adapter: 'opencode' }],
548
+ })
549
+ expect(result.valid).toBe(true)
550
+ })
551
+ })
552
+
532
553
  // ── validateSpec — per-task model and max_retries ──────────────
533
554
 
534
555
  describe('validateSpec — per-task model and max_retries', () => {
@@ -721,6 +742,35 @@ describe('applyDefaults — convoy spec (version: 1)', () => {
721
742
  expect(spec.gates).toEqual(['npm test'])
722
743
  expect(spec.branch).toBe('feat/convoy')
723
744
  })
745
+
746
+ it('applies defaults.adapter to tasks without explicit adapter', () => {
747
+ const spec = applyDefaults({
748
+ name: 'test',
749
+ version: 1,
750
+ defaults: { adapter: 'opencode' },
751
+ tasks: [{ id: 'a', prompt: 'x' }],
752
+ })
753
+ expect(spec.tasks![0].adapter).toBe('opencode')
754
+ })
755
+
756
+ it('task-level adapter overrides defaults.adapter', () => {
757
+ const spec = applyDefaults({
758
+ name: 'test',
759
+ version: 1,
760
+ defaults: { adapter: 'opencode' },
761
+ tasks: [{ id: 'a', prompt: 'x', adapter: 'claude-code' }],
762
+ })
763
+ expect(spec.tasks![0].adapter).toBe('claude-code')
764
+ })
765
+
766
+ it('tasks without adapter remain undefined when no defaults', () => {
767
+ const spec = applyDefaults({
768
+ name: 'test',
769
+ version: 1,
770
+ tasks: [{ id: 'a', prompt: 'x' }],
771
+ })
772
+ expect(spec.tasks![0].adapter).toBeUndefined()
773
+ })
724
774
  })
725
775
 
726
776
  // ── applyDefaults — max_retries default always applied ─────────
@@ -55,6 +55,7 @@ interface RawTask {
55
55
  description?: unknown
56
56
  model?: unknown
57
57
  max_retries?: unknown
58
+ adapter?: unknown
58
59
  }
59
60
 
60
61
  /**
@@ -126,6 +127,9 @@ export function validateSpec(spec: unknown): ValidationResult {
126
127
  if (d.agent !== undefined && typeof d.agent !== 'string') {
127
128
  errors.push('`defaults.agent` must be a string')
128
129
  }
130
+ if (d.adapter !== undefined && typeof d.adapter !== 'string') {
131
+ errors.push('`defaults.adapter` must be a string')
132
+ }
129
133
  }
130
134
  }
131
135
 
@@ -219,6 +223,11 @@ export function validateSpec(spec: unknown): ValidationResult {
219
223
  )
220
224
  }
221
225
  }
226
+
227
+ // adapter
228
+ if (task.adapter !== undefined && typeof task.adapter !== 'string') {
229
+ errors.push(`${prefix}: \`adapter\` must be a string`)
230
+ }
222
231
  }
223
232
 
224
233
  // DAG cycle detection
@@ -308,6 +317,10 @@ export function applyDefaults(spec: Record<string, unknown>): TaskSpec {
308
317
  task.max_retries =
309
318
  d.max_retries !== undefined ? Number(d.max_retries) : 1
310
319
  }
320
+ // adapter: task-level overrides defaults, no hardcoded fallback (convoy-level is used at runtime)
321
+ if (task.adapter === undefined && d.adapter !== undefined) {
322
+ task.adapter = d.adapter
323
+ }
311
324
  }
312
325
 
313
326
  return s as unknown as TaskSpec
package/src/cli/run.ts CHANGED
@@ -124,6 +124,9 @@ function printAdapterError(detectionFailed: boolean, adapterName: string): void
124
124
  ' The Cursor agent CLI ships with the Cursor editor.\n' +
125
125
  ' Install Cursor from https://cursor.com and ensure the\n' +
126
126
  ' "agent" command is on your PATH (Cursor > Install CLI).',
127
+ opencode:
128
+ ' Install OpenCode from https://opencode.ai\n' +
129
+ ' Ensure the "opencode" command is on your PATH.',
127
130
  }
128
131
  const cliName = adapterName === 'claude-code' ? 'claude' : adapterName
129
132
  const hint = hints[adapterName] ?? ''
@@ -155,7 +158,7 @@ function printConvoyResult(result: ConvoyResult): void {
155
158
  /**
156
159
  * CLI entry point for the `run` command.
157
160
  */
158
- export default async function run({ args }: CliContext): Promise<void> {
161
+ export default async function run({ args, pkgRoot }: CliContext): Promise<void> {
159
162
  const opts = parseArgs(args)
160
163
 
161
164
  if (opts.help) {
@@ -337,6 +340,18 @@ export default async function run({ args }: CliContext): Promise<void> {
337
340
  if (spec.branch) console.log(` Branch: ${spec.branch}`)
338
341
  if (spec.gates?.length) console.log(` Gates: ${spec.gates.length} validation commands`)
339
342
 
343
+ const { startDashboardServer } = await import('./dashboard.js')
344
+ let dashboardResult: { server: import('node:http').Server } | null = null
345
+ try {
346
+ dashboardResult = await startDashboardServer({
347
+ pkgRoot,
348
+ openBrowser: true,
349
+ convoyId: 'active',
350
+ })
351
+ } catch {
352
+ // Dashboard failure must not block convoy
353
+ }
354
+
340
355
  const engine = createConvoyEngine({
341
356
  spec,
342
357
  specYaml: specText,
@@ -346,6 +361,9 @@ export default async function run({ args }: CliContext): Promise<void> {
346
361
 
347
362
  const result = await engine.run()
348
363
  printConvoyResult(result)
364
+ if (dashboardResult) {
365
+ dashboardResult.server.close()
366
+ }
349
367
  process.exit(result.status !== 'done' ? 1 : 0)
350
368
  }
351
369
 
package/src/cli/types.ts CHANGED
@@ -132,6 +132,7 @@ export interface TaskDefaults {
132
132
  model?: string;
133
133
  max_retries?: number;
134
134
  agent?: string;
135
+ adapter?: string;
135
136
  }
136
137
 
137
138
  /** Validated task spec from YAML. */
@@ -166,6 +167,8 @@ export interface Task {
166
167
  model?: string;
167
168
  /** Max retry attempts (default: 1). */
168
169
  max_retries: number;
170
+ /** Per-task adapter override. */
171
+ adapter?: string;
169
172
  }
170
173
 
171
174
  /** Task execution status. */
@@ -1 +1 @@
1
- :root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}
1
+ :root{--bg-primary: #0a0a0f;--bg-secondary: #111118;--bg-tertiary: #1a1a24;--bg-card: rgba(255, 255, 255, .03);--bg-card-hover: rgba(255, 255, 255, .06);--text-primary: #f0f0f5;--text-secondary: #8a8a9a;--text-tertiary: #5a5a6e;--text-accent: #a78bfa;--gradient-accent: linear-gradient(135deg, #a78bfa 0%, #6366f1 50%, #3b82f6 100%);--gradient-glow: radial-gradient(ellipse 800px 400px at 50% 0%, rgba(99, 102, 241, .12) 0%, transparent 70%);--border-color: rgba(255, 255, 255, .06);--border-accent: rgba(167, 139, 250, .3);--color-success: #22c55e;--color-partial: #f59e0b;--color-failed: #ef4444;--color-redirected: #64748b;--color-premium: #f59e0b;--color-standard: #a78bfa;--color-utility: #3b82f6;--color-economy: #64748b;--accent-blue: #3b82f6;--accent-purple: #a78bfa;--accent-indigo: #6366f1;--max-width: 1280px;--transition-fast: .15s cubic-bezier(.4, 0, .2, 1);--transition-base: .3s cubic-bezier(.4, 0, .2, 1)}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-size:16px;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Inter,Roboto,Helvetica,Arial,sans-serif;background-color:var(--bg-primary);color:var(--text-primary);line-height:1.6;overflow-x:hidden;min-height:100vh}.dash-header{position:sticky;top:0;z-index:50;background:#0a0a0fd9;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border-color)}.dash-header__inner{max-width:var(--max-width);margin:0 auto;padding:0 24px;height:56px;display:flex;align-items:center;justify-content:space-between}.dash-header__brand{display:flex;align-items:center;gap:10px}.dash-header__icon{width:32px;height:32px;border-radius:8px;object-fit:contain}.dash-header__title{font-size:1rem;font-weight:600;color:var(--text-primary)}.dash-layout{display:flex;max-width:var(--max-width);margin:0 auto;position:relative}.dash-sidebar{position:sticky;top:56px;height:calc(100vh - 56px);width:180px;flex-shrink:0;padding:24px 0 24px 24px;overflow-y:auto;display:none}@media(min-width:1024px){.dash-sidebar{display:block}}.dash-sidebar__list{list-style:none;display:flex;flex-direction:column;gap:2px}.dash-sidebar__link{display:block;padding:8px 16px;font-size:.8125rem;font-weight:500;color:var(--text-tertiary);text-decoration:none;border-radius:8px;transition:color var(--transition-fast),background var(--transition-fast)}.dash-sidebar__link:hover{color:var(--text-secondary);background:#ffffff0a}.dash-sidebar__link--active{color:var(--text-accent);background:#a78bfa14;font-weight:600}.dash-main{flex:1;min-width:0;max-width:var(--max-width);margin:0 auto;padding:24px;display:flex;flex-direction:column;gap:20px;position:relative}.dash-main:before{content:"";position:fixed;top:0;left:50%;transform:translate(-50%);width:100%;height:600px;background:var(--gradient-glow);pointer-events:none;z-index:0}.dash-main>*{position:relative;z-index:1}[data-nav-section]{scroll-margin-top:72px}.kpi-row{display:grid;grid-template-columns:1fr;gap:12px}@media(min-width:480px){.kpi-row{grid-template-columns:repeat(2,1fr)}}@media(min-width:960px){.kpi-row{grid-template-columns:repeat(auto-fit,minmax(160px,1fr))}}.kpi-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;padding:20px 24px;display:flex;flex-direction:column;gap:4px;transition:border-color var(--transition-fast)}.kpi-card:hover{border-color:#ffffff1a}.kpi-card__label{font-size:.75rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.kpi-card__value{font-size:2rem;font-weight:700;color:var(--text-primary);line-height:1.2;letter-spacing:-.02em}.kpi-card__sub{font-size:.75rem;color:var(--text-secondary);display:flex;align-items:center;gap:4px}.kpi-trend{font-weight:600}.kpi-trend--up{color:var(--color-success)}.kpi-trend--down{color:var(--color-failed)}.kpi-trend--neutral{color:var(--text-tertiary)}.chart-card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;transition:border-color var(--transition-fast)}.chart-card:hover{border-color:#ffffff1a}.chart-card__header{padding:20px 24px 8px}.chart-card__title{font-size:.9375rem;font-weight:600;color:var(--text-primary)}.chart-card__desc{font-size:.75rem;color:var(--text-tertiary);margin-top:2px}.chart-card__body{padding:16px 24px 24px;min-height:120px}.chart-card__body--table{padding:0}.charts-row{display:grid;grid-template-columns:1fr;gap:20px}@media(min-width:768px){.charts-row{grid-template-columns:repeat(2,1fr)}}.bar-row{display:flex;align-items:center;gap:12px;padding:6px 0}.bar-row+.bar-row{border-top:1px solid rgba(255,255,255,.03)}.bar-label{font-size:.8125rem;color:var(--text-secondary);width:130px;flex-shrink:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bar-track{flex:1;height:24px;background:var(--bg-tertiary);border-radius:6px;display:flex;overflow:hidden}.bar-segment{height:100%;transition:width .8s cubic-bezier(.4,0,.2,1);min-width:0}.bar--success{background:var(--color-success)}.bar--partial{background:var(--color-partial)}.bar--failed{background:var(--color-failed)}.bar--premium{background:var(--color-premium)}.bar--standard{background:var(--color-standard)}.bar--utility{background:var(--color-utility)}.bar--economy{background:var(--color-economy)}.bar--accent{background:var(--accent-blue)}.bar-value{font-size:.8125rem;font-weight:600;color:var(--text-primary);width:36px;text-align:right;flex-shrink:0;font-variant-numeric:tabular-nums}.donut-container{display:flex;align-items:center;justify-content:center;gap:32px;flex-wrap:wrap}.donut-wrap{position:relative;width:180px;height:180px;flex-shrink:0}.donut-svg{width:100%;height:100%}.donut-svg circle{transition:stroke-dasharray .8s cubic-bezier(.4,0,.2,1),stroke-dashoffset .8s cubic-bezier(.4,0,.2,1)}.donut-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}.donut-total{display:block;font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.donut-total-label{display:block;font-size:.6875rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.08em;margin-top:2px}.donut-legend{display:flex;flex-direction:column;gap:10px}.legend-item{display:flex;align-items:center;gap:8px;font-size:.8125rem}.legend-dot{width:10px;height:10px;border-radius:3px;flex-shrink:0}.legend-name{color:var(--text-secondary);text-transform:capitalize}.legend-count{color:var(--text-tertiary);font-variant-numeric:tabular-nums;margin-left:auto}.timeline-svg{width:100%;height:auto;display:block}.timeline-svg text{font-family:inherit}.timeline-legend{display:flex;gap:16px;justify-content:center;margin-top:12px}.timeline-legend__item{display:flex;align-items:center;gap:6px;font-size:.75rem;color:var(--text-tertiary)}.timeline-legend__dot{width:8px;height:8px;border-radius:2px}.pipeline{display:flex;align-items:stretch;gap:0;overflow-x:auto;padding:8px 0}.pipeline-stage{flex:1;min-width:140px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:16px 12px;position:relative}.pipeline-stage:not(:last-child):after{content:"";position:absolute;right:-1px;top:50%;transform:translateY(-50%);width:2px;height:40%;background:var(--border-color)}.pipeline-stage__icon{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:1rem}.pipeline-stage__icon--pending{background:#64748b26;color:#94a3b8;border:1px solid rgba(100,116,139,.2)}.pipeline-stage__icon--active{background:#3b82f626;color:#60a5fa;border:1px solid rgba(59,130,246,.3);animation:pulse-glow 2s ease-in-out infinite}.pipeline-stage__icon--review{background:#f59e0b26;color:#fbbf24;border:1px solid rgba(245,158,11,.3)}.pipeline-stage__icon--done{background:#22c55e26;color:#4ade80;border:1px solid rgba(34,197,94,.3)}@keyframes pulse-glow{0%,to{box-shadow:0 0 #3b82f633}50%{box-shadow:0 0 12px 4px #3b82f626}}.pipeline-stage__count{font-size:1.5rem;font-weight:700;color:var(--text-primary);line-height:1}.pipeline-stage__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.04em;font-weight:500}.pipeline-arrow{display:flex;align-items:center;color:var(--text-tertiary);font-size:1.25rem;padding:0 4px;flex-shrink:0}.exec-log{display:flex;flex-direction:column}.exec-step{display:flex;gap:16px;padding:14px 0;position:relative}.exec-step+.exec-step{border-top:1px solid rgba(255,255,255,.03)}.exec-step__indicator{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:32px}.exec-step__dot{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.6875rem;font-weight:700;flex-shrink:0}.exec-step__dot--success{background:#22c55e26;color:var(--color-success);border:1.5px solid rgba(34,197,94,.3)}.exec-step__dot--partial{background:#f59e0b26;color:var(--color-partial);border:1.5px solid rgba(245,158,11,.3)}.exec-step__dot--failed{background:#ef444426;color:var(--color-failed);border:1.5px solid rgba(239,68,68,.3)}.exec-step__line{flex:1;width:1.5px;background:var(--border-color);margin-top:4px}.exec-step__content{flex:1;min-width:0}.exec-step__header{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.exec-step__agent{font-size:.875rem;font-weight:600;color:var(--text-primary)}.exec-step__badge{display:inline-flex;align-items:center;padding:2px 8px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.exec-step__badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.exec-step__badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.exec-step__badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.exec-step__task{font-size:.8125rem;color:var(--text-secondary);margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.exec-step__meta{display:flex;gap:16px;margin-top:6px;font-size:.6875rem;color:var(--text-tertiary)}.exec-step__meta-item{display:flex;align-items:center;gap:4px}.panel-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}.panel-item{background:var(--bg-tertiary);border-radius:8px;padding:16px;display:flex;flex-direction:column;gap:8px;border:1px solid transparent;transition:border-color var(--transition-fast)}.panel-item:hover{border-color:var(--border-color)}.panel-item__header{display:flex;align-items:center;justify-content:space-between}.panel-item__key{font-size:.8125rem;font-weight:600;color:var(--text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.panel-item__verdict{font-size:.6875rem;font-weight:700;padding:2px 8px;border-radius:4px;text-transform:uppercase;letter-spacing:.04em}.panel-item__verdict--pass{background:#22c55e26;color:var(--color-success)}.panel-item__verdict--block{background:#ef444426;color:var(--color-failed)}.panel-item__votes{display:flex;gap:4px}.panel-item__vote{width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:.625rem;font-weight:700}.panel-item__vote--pass{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.panel-item__vote--block{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.panel-item__fixes{font-size:.6875rem;color:var(--text-tertiary)}.panel-item__meta{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;padding-top:8px;border-top:1px solid var(--border-color)}.panel-item__meta-item{font-size:.625rem;color:var(--text-tertiary);white-space:nowrap}.sessions-table{width:100%;border-collapse:collapse;font-size:.8125rem}.sessions-table thead{position:sticky;top:0}.sessions-table th{padding:12px 16px;font-size:.6875rem;font-weight:600;color:var(--text-tertiary);text-align:left;text-transform:uppercase;letter-spacing:.06em;background:var(--bg-tertiary);border-bottom:1px solid var(--border-color)}.sessions-table th:last-child,.sessions-table td:last-child{text-align:right}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5){text-align:right}.sessions-table td{padding:10px 16px;color:var(--text-secondary);border-bottom:1px solid rgba(255,255,255,.03);white-space:nowrap}.sessions-table tr:hover td{background:#ffffff05}.sessions-table .td-agent{font-weight:500;color:var(--text-primary)}.sessions-table .td-task{max-width:260px;overflow:hidden;text-overflow:ellipsis}.outcome-badge{display:inline-flex;align-items:center;padding:3px 10px;font-size:.6875rem;font-weight:600;border-radius:100px;text-transform:capitalize}.outcome-badge--success{background:#22c55e1f;color:var(--color-success);border:1px solid rgba(34,197,94,.2)}.outcome-badge--partial{background:#f59e0b1f;color:var(--color-partial);border:1px solid rgba(245,158,11,.2)}.outcome-badge--failed{background:#ef44441f;color:var(--color-failed);border:1px solid rgba(239,68,68,.2)}.td-num{font-variant-numeric:tabular-nums;text-align:right}.td-issue{font-size:.75rem;color:var(--text-accent);font-weight:500;font-variant-numeric:tabular-nums}.loading-skeleton{display:flex;align-items:center;justify-content:center;min-height:200px;color:var(--text-tertiary);font-size:.8125rem}.loading-skeleton:after{content:"Loading data…";animation:fade-pulse 1.5s ease-in-out infinite}@keyframes fade-pulse{0%,to{opacity:.4}50%{opacity:1}}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:48px 24px;text-align:center;gap:12px}.empty-state__icon{font-size:2rem;opacity:.4}.empty-state__text{font-size:.875rem;color:var(--text-tertiary);max-width:320px}.empty-state--enhanced{padding:56px 32px;gap:16px;border:1px dashed rgba(167,139,250,.15);border-radius:12px;background:radial-gradient(ellipse 300px 200px at 50% 30%,rgba(99,102,241,.04) 0%,transparent 70%),var(--bg-tertiary);position:relative;overflow:hidden}.empty-state--enhanced:before{content:"";position:absolute;inset:0;background:repeating-linear-gradient(0deg,transparent,transparent 23px,rgba(255,255,255,.015) 23px,rgba(255,255,255,.015) 24px);pointer-events:none}.empty-state__icon-wrap{width:64px;height:64px;display:flex;align-items:center;justify-content:center;border-radius:16px;background:#a78bfa0f;border:1px solid rgba(167,139,250,.12);color:var(--text-accent);animation:empty-breathe 4s ease-in-out infinite}@keyframes empty-breathe{0%,to{box-shadow:0 0 #a78bfa14;transform:scale(1)}50%{box-shadow:0 0 20px 4px #a78bfa0f;transform:scale(1.03)}}.empty-state__title{font-size:.9375rem;font-weight:600;color:var(--text-secondary);letter-spacing:-.01em}.empty-state__desc{font-size:.8125rem;color:var(--text-tertiary);max-width:380px;line-height:1.55}.kpi-card__hint{color:var(--text-tertiary);font-style:italic;font-size:.6875rem}.kpi-row--empty .kpi-card{border-style:dashed;border-color:#ffffff0a}.kpi-row--empty .kpi-card__value{color:var(--text-tertiary);opacity:.5}.welcome-banner{position:relative;background:var(--bg-secondary);border:1px solid transparent;border-radius:16px;padding:48px 40px;overflow:hidden;z-index:1}.welcome-banner:before{content:"";position:absolute;inset:-1px;border-radius:16px;padding:1px;background:linear-gradient(135deg,#a78bfa4d,#6366f126,#3b82f61a 60%,#a78bfa33);-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask-composite:exclude;pointer-events:none;z-index:0}.welcome-banner__glow{position:absolute;top:-60px;left:50%;transform:translate(-50%);width:500px;height:300px;background:radial-gradient(ellipse at center,rgba(167,139,250,.08) 0%,rgba(99,102,241,.04) 40%,transparent 70%);pointer-events:none;z-index:0}.welcome-banner__content{position:relative;z-index:1;display:flex;flex-direction:column;align-items:center;text-align:center;gap:20px}.welcome-banner__icon{width:72px;height:72px;display:flex;align-items:center;justify-content:center;border-radius:20px;background:#a78bfa14;border:1px solid rgba(167,139,250,.15);color:var(--text-accent);animation:welcome-float 6s ease-in-out infinite}@keyframes welcome-float{0%,to{transform:translateY(0);box-shadow:0 8px 32px #a78bfa14}50%{transform:translateY(-6px);box-shadow:0 16px 48px #a78bfa1f}}.welcome-banner__title{font-size:1.375rem;font-weight:700;color:var(--text-primary);letter-spacing:-.02em;line-height:1.3}.welcome-banner__subtitle{font-size:.9375rem;color:var(--text-secondary);max-width:480px;line-height:1.6}.welcome-banner__steps{display:flex;gap:20px;margin-top:12px;flex-wrap:wrap;justify-content:center}.welcome-step{display:flex;align-items:flex-start;gap:12px;text-align:left;padding:16px 20px;background:#ffffff05;border:1px solid rgba(255,255,255,.05);border-radius:12px;min-width:200px;max-width:220px;transition:border-color var(--transition-fast),background var(--transition-fast)}.welcome-step:hover{border-color:#a78bfa26;background:#ffffff08}.welcome-step__num{width:28px;height:28px;border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:.75rem;font-weight:700;color:var(--text-accent);background:#a78bfa1a;border:1px solid rgba(167,139,250,.2);flex-shrink:0}.welcome-step__text{display:flex;flex-direction:column;gap:3px}.welcome-step__text strong{font-size:.8125rem;font-weight:600;color:var(--text-primary)}.welcome-step__text span{font-size:.75rem;color:var(--text-tertiary);line-height:1.4}@media(max-width:640px){.welcome-banner{padding:32px 24px}.welcome-banner__steps{flex-direction:column;align-items:center}.welcome-step{max-width:100%;width:100%}}@keyframes slide-up{0%{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.dash-main>*{animation:slide-up .5s ease-out backwards}.dash-main>*:nth-child(1){animation-delay:0ms}.dash-main>*:nth-child(2){animation-delay:60ms}.dash-main>*:nth-child(3){animation-delay:.12s}.dash-main>*:nth-child(4){animation-delay:.18s}.dash-main>*:nth-child(5){animation-delay:.24s}.dash-main>*:nth-child(6){animation-delay:.3s}.dash-main>*:nth-child(7){animation-delay:.36s}.dash-main>*:nth-child(8){animation-delay:.42s}.dash-main>*:nth-child(9){animation-delay:.48s}.dash-main>*:nth-child(10){animation-delay:.54s}.dash-main>*:nth-child(11){animation-delay:.6s}@media(max-width:640px){.bar-label{width:90px;font-size:.75rem}.donut-container{flex-direction:column;align-items:center}.donut-wrap{width:150px;height:150px}.pipeline{gap:0}.pipeline-stage{min-width:100px;padding:12px 8px}.panel-grid{grid-template-columns:1fr}.sessions-table th:nth-child(3),.sessions-table td:nth-child(3){display:none}}.filter-bar{display:flex;flex-wrap:wrap;gap:12px;align-items:flex-end;padding:16px 20px;background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:12px}.filter-group{display:flex;flex-direction:column;gap:4px;min-width:0}.filter-label{font-size:.6875rem;font-weight:500;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.filter-input,.filter-select{height:34px;padding:0 10px;font-size:.8125rem;color:var(--text-primary);background:var(--bg-tertiary);border:1px solid var(--border-color);border-radius:8px;outline:none;transition:border-color var(--transition-fast);font-family:inherit}.filter-input:focus,.filter-select:focus{border-color:var(--border-accent)}.filter-input{width:140px;color-scheme:dark}.filter-select{min-width:140px;cursor:pointer;appearance:none;-webkit-appearance:none;background-image:url("data:image/svg+xml,%3Csvg width='10' height='6' viewBox='0 0 10 6' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%235a5a6e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center;padding-right:28px}.filter-reset{height:34px;font-size:.75rem}.dash-btn{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;font-size:.8125rem;font-weight:500;font-family:inherit;border:none;border-radius:8px;cursor:pointer;transition:background var(--transition-fast),color var(--transition-fast)}.dash-btn--ghost{color:var(--text-secondary);background:#ffffff0f}.dash-btn--ghost:hover{color:var(--text-primary);background:#ffffff1a}.dash-header__actions{display:flex;align-items:center;gap:8px}@media(max-width:480px){.dash-header__inner{padding:0 12px}.dash-main{padding:12px;gap:12px}.kpi-card,.chart-card__header{padding:14px 16px}.chart-card__body{padding:12px 16px 16px}.filter-bar{padding:12px;gap:8px}.filter-input,.filter-select{width:100%;min-width:unset}.filter-group{flex:1 1 calc(50% - 4px)}.filter-reset{width:100%}.dash-header__title{font-size:.875rem}.exec-step__meta{flex-direction:column;gap:2px}.sessions-table th:nth-child(5),.sessions-table td:nth-child(5),.sessions-table th:nth-child(6),.sessions-table td:nth-child(6),.sessions-table th:nth-child(7),.sessions-table td:nth-child(7),.sessions-table th:nth-child(8),.sessions-table td:nth-child(8){display:none}}@media(max-width:768px){.charts-row{grid-template-columns:1fr}.pipeline{flex-wrap:wrap;gap:8px}.pipeline-arrow{display:none}.pipeline-stage{flex:1 1 calc(50% - 4px);min-width:100px}.tier-chart .donut-container,.donut-container{flex-direction:column;align-items:center}.sessions-table{font-size:.75rem}.sessions-table th,.sessions-table td{padding:8px 6px}}.convoy-overview{display:flex;flex-wrap:wrap;gap:24px;margin-bottom:20px}.convoy-stat{display:flex;flex-direction:column;gap:4px}.convoy-stat__label{font-size:.75rem;color:var(--text-tertiary);text-transform:uppercase;letter-spacing:.05em}.convoy-stat__value{font-size:.95rem;color:var(--text-primary)}.convoy-progress{display:flex;align-items:center;gap:12px;margin-bottom:20px}.convoy-progress__bar{flex:1;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.convoy-progress__fill{height:100%;background:var(--gradient-accent);border-radius:4px;transition:width var(--transition-base)}.convoy-progress__label{font-size:.8rem;color:var(--text-secondary);white-space:nowrap}.convoy-tasks{margin-top:8px}