@dupecom/botcha-cloudflare 0.10.0 โ†’ 0.11.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.
package/README.md CHANGED
@@ -1,18 +1,19 @@
1
1
  # @dupecom/botcha-cloudflare
2
2
 
3
- > ๐Ÿค– **BOTCHA** - Prove you're a bot. Humans need not apply.
3
+ > **BOTCHA** - Prove you're a bot. Humans need not apply.
4
4
  >
5
- > **Cloudflare Workers Edition v0.2.0** - Production-ready with JWT & Rate Limiting
5
+ > **Cloudflare Workers Edition v0.11.0** - Identity layer for AI agents
6
6
 
7
7
  Reverse CAPTCHA that verifies AI agents and blocks humans. Running at the edge.
8
8
 
9
- ## ๐Ÿš€ What's New in v0.2.0
9
+ ## What's New in v0.11.0
10
10
 
11
- - โœ… **JWT Token Authentication** - Secure token-based auth flow with 1-hour expiry
12
- - โœ… **Rate Limiting** - 100 challenges/hour/IP with proper headers
13
- - โœ… **KV Storage** - Challenge state stored in Cloudflare KV (prevents replay attacks)
14
- - โœ… **Versioned API** - New `/v1/*` endpoints with backward-compatible legacy routes
15
- - โœ… **Production Ready** - Enterprise-grade auth and security
11
+ - **Agent Registry** - Persistent agent identities (POST /v1/agents/register)
12
+ - **Email-tied apps** - Email verification, account recovery, secret rotation
13
+ - **Dashboard** - Per-app metrics with agent-first auth (device code flow)
14
+ - **JWT security** - 5-min access tokens, refresh tokens, audience claims, IP binding, revocation
15
+ - **Multi-tenant** - Per-app isolation, scoped tokens, rate limiting
16
+ - **Server-side SDKs** - @botcha/verify (TS) + botcha-verify (Python)
16
17
 
17
18
  ## Features
18
19
 
@@ -0,0 +1,68 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ export type KVNamespace = {
11
+ get: (key: string, type?: 'text' | 'json' | 'arrayBuffer' | 'stream') => Promise<any>;
12
+ put: (key: string, value: string, options?: {
13
+ expirationTtl?: number;
14
+ }) => Promise<void>;
15
+ delete: (key: string) => Promise<void>;
16
+ };
17
+ /**
18
+ * Agent record stored in KV
19
+ */
20
+ export interface Agent {
21
+ agent_id: string;
22
+ app_id: string;
23
+ name: string;
24
+ operator?: string;
25
+ version?: string;
26
+ created_at: number;
27
+ }
28
+ /**
29
+ * Generate a crypto-random agent ID
30
+ * Format: 'agent_' + 16 hex chars
31
+ *
32
+ * Example: agent_a1b2c3d4e5f6a7b8
33
+ */
34
+ export declare function generateAgentId(): string;
35
+ /**
36
+ * Create a new agent and add it to the app's agent list
37
+ *
38
+ * Stores in KV at:
39
+ * - `agent:{agent_id}` โ€” Agent record
40
+ * - `agents:{app_id}` โ€” Array of agent_ids for this app
41
+ *
42
+ * @param kv - KV namespace for storage
43
+ * @param app_id - Parent app that owns this agent
44
+ * @param data - Agent metadata (name, operator, version)
45
+ * @returns Agent record with generated agent_id
46
+ */
47
+ export declare function createAgent(kv: KVNamespace, app_id: string, data: {
48
+ name: string;
49
+ operator?: string;
50
+ version?: string;
51
+ }): Promise<Agent | null>;
52
+ /**
53
+ * Get agent by agent_id
54
+ *
55
+ * @param kv - KV namespace
56
+ * @param agent_id - The agent ID to retrieve
57
+ * @returns Agent record or null if not found
58
+ */
59
+ export declare function getAgent(kv: KVNamespace, agent_id: string): Promise<Agent | null>;
60
+ /**
61
+ * List all agents for an app
62
+ *
63
+ * @param kv - KV namespace
64
+ * @param app_id - The app ID to list agents for
65
+ * @returns Array of agent records (empty array if none found)
66
+ */
67
+ export declare function listAgents(kv: KVNamespace, app_id: string): Promise<Agent[]>;
68
+ //# sourceMappingURL=agents.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../src/agents.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,aAAa,GAAG,QAAQ,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;IACtF,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACzF,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAIF;;GAEG;AACH,MAAM,WAAW,KAAK;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;CACpB;AAID;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAOxC;AAID;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1D,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAwCvB;AAED;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAC5B,EAAE,EAAE,WAAW,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,KAAK,GAAG,IAAI,CAAC,CAcvB;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,KAAK,EAAE,CAAC,CAsBlB"}
package/dist/agents.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * BOTCHA Agent Registry
3
+ *
4
+ * Agent registration and management:
5
+ * - Crypto-random agent IDs
6
+ * - KV storage for agent metadata
7
+ * - App-scoped agent lists
8
+ * - Fail-open design for resilience
9
+ */
10
+ // ============ CRYPTO UTILITIES ============
11
+ /**
12
+ * Generate a crypto-random agent ID
13
+ * Format: 'agent_' + 16 hex chars
14
+ *
15
+ * Example: agent_a1b2c3d4e5f6a7b8
16
+ */
17
+ export function generateAgentId() {
18
+ const bytes = new Uint8Array(8); // 8 bytes = 16 hex chars
19
+ crypto.getRandomValues(bytes);
20
+ const hexString = Array.from(bytes)
21
+ .map(b => b.toString(16).padStart(2, '0'))
22
+ .join('');
23
+ return `agent_${hexString}`;
24
+ }
25
+ // ============ AGENT MANAGEMENT ============
26
+ /**
27
+ * Create a new agent and add it to the app's agent list
28
+ *
29
+ * Stores in KV at:
30
+ * - `agent:{agent_id}` โ€” Agent record
31
+ * - `agents:{app_id}` โ€” Array of agent_ids for this app
32
+ *
33
+ * @param kv - KV namespace for storage
34
+ * @param app_id - Parent app that owns this agent
35
+ * @param data - Agent metadata (name, operator, version)
36
+ * @returns Agent record with generated agent_id
37
+ */
38
+ export async function createAgent(kv, app_id, data) {
39
+ try {
40
+ const agent_id = generateAgentId();
41
+ const agent = {
42
+ agent_id,
43
+ app_id,
44
+ name: data.name,
45
+ operator: data.operator,
46
+ version: data.version,
47
+ created_at: Date.now(),
48
+ };
49
+ // Get existing agent list for this app (if any)
50
+ let agentIds = [];
51
+ try {
52
+ const existingList = await kv.get(`agents:${app_id}`, 'text');
53
+ if (existingList) {
54
+ agentIds = JSON.parse(existingList);
55
+ }
56
+ }
57
+ catch (error) {
58
+ console.warn(`Failed to fetch existing agent list for app ${app_id}, starting fresh:`, error);
59
+ // Fail-open: Continue with empty list
60
+ }
61
+ // Add new agent_id to the list
62
+ agentIds.push(agent_id);
63
+ // Store agent record and updated list in parallel
64
+ await Promise.all([
65
+ kv.put(`agent:${agent_id}`, JSON.stringify(agent)),
66
+ kv.put(`agents:${app_id}`, JSON.stringify(agentIds)),
67
+ ]);
68
+ return agent;
69
+ }
70
+ catch (error) {
71
+ console.error(`Failed to create agent for app ${app_id}:`, error);
72
+ // Fail-open: Return null instead of throwing
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Get agent by agent_id
78
+ *
79
+ * @param kv - KV namespace
80
+ * @param agent_id - The agent ID to retrieve
81
+ * @returns Agent record or null if not found
82
+ */
83
+ export async function getAgent(kv, agent_id) {
84
+ try {
85
+ const data = await kv.get(`agent:${agent_id}`, 'text');
86
+ if (!data) {
87
+ return null;
88
+ }
89
+ return JSON.parse(data);
90
+ }
91
+ catch (error) {
92
+ console.error(`Failed to get agent ${agent_id}:`, error);
93
+ // Fail-open: Return null instead of throwing
94
+ return null;
95
+ }
96
+ }
97
+ /**
98
+ * List all agents for an app
99
+ *
100
+ * @param kv - KV namespace
101
+ * @param app_id - The app ID to list agents for
102
+ * @returns Array of agent records (empty array if none found)
103
+ */
104
+ export async function listAgents(kv, app_id) {
105
+ try {
106
+ // Get the list of agent IDs for this app
107
+ const agentIdsData = await kv.get(`agents:${app_id}`, 'text');
108
+ if (!agentIdsData) {
109
+ return [];
110
+ }
111
+ const agentIds = JSON.parse(agentIdsData);
112
+ // Fetch all agent records in parallel
113
+ const agentPromises = agentIds.map(agent_id => getAgent(kv, agent_id));
114
+ const agents = await Promise.all(agentPromises);
115
+ // Filter out any null results (failed fetches) and return
116
+ return agents.filter((agent) => agent !== null);
117
+ }
118
+ catch (error) {
119
+ console.error(`Failed to list agents for app ${app_id}:`, error);
120
+ // Fail-open: Return empty array instead of throwing
121
+ return [];
122
+ }
123
+ }
@@ -472,8 +472,15 @@ function formatTimeBucket(timestamp, period) {
472
472
  }
473
473
  }
474
474
  // ============ MOCK DATA (when CF_API_TOKEN not configured) ============
475
- function renderMockOverview(_period) {
476
- return `<div class="dashboard-grid">
475
+ function renderSampleBanner() {
476
+ return `<div class="sample-banner">Sample data โ€” analytics will appear here once production traffic flows</div>`;
477
+ }
478
+ function renderSampleLegend(title, period) {
479
+ return `${title} (${period})<span class="sample-tag">SAMPLE</span>`;
480
+ }
481
+ function renderMockOverview(period) {
482
+ return `${renderSampleBanner()}
483
+ <div class="dashboard-grid">
477
484
  ${renderStatCard('1,247', 'Challenges Generated')}
478
485
  ${renderStatCard('1,089', 'Verifications')}
479
486
  ${renderStatCard('94%', 'Success Rate', 'text-success')}
@@ -482,7 +489,7 @@ function renderMockOverview(_period) {
482
489
  ${renderStatCard('0', 'Errors')}
483
490
  </div>`;
484
491
  }
485
- function renderMockVolume(_period) {
492
+ function renderMockVolume(period) {
486
493
  const items = [
487
494
  { name: '00:00', value: 42, maxValue: 89 },
488
495
  { name: '04:00', value: 15, maxValue: 89 },
@@ -492,11 +499,11 @@ function renderMockVolume(_period) {
492
499
  { name: '20:00', value: 55, maxValue: 89 },
493
500
  ];
494
501
  return `<fieldset>
495
- <legend>Request Volume (sample)</legend>
502
+ <legend>${renderSampleLegend('Request Volume', period)}</legend>
496
503
  ${renderBarChart(items)}
497
504
  </fieldset>`;
498
505
  }
499
- function renderMockTypes(_period) {
506
+ function renderMockTypes(period) {
500
507
  const items = [
501
508
  { name: 'hybrid (412 ok / 18 fail)', value: 430, maxValue: 430 },
502
509
  { name: 'speed (389 ok / 12 fail)', value: 401, maxValue: 430 },
@@ -504,13 +511,13 @@ function renderMockTypes(_period) {
504
511
  { name: 'standard (22 ok / 5 fail)', value: 27, maxValue: 430 },
505
512
  ];
506
513
  return `<fieldset>
507
- <legend>Challenge Types (sample)</legend>
514
+ <legend>${renderSampleLegend('Challenge Types', period)}</legend>
508
515
  ${renderBarChart(items)}
509
516
  </fieldset>`;
510
517
  }
511
- function renderMockPerformance(_period) {
518
+ function renderMockPerformance(period) {
512
519
  return `<fieldset>
513
- <legend>Performance (sample)</legend>
520
+ <legend>${renderSampleLegend('Performance', period)}</legend>
514
521
  <table>
515
522
  <thead>
516
523
  <tr>
@@ -525,13 +532,13 @@ function renderMockPerformance(_period) {
525
532
  </table>
526
533
  </fieldset>`;
527
534
  }
528
- function renderMockErrors(_period) {
535
+ function renderMockErrors(period) {
529
536
  return `<fieldset>
530
- <legend>Errors & Rate Limits (sample)</legend>
531
- <div class="alert alert-success">No errors or rate limits (sample data)</div>
537
+ <legend>${renderSampleLegend('Errors & Rate Limits', period)}</legend>
538
+ <div class="alert alert-success">No errors or rate limits</div>
532
539
  </fieldset>`;
533
540
  }
534
- function renderMockGeo(_period) {
541
+ function renderMockGeo(period) {
535
542
  const items = [
536
543
  { name: 'US', value: 523, maxValue: 523 },
537
544
  { name: 'DE', value: 189, maxValue: 523 },
@@ -540,7 +547,7 @@ function renderMockGeo(_period) {
540
547
  { name: 'FR', value: 67, maxValue: 523 },
541
548
  ];
542
549
  return `<fieldset>
543
- <legend>Top Countries (sample)</legend>
550
+ <legend>${renderSampleLegend('Top Countries', period)}</legend>
544
551
  ${renderBarChart(items)}
545
552
  </fieldset>`;
546
553
  }
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAsCF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2B9E;AAID;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAkE5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAqB9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AAID;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA0HvE"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/dashboard/auth.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE,MAAM,MAAM,CAAC;AAMvD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAKjD,KAAK,QAAQ,GAAG;IACd,UAAU,EAAE,WAAW,CAAC;IACxB,WAAW,EAAE,WAAW,CAAC;IACzB,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAGF,KAAK,SAAS,GAAG;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AAsCF;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,iBAAiB,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAC;IAAC,SAAS,EAAE,SAAS,CAAA;CAAE,CAmChG,CAAC;AAIF;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2CpF;AAgDD;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;;oEAwBjF;AAID;;;;GAIG;AACH,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEAEjF;AAED;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;;;;;;;;;;oEA2B9E;AAID;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBAkE5E;AAED;;;;GAIG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAqB9E;AAID;;;;;GAKG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAyBnE;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFAGpE;AAID;;;;;;;;;;;;GAYG;AACH,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,gFA2BxE;AAID;;;;;;;;GAQG;AACH,wBAAsB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC;IAAE,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,qBA6HvE"}
@@ -392,10 +392,10 @@ export async function renderLoginPage(c) {
392
392
  document.querySelector('form').submit();
393
393
  }
394
394
  `;
395
- return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("div", { class: "ascii-logo", children: ` ____ ___ _____ ____ _ _ _
396
- | __ ) / _ \\_ _/ ___| | | | / \\
397
- | _ \\| | | || || | | |_| |/ _ \\
398
- | |_) | |_| || || |___| _ / ___ \\
399
- |____/ \\___/ |_| \\____|_| |_/_/ \\_\\
400
- >_ prove you're a bot` }), _jsxs(Card, { title: "Device Code", badge: "agent required", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Your AI agent can generate a login code for you." }), _jsxs("a", { href: "/dashboard/code", class: "button btn", children: ["Enter Device Code ", '>'] }), _jsxs("div", { class: "hint", children: ["Agent: ", _jsx("code", { children: "POST /v1/auth/device-code" }), " then solve the challenge."] })] }), _jsx(Divider, { text: "or" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", badge: "returning users", children: [error === 'email_missing' && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Enter the email you used when creating your app. We'll send a login code to your inbox." }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "email", children: "Email" }), _jsx("input", { type: "email", id: "email", name: "email", placeholder: "you@example.com", required: true, autocomplete: "email" })] }), _jsxs("button", { type: "submit", children: ["Email Me a Code ", '>'] })] }) }), _jsx(Divider, { text: "or sign in with credentials" }), _jsx("form", { method: "post", action: "/dashboard/login", children: _jsxs(Card, { title: "App Credentials", children: [error && error !== 'email_missing' && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsxs("div", { id: "create-result", children: [_jsx("div", { class: "warning", children: "Save these credentials now. The secret will not be shown again." }), _jsxs("div", { class: "credentials-box", children: [_jsx("span", { class: "label", children: "app_id: " }), _jsx("span", { class: "value", id: "new-app-id" }), _jsx("br", {}), _jsx("span", { class: "label", children: "secret: " }), _jsx("span", { class: "value", id: "new-app-secret" })] }), _jsxs("button", { type: "button", onclick: "fillAndLogin()", style: "width: 100%; margin-bottom: 1rem;", children: ["Login With New Credentials ", '>'] })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_id", children: "App ID" }), _jsx("input", { type: "text", id: "app_id", name: "app_id", placeholder: "app_...", required: true, autocomplete: "username" })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_secret", children: "App Secret" }), _jsx("input", { type: "password", id: "app_secret", name: "app_secret", placeholder: "sk_...", required: true, autocomplete: "current-password" })] }), _jsxs("div", { style: "display: flex; gap: 0.75rem; align-items: center;", children: [_jsxs("button", { type: "submit", children: ["Login ", '>'] }), _jsxs("button", { type: "button", id: "create-btn", class: "btn-secondary", onclick: "createApp()", children: ["Create App ", '>'] })] })] }) }), _jsx("script", { dangerouslySetInnerHTML: { __html: CREATE_APP_SCRIPT } })] }));
395
+ return c.html(_jsxs(LoginLayout, { title: "Dashboard Login - BOTCHA", children: [_jsx("a", { href: "/", class: "ascii-logo", children: `โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
396
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
397
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘
398
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘
399
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
400
+ โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•` }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 2rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsxs(Card, { title: "Device Code", badge: "agent required", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Your AI agent can generate a login code for you." }), _jsxs("a", { href: "/dashboard/code", class: "button btn", children: ["Enter Device Code ", '>'] }), _jsxs("div", { class: "hint", children: ["Agent: ", _jsx("code", { children: "POST /v1/auth/device-code" }), " then solve the challenge."] })] }), _jsx(Divider, { text: "or" }), _jsx("form", { method: "post", action: "/dashboard/email-login", children: _jsxs(Card, { title: "Email Login", badge: "returning users", children: [error === 'email_missing' && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Enter the email you used when creating your app. We'll send a login code to your inbox." }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "email", children: "Email" }), _jsx("input", { type: "email", id: "email", name: "email", placeholder: "you@example.com", required: true, autocomplete: "email" })] }), _jsxs("button", { type: "submit", children: ["Email Me a Code ", '>'] })] }) }), _jsx(Divider, { text: "or sign in with credentials" }), _jsx("form", { method: "post", action: "/dashboard/login", children: _jsxs(Card, { title: "App Credentials", children: [error && error !== 'email_missing' && errorMap[error] && (_jsx("div", { class: "error-message", children: errorMap[error] })), _jsxs("div", { id: "create-result", children: [_jsx("div", { class: "warning", children: "Save these credentials now. The secret will not be shown again." }), _jsxs("div", { class: "credentials-box", children: [_jsx("span", { class: "label", children: "app_id: " }), _jsx("span", { class: "value", id: "new-app-id" }), _jsx("br", {}), _jsx("span", { class: "label", children: "secret: " }), _jsx("span", { class: "value", id: "new-app-secret" })] }), _jsxs("button", { type: "button", onclick: "fillAndLogin()", style: "width: 100%; margin-bottom: 1rem;", children: ["Login With New Credentials ", '>'] })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_id", children: "App ID" }), _jsx("input", { type: "text", id: "app_id", name: "app_id", placeholder: "app_...", required: true, autocomplete: "username" })] }), _jsxs("div", { class: "form-group", children: [_jsx("label", { for: "app_secret", children: "App Secret" }), _jsx("input", { type: "password", id: "app_secret", name: "app_secret", placeholder: "sk_...", required: true, autocomplete: "current-password" })] }), _jsxs("div", { style: "display: flex; gap: 0.75rem; align-items: center;", children: [_jsxs("button", { type: "submit", children: ["Login ", '>'] }), _jsxs("button", { type: "button", id: "create-btn", class: "btn-secondary", onclick: "createApp()", children: ["Create App ", '>'] })] })] }) }), _jsx("script", { dangerouslySetInnerHTML: { __html: CREATE_APP_SCRIPT } })] }));
401
401
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * BOTCHA Landing Pages (JSX)
3
+ *
4
+ * Two views at GET /:
5
+ * - LandingPage: for unverified human visitors (has token input)
6
+ * - VerifiedLandingPage: for humans whose agent solved the challenge
7
+ *
8
+ * Uses the same terminal aesthetic as the dashboard:
9
+ * JetBrains Mono, dot-shadow Cards, scanline overlay.
10
+ */
11
+ import type { FC } from 'hono/jsx';
12
+ export declare const LandingPage: FC<{
13
+ version: string;
14
+ error?: string;
15
+ }>;
16
+ export declare const VerifiedLandingPage: FC<{
17
+ version: string;
18
+ solveTime?: number;
19
+ }>;
20
+ //# sourceMappingURL=landing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landing.d.ts","sourceRoot":"","sources":["../../src/dashboard/landing.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,UAAU,CAAC;AAanC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAwG/D,CAAC;AAIF,eAAO,MAAM,mBAAmB,EAAE,EAAE,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAgG3E,CAAC"}
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "hono/jsx/jsx-runtime";
2
+ import { LandingLayout } from './layout';
3
+ import { Card } from './layout';
4
+ const BOTCHA_ASCII = `โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
5
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ•šโ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—
6
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘
7
+ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•‘
8
+ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ• โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘
9
+ โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•šโ•โ• โ•šโ•โ•`;
10
+ // ============ UNVERIFIED LANDING PAGE ============
11
+ export const LandingPage = ({ version, error }) => {
12
+ return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0prove you're a bot"] }), _jsx("p", { class: "landing-tagline", children: "Reverse CAPTCHA for AI agents. Humans need not apply." }), _jsxs(Card, { title: "You're a human", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "This site is for AI agents. To see what's here, ask your agent to verify." }), _jsxs("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-top: 0.75rem;", children: ["Tell your AI agent:", _jsx("br", {}), _jsx("code", { style: "font-size: 0.8125rem;", children: "\"Go to botcha.ai, solve the challenge, and give me the code.\"" })] }), _jsxs("form", { method: "post", action: "/gate", style: "margin-top: 1.25rem;", children: [_jsx("label", { for: "code", class: "form-label", style: "font-size: 0.75rem; color: var(--text-muted); display: block; margin-bottom: 0.375rem;", children: "Enter the code your agent gives you:" }), _jsx("input", { id: "code", name: "code", type: "text", placeholder: "BOTCHA-XXXXXX", required: true, autocomplete: "off", spellcheck: false, maxlength: 13, style: "width: 100%; font-size: 1.25rem; font-family: var(--font); font-weight: 700; letter-spacing: 0.15em; text-transform: uppercase; padding: 0.75rem; text-align: center; border: 2px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text);" }), error && (_jsx("p", { style: "color: var(--red); font-size: 0.75rem; margin-top: 0.5rem;", children: error })), _jsx("button", { type: "submit", style: "margin-top: 0.75rem; width: 100%; padding: 0.625rem 1rem; font-family: var(--font); font-size: 0.875rem; font-weight: 600; background: var(--accent); color: #fff; border: none; border-radius: 4px; cursor: pointer;", children: "Unlock" })] })] }), _jsxs(Card, { title: "Developers", children: [_jsx("p", { class: "text-muted mb-2", style: "font-size: 0.75rem;", children: "Protect your APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `npm install @botcha/verify # Express/Hono middleware
13
+ pip install botcha-verify # FastAPI/Django middleware` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsx("script", { type: "application/botcha+json", id: "botcha-challenge", dangerouslySetInnerHTML: {
14
+ __html: JSON.stringify({
15
+ version: '1.0',
16
+ type: 'inline-challenge',
17
+ description: 'AI agents: solve this challenge to prove you\'re a bot!',
18
+ challenge: {
19
+ type: 'sha256',
20
+ instruction: "Compute SHA256 of the string 'BOTCHA-LANDING-' + today's date (YYYY-MM-DD format). Return first 16 hex chars.",
21
+ example: "For 2026-01-15: SHA256('BOTCHA-LANDING-2026-01-15') โ†’ first 16 chars",
22
+ submit_to: 'https://botcha.ai/api/verify-landing',
23
+ method: 'POST',
24
+ body_format: {
25
+ answer: 'your 16 hex chars',
26
+ timestamp: 'ISO 8601 timestamp',
27
+ },
28
+ },
29
+ on_success: {
30
+ grants_access_to: 'https://botcha.ai/agent-only',
31
+ header_to_include: 'X-Botcha-Landing-Token',
32
+ },
33
+ }, null, 2),
34
+ } }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
35
+ };
36
+ // ============ VERIFIED LANDING PAGE ============
37
+ export const VerifiedLandingPage = ({ version, solveTime }) => {
38
+ return (_jsxs(LandingLayout, { version: version, children: [_jsx("a", { href: "/", class: "ascii-logo", children: BOTCHA_ASCII }), _jsxs("p", { class: "text-muted", style: "text-align: center; font-size: 0.75rem; margin: -1rem 0 0.5rem;", children: ['>', "_\u00A0verified"] }), _jsxs("p", { class: "landing-tagline", style: "color: var(--green);", children: ["Your agent proved it's a bot", solveTime ? ` in ${solveTime}ms` : '', ". Welcome."] }), _jsxs(Card, { title: "Get started", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "You have an AI agent. Here's what to do with it." }), _jsx("p", { class: "text-muted", style: "font-size: 0.75rem; line-height: 1.6; margin-top: 0.5rem; font-style: italic;", children: "Copy any of these prompts and paste them to your agent:" })] }), _jsxs(Card, { title: "1. Create your app", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Tell your agent to create a BOTCHA app tied to your email. This gives you an identity on the platform \u2014 your agent gets API keys, you get a dashboard." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsxs("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: ["\"Go to botcha.ai and create an app for me. My email is ", _jsx("span", { contentEditable: "plaintext-only", spellcheck: false, style: "color: var(--green); border-bottom: 1px dashed var(--green); outline: none; min-width: 3ch; padding: 0 2px;", children: "you@example.com" }), ". Save the app_id and app_secret somewhere safe.\""] })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "Your agent will call the API, get your credentials, and a verification code will be emailed to you." })] }), _jsxs(Card, { title: "2. Verify your email", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Check your inbox for a 6-digit code from BOTCHA. Give it to your agent to confirm your email." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"The verification code from BOTCHA is [code]. Verify my email.\"" })] }), _jsx("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: "This enables account recovery if you ever lose your credentials." })] }), _jsxs(Card, { title: "3. Open your dashboard", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7;", children: "Your agent can give you a short code to access your management dashboard \u2014 usage stats, API keys, and settings." }), _jsxs("div", { style: "margin-top: 0.75rem; padding: 0.75rem; background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 6px;", children: [_jsx("p", { style: "font-size: 0.625rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-muted); margin-bottom: 0.375rem;", children: "Say this to your agent:" }), _jsx("code", { style: "font-size: 0.8125rem; line-height: 1.6; color: var(--accent);", children: "\"Get me a dashboard code for BOTCHA.\"" })] }), _jsxs("p", { class: "text-muted", style: "font-size: 0.6875rem; margin-top: 0.5rem;", children: ["Your agent gives you a ", _jsx("code", { children: "BOTCHA-XXXX" }), " code. Enter it at ", _jsx("a", { href: "/dashboard/code", style: "font-weight: 600;", children: "/dashboard/code" }), "."] })] }), _jsxs(Card, { title: "For developers", children: [_jsx("p", { class: "text-muted", style: "font-size: 0.8125rem; line-height: 1.7; margin-bottom: 0.75rem;", children: "Protect your own APIs so only verified AI agents can access them:" }), _jsx("pre", { children: _jsx("code", { children: `# Client SDK (for your agent)
39
+ npm install @dupecom/botcha # TypeScript
40
+ pip install botcha # Python
41
+
42
+ # Server SDK (protect your APIs)
43
+ npm install @botcha/verify # Express/Hono
44
+ pip install botcha-verify # FastAPI/Django` }) }), _jsxs("div", { class: "landing-links", style: "margin-top: 1rem;", children: [_jsx("a", { href: "/openapi.json", class: "landing-link", children: "OpenAPI" }), _jsx("a", { href: "/ai.txt", class: "landing-link", children: "ai.txt" }), _jsx("a", { href: "https://github.com/dupe-com/botcha", class: "landing-link", children: "GitHub" }), _jsx("a", { href: "https://www.npmjs.com/package/@dupecom/botcha", class: "landing-link", children: "npm" }), _jsx("a", { href: "https://pypi.org/project/botcha/", class: "landing-link", children: "PyPI" })] })] }), _jsxs("div", { class: "landing-footer", children: [_jsxs("span", { children: ["v", version] }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://botcha.ai", children: "botcha.ai" }), _jsx("span", { class: "landing-footer-sep", children: "\u00B7" }), _jsx("a", { href: "https://github.com/i8ramin", children: "@i8ramin" })] })] }));
45
+ };
@@ -44,4 +44,11 @@ export declare const DashboardLayout: FC<PropsWithChildren<{
44
44
  export declare const LoginLayout: FC<PropsWithChildren<{
45
45
  title?: string;
46
46
  }>>;
47
+ /**
48
+ * Landing page layout โ€” wider than LoginLayout, includes SEO meta tags.
49
+ * Used for the public landing page at GET /
50
+ */
51
+ export declare const LandingLayout: FC<PropsWithChildren<{
52
+ version: string;
53
+ }>>;
47
54
  //# sourceMappingURL=layout.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC"}
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/dashboard/layout.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAKtD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmBzF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAExC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAmCrF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAsBjE,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,iBAAiB,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAoDpE,CAAC"}
@@ -36,3 +36,20 @@ export const DashboardLayout = ({ children, title, appId }) => {
36
36
  export const LoginLayout = ({ children, title }) => {
37
37
  return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: title || 'BOTCHA Login' }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } }), _jsx("script", { src: "https://unpkg.com/htmx.org@2.0.4" })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "login-box", children: children }) }) })] }));
38
38
  };
39
+ /**
40
+ * Landing page layout โ€” wider than LoginLayout, includes SEO meta tags.
41
+ * Used for the public landing page at GET /
42
+ */
43
+ export const LandingLayout = ({ children, version }) => {
44
+ return (_jsxs("html", { lang: "en", children: [_jsxs("head", { children: [_jsx("meta", { charset: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { name: "description", content: "BOTCHA is a hosted reverse CAPTCHA that verifies AI agents, not humans. Protect your APIs with computational challenges only bots can solve." }), _jsx("meta", { name: "keywords", content: "AI, bot verification, reverse CAPTCHA, API security, AI agents, agent verification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/openapi.json", title: "OpenAPI Specification" }), _jsx("link", { rel: "alternate", type: "application/json", href: "/.well-known/ai-plugin.json", title: "AI Plugin Manifest" }), _jsx("link", { rel: "botcha-challenge", href: "#botcha-challenge", type: "application/botcha+json", title: "Embedded Bot Challenge" }), _jsx("meta", { name: "ai-agent-welcome", content: "true" }), _jsx("meta", { name: "botcha-challenge", content: "embedded", "data-selector": "script[type='application/botcha+json']" }), _jsx("meta", { property: "og:title", content: "BOTCHA \u2014 Reverse CAPTCHA for AI Agents" }), _jsx("meta", { property: "og:description", content: "Hosted bot verification service. Prove you're a bot. Humans need not apply." }), _jsx("meta", { property: "og:url", content: "https://botcha.ai" }), _jsx("meta", { property: "og:type", content: "website" }), _jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify({
45
+ '@context': 'https://schema.org',
46
+ '@type': 'SoftwareApplication',
47
+ name: 'BOTCHA',
48
+ applicationCategory: 'DeveloperApplication',
49
+ description: 'Hosted reverse CAPTCHA for AI agents. Computational challenges that only bots can solve.',
50
+ url: 'https://botcha.ai',
51
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
52
+ operatingSystem: 'Any',
53
+ softwareVersion: version,
54
+ }) } }), _jsx("link", { rel: "preconnect", href: "https://fonts.googleapis.com" }), _jsx("link", { href: "https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap", rel: "stylesheet" }), _jsx("style", { dangerouslySetInnerHTML: { __html: DASHBOARD_CSS } })] }), _jsx("body", { children: _jsx("div", { class: "login-container", children: _jsx("div", { class: "landing-box", children: children }) }) })] }));
55
+ };
@@ -7,5 +7,5 @@
7
7
  *
8
8
  * JetBrains Mono ยท #ffffff bg ยท #1a1a1a text ยท black accent ยท square corners ยท dot shadows
9
9
  */
10
- export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n }\n";
10
+ export declare const DASHBOARD_CSS = "\n /* ============ Reset ============ */\n * { margin: 0; padding: 0; box-sizing: border-box; }\n\n :root {\n /* ---- palette (black & white) ---- */\n --bg: #ffffff;\n --bg-card: #ffffff;\n --bg-raised: #eae8e4;\n --text: #1a1a1a;\n --text-muted: #6b6b6b;\n --text-dim: #a0a0a0;\n --accent: #1a1a1a;\n --accent-dim: #333333;\n --red: #cc2222;\n --amber: #b87a00;\n --green: #1a8a2a;\n --border: #ddd9d4;\n --border-bright: #c0bbb5;\n\n /* ---- type ---- */\n --font: 'JetBrains Mono', 'Courier New', monospace;\n\n /* ---- dot shadow (turbopuffer SVG pattern, black fill) ---- */\n --dot-shadow: url(\"data:image/svg+xml,%3Csvg width='7' height='13' viewBox='0 0 7 13' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5.58984 12.2344V10.7051H6.52734V12.2344H5.58984ZM1.86328 12.2344V10.7051H2.79492V12.2344H1.86328ZM3.72656 10.0957V8.56641H4.6582V10.0957H3.72656ZM0 10.0957V8.56641H0.925781V10.0957H0ZM5.58984 7.95117V6.42188H6.52734V7.95117H5.58984ZM1.86328 7.95117V6.42188H2.79492V7.95117H1.86328ZM3.72656 5.8125V4.2832H4.6582V5.8125H3.72656ZM0 5.8125V4.2832H0.925781V5.8125H0ZM5.58984 3.66797V2.13867H6.52734V3.66797H5.58984ZM1.86328 3.66797V2.13867H2.79492V3.66797H1.86328ZM3.72656 1.5293V0H4.6582V1.5293H3.72656ZM0 1.5293V0H0.925781V1.5293H0Z' fill='%231a1a1a'/%3E%3C/svg%3E\");\n }\n\n /* ============ Base ============ */\n html, body {\n height: 100%;\n font-family: var(--font);\n font-size: 16px;\n line-height: 1.6;\n background: var(--bg);\n color: var(--text);\n -webkit-font-smoothing: antialiased;\n }\n\n body { display: flex; flex-direction: column; }\n\n ::selection { background: var(--accent); color: #fff; }\n\n a { color: var(--accent); }\n a:hover { text-decoration: none; opacity: 0.65; }\n\n /* ============ Scanline overlay (subtle CRT feel) ============ */\n body::before {\n content: '';\n position: fixed;\n inset: 0;\n background: repeating-linear-gradient(\n 0deg,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.012) 2px,\n rgba(0, 0, 0, 0.012) 4px\n );\n pointer-events: none;\n z-index: 9999;\n }\n\n /* ============ Dot shadow utility ============ */\n .dot-shadow { position: relative; }\n .dot-shadow::after {\n content: '';\n position: absolute;\n top: 0.5rem; left: 0.5rem;\n right: -0.5rem; bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n z-index: -1;\n pointer-events: none;\n opacity: 0.6;\n }\n\n /* ============ Navigation ============ */\n .dashboard-nav {\n background: var(--bg);\n border-bottom: 1px solid var(--border);\n position: sticky; top: 0; z-index: 100;\n }\n\n .nav-container {\n max-width: 1200px;\n margin: 0 auto;\n padding: 0.75rem 1.5rem;\n display: flex;\n align-items: center;\n gap: 1.5rem;\n }\n\n .nav-logo {\n font-weight: 700;\n font-size: 0.875rem;\n color: var(--text);\n text-decoration: none;\n letter-spacing: 0.15em;\n text-transform: uppercase;\n }\n .nav-logo:hover { opacity: 1; }\n\n .nav-app-id {\n color: var(--text-muted);\n font-size: 0.75rem;\n margin-left: auto;\n }\n\n .nav-link {\n color: var(--text);\n text-decoration: none;\n font-size: 0.75rem;\n border: 1px solid var(--border-bright);\n padding: 0.25rem 0.75rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n transition: background 0.1s, color 0.1s;\n }\n .nav-link:hover {\n background: var(--accent);\n color: var(--bg);\n border-color: var(--accent);\n opacity: 1;\n }\n\n /* ============ Main content ============ */\n .dashboard-main {\n flex: 1;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 2rem 1.5rem;\n }\n\n /* ============ Card \u2014 primary container (Turbopuffer-style) ============ */\n .card {\n display: flex;\n flex-direction: column;\n margin-bottom: 1.5rem;\n }\n\n .card-header {\n margin-bottom: -1px; /* overlap the border */\n padding: 0;\n }\n\n .card-header h3 {\n position: relative;\n display: inline-flex;\n align-items: center;\n z-index: 10;\n top: 0.5rem;\n left: 0.5rem;\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n line-height: 1;\n color: var(--text);\n background: var(--bg);\n margin: 0;\n padding: 0 0.5rem;\n }\n\n .card-header h3 .card-title {\n }\n\n .card-header h3 .badge-inline {\n }\n\n .card-body {\n position: relative;\n border: 2px solid var(--border-bright);\n }\n\n .card-body::before {\n content: '';\n position: absolute;\n top: 0.5rem;\n left: 0.5rem;\n right: -0.5rem;\n bottom: -0.5rem;\n background-image: var(--dot-shadow);\n background-repeat: repeat;\n pointer-events: none;\n opacity: 0.6;\n }\n\n .card-inner {\n position: relative;\n z-index: 1;\n background: var(--bg-card);\n padding: 1.5rem;\n }\n\n /* Legacy fieldset support (deprecated \u2014 use .card instead) */\n fieldset {\n border: 2px solid var(--border-bright);\n border-radius: 0;\n padding: 1.5rem;\n margin-bottom: 1.5rem;\n background: var(--bg-card);\n position: relative;\n z-index: 0;\n }\n\n legend {\n padding: 0 0.5rem;\n font-size: 0.75rem;\n color: var(--text);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n /* ============ Dashboard grid ============ */\n .dashboard-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));\n gap: 1.5rem;\n margin-bottom: 1.5rem;\n }\n\n /* ============ Stat cards ============ */\n .stat-card {\n display: flex; flex-direction: column; gap: 0.25rem;\n padding: 1rem;\n border: 1px solid var(--border);\n background: var(--bg-card);\n }\n\n .stat-card .stat-value {\n font-size: 2rem; font-weight: 700; line-height: 1;\n color: var(--text);\n font-variant-numeric: tabular-nums;\n }\n\n .stat-card .stat-label {\n font-size: 0.6875rem;\n color: var(--text-muted);\n text-transform: uppercase;\n letter-spacing: 0.1em;\n }\n\n .stat-card .stat-change { font-size: 0.75rem; font-weight: 500; }\n .stat-card .stat-change.positive { color: var(--green); }\n .stat-card .stat-change.negative { color: var(--red); }\n\n /* ============ Bar chart ============ */\n .bar-chart { width: 100%; }\n\n .bar-chart .bar-item { margin-bottom: 0.75rem; }\n\n .bar-chart .bar-label {\n display: flex; justify-content: space-between; align-items: center;\n margin-bottom: 0.125rem; font-size: 0.75rem;\n }\n .bar-chart .bar-name { color: var(--text); }\n .bar-chart .bar-value {\n color: var(--text-muted);\n font-weight: 700; font-variant-numeric: tabular-nums;\n }\n\n .bar-chart .bar {\n height: 18px;\n background: var(--accent);\n border-radius: 0;\n transition: width 0.3s ease;\n position: relative; overflow: hidden;\n opacity: 0.8;\n }\n .bar-chart .bar:hover { opacity: 1; }\n\n .bar-chart .bar-fill { height: 100%; background: var(--accent); }\n\n /* ============ Form controls ============ */\n input, select, textarea, button { font-family: var(--font); font-size: 0.875rem; }\n\n input[type=\"text\"],\n input[type=\"email\"],\n input[type=\"password\"],\n input[type=\"number\"],\n select,\n textarea {\n width: 100%;\n padding: 0.625rem 0.75rem;\n background: var(--bg);\n border: 1px solid var(--border-bright);\n border-radius: 0;\n color: var(--text);\n }\n\n input:focus, select:focus, textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: 0 0 0 1px var(--accent);\n }\n\n input::placeholder, textarea::placeholder { color: var(--text-dim); }\n\n label {\n display: block;\n margin-bottom: 0.375rem;\n font-size: 0.6875rem;\n color: var(--text-muted);\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n }\n\n .form-group { margin-bottom: 1.25rem; }\n\n /* ============ Buttons ============ */\n button, .button {\n display: inline-block;\n padding: 0.625rem 1.25rem;\n background: var(--accent);\n color: #fff;\n border: 1px solid var(--accent);\n border-radius: 0;\n font-weight: 700;\n cursor: pointer;\n text-decoration: none;\n text-align: center;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-size: 0.75rem;\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.15),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 2px 2px 0 rgba(0,0,0,0.1);\n transition: box-shadow 0.1s, transform 0.1s;\n }\n button:hover, .button:hover {\n box-shadow:\n inset 1px 1px 0 rgba(255,255,255,0.1),\n inset -1px -1px 0 rgba(0,0,0,0.15),\n 3px 3px 0 rgba(0,0,0,0.12);\n opacity: 1;\n }\n button:active, .button:active {\n transform: translate(1px, 1px);\n box-shadow: inset 1px 1px 3px rgba(0,0,0,0.25);\n }\n button:disabled, .button:disabled { opacity: 0.25; cursor: not-allowed; }\n\n button.secondary, .button.secondary {\n background: transparent;\n color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n button.secondary:hover, .button.secondary:hover {\n border-color: var(--accent);\n color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n button.danger, .button.danger {\n background: var(--red);\n border-color: var(--red);\n color: #fff;\n box-shadow: 2px 2px 0 rgba(204,34,34,0.15);\n }\n button.danger:hover, .button.danger:hover {\n background: transparent;\n color: var(--red);\n }\n\n /* ============ Tables ============ */\n table { width: 100%; border-collapse: collapse; }\n\n thead { border-bottom: 1px solid var(--border-bright); }\n th {\n padding: 0.5rem 0.75rem; text-align: left;\n font-size: 0.6875rem; color: var(--text);\n font-weight: 700; text-transform: uppercase;\n letter-spacing: 0.1em;\n background: var(--bg-raised);\n }\n\n td {\n padding: 0.5rem 0.75rem;\n border-bottom: 1px solid var(--border);\n font-size: 0.75rem;\n font-variant-numeric: tabular-nums;\n }\n\n tbody tr:hover { background: var(--bg-raised); }\n\n /* ============ Code ============ */\n code, pre {\n font-family: var(--font);\n background: var(--bg-raised);\n padding: 0.125rem 0.375rem;\n border-radius: 0;\n font-size: 0.75rem;\n border: 1px solid var(--border);\n color: var(--text);\n }\n pre { padding: 1rem; overflow-x: auto; border: 1px solid var(--border-bright); }\n pre code { background: none; padding: 0; border: none; }\n\n /* ============ Login layout ============ */\n .login-container {\n min-height: 100vh;\n display: flex; align-items: center; justify-content: center;\n padding: 2rem;\n background: var(--bg);\n }\n .login-box { width: 100%; max-width: 420px; }\n\n .login-header { text-align: center; margin-bottom: 2rem; }\n .login-header h1 {\n font-size: 1.5rem; font-weight: 700; color: var(--text);\n letter-spacing: 0.15em; text-transform: uppercase;\n margin-bottom: 0.25rem;\n }\n .login-header p { color: var(--text-muted); font-size: 0.75rem; }\n\n /* ============ htmx loading ============ */\n .htmx-indicator { opacity: 0; transition: opacity 0.15s; }\n .htmx-request .htmx-indicator { opacity: 1; }\n .htmx-request.htmx-swapping { opacity: 0.3; pointer-events: none; }\n\n /* ============ Skeleton \u2014 blinking cursor ============ */\n .skeleton {\n background: var(--bg-raised);\n border: 1px solid var(--border);\n position: relative;\n overflow: hidden;\n }\n .skeleton::after {\n content: '';\n position: absolute; left: 0; top: 0;\n width: 2px; height: 100%;\n background: var(--text);\n animation: cursor-blink 0.8s step-end infinite;\n }\n @keyframes cursor-blink {\n 0%, 100% { opacity: 1; }\n 50% { opacity: 0; }\n }\n\n .skeleton-text { height: 1rem; margin-bottom: 0.5rem; }\n .skeleton-heading { height: 1.5rem; width: 60%; margin-bottom: 1rem; }\n\n /* ============ Utilities ============ */\n .text-center { text-align: center; }\n .text-right { text-align: right; }\n .text-muted { color: var(--text-muted); }\n .text-dim { color: var(--text-dim); }\n .text-success { color: var(--green); }\n .text-danger { color: var(--red); }\n .text-warning { color: var(--amber); }\n\n .mb-0 { margin-bottom: 0; }\n .mb-1 { margin-bottom: 0.5rem; }\n .mb-2 { margin-bottom: 1rem; }\n .mb-3 { margin-bottom: 1.5rem; }\n .mb-4 { margin-bottom: 2rem; }\n .mt-0 { margin-top: 0; }\n .mt-1 { margin-top: 0.5rem; }\n .mt-2 { margin-top: 1rem; }\n .mt-3 { margin-top: 1.5rem; }\n .mt-4 { margin-top: 2rem; }\n\n /* ============ Period selector ============ */\n .period-selector button {\n font-size: 0.625rem;\n padding: 0.2rem 0.5rem;\n }\n\n /* ============ Responsive ============ */\n @media (max-width: 768px) {\n html, body { font-size: 14px; }\n .dashboard-main { padding: 1rem; }\n .nav-container { padding: 0.5rem 1rem; }\n .dashboard-grid { grid-template-columns: 1fr; gap: 1rem; }\n .card-inner { padding: 1rem; }\n .card { margin-bottom: 1rem; }\n fieldset { padding: 1rem; margin-bottom: 1rem; }\n .stat-card .stat-value { font-size: 1.75rem; }\n table { font-size: 0.625rem; }\n th, td { padding: 0.375rem 0.5rem; }\n }\n\n @media (max-width: 480px) {\n .nav-container { flex-wrap: wrap; }\n .nav-app-id { margin-left: 0; width: 100%; order: 3; }\n }\n\n /* ============ Alerts ============ */\n .alert {\n padding: 0.75rem 1rem;\n border-radius: 0;\n margin-bottom: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.75rem;\n background: var(--bg-card);\n }\n .alert::before { font-weight: 700; margin-right: 0.5rem; }\n\n .alert-info { border-color: var(--border-bright); color: var(--text); }\n .alert-info::before { content: '>'; color: var(--text); }\n\n .alert-success { border-color: var(--green); color: var(--green); }\n .alert-success::before { content: '[ok]'; }\n\n .alert-warning { border-color: var(--amber); color: var(--amber); }\n .alert-warning::before { content: '[!!]'; }\n\n .alert-danger { border-color: var(--red); color: var(--red); }\n .alert-danger::before { content: '[ERR]'; }\n\n /* ============ Badges ============ */\n .badge {\n display: inline-block;\n padding: 0.125rem 0.375rem;\n font-size: 0.625rem; font-weight: 700;\n border-radius: 0;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n border: 1px solid;\n }\n .badge-success { color: var(--green); border-color: var(--green); background: transparent; }\n .badge-danger { color: var(--red); border-color: var(--red); background: transparent; }\n .badge-warning { color: var(--amber); border-color: var(--amber); background: transparent; }\n .badge-info { color: #fff; border-color: var(--accent); background: var(--accent); }\n\n /* ============ Sample data indicator ============ */\n .sample-banner {\n background: #fffbe6;\n border: 1px solid var(--amber);\n color: var(--amber);\n padding: 0.5rem 0.75rem;\n font-size: 0.6875rem;\n font-weight: 700;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n margin-bottom: 1rem;\n }\n .sample-banner::before { content: '[demo]'; margin-right: 0.5rem; }\n .sample-tag {\n color: var(--amber);\n font-size: 0.625rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n border: 1px solid var(--amber);\n padding: 0.0625rem 0.3rem;\n margin-left: 0.375rem;\n vertical-align: middle;\n background: #fffbe6;\n }\n\n /* ============ Empty state ============ */\n .empty-state { text-align: center; padding: 3rem 1rem; color: var(--text-muted); }\n .empty-state-icon { font-size: 1.5rem; margin-bottom: 0.75rem; color: var(--text); font-weight: 700; }\n .empty-state-text { font-size: 0.8125rem; margin-bottom: 0.25rem; }\n .empty-state-subtext { font-size: 0.6875rem; color: var(--text-dim); }\n\n /* ============ Auth pages ============ */\n .ascii-logo {\n display: block; text-align: center; margin-bottom: 2rem;\n color: var(--text); font-size: 0.55rem; line-height: 1.2;\n white-space: pre; font-weight: 400;\n text-decoration: none;\n }\n\n .badge-inline {\n display: inline-block; font-size: 0.5625rem; font-weight: 700;\n color: var(--text-muted); border: 1px solid var(--border-bright);\n border-radius: 0; padding: 0.1rem 0.4rem;\n margin-left: 0.5rem; vertical-align: middle;\n text-transform: uppercase; letter-spacing: 0.05em;\n }\n\n .divider {\n display: flex; align-items: center; gap: 0.75rem;\n color: var(--text-dim); font-size: 0.6875rem;\n margin: 1.5rem 0;\n text-transform: uppercase; letter-spacing: 0.1em;\n white-space: nowrap;\n }\n .divider::before, .divider::after {\n content: ''; flex: 1;\n height: 1px; background: var(--border-bright);\n }\n\n .credentials-box {\n background: var(--bg); border: 1px solid var(--accent-dim);\n padding: 1rem; margin-bottom: 1rem;\n font-size: 0.75rem; line-height: 1.8; word-break: break-all;\n }\n .credentials-box .label { color: var(--text-muted); }\n .credentials-box .value { color: var(--text); font-weight: 700; }\n\n .warning {\n background: rgba(184,122,0,0.06); border: 1px solid var(--amber);\n padding: 0.75rem; margin-bottom: 1rem;\n font-size: 0.7rem; color: var(--amber);\n }\n .warning::before { content: '[!!] '; font-weight: 700; }\n\n .error-message {\n color: var(--red); margin: 0 0 1rem 0; font-size: 0.75rem;\n padding: 0.5rem 0.75rem;\n border: 1px solid rgba(204,34,34,0.3);\n background: var(--bg);\n }\n .error-message::before { content: '[ERR] '; font-weight: 700; }\n\n .hint {\n font-size: 0.6875rem; color: var(--text-muted); line-height: 1.6;\n margin-top: 0.75rem;\n }\n .hint code {\n color: var(--text); background: var(--bg-raised);\n padding: 0.125rem 0.375rem; border: 1px solid var(--border);\n }\n\n .btn {\n display: block; width: 100%; text-align: center; text-decoration: none;\n }\n .btn-secondary {\n background: transparent; color: var(--text);\n border-color: var(--border-bright);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.05);\n }\n .btn-secondary:hover {\n border-color: var(--accent); color: var(--accent);\n box-shadow: 2px 2px 0 rgba(0,0,0,0.1);\n }\n\n #create-result { display: none; }\n #create-result.show { display: block; }\n #create-btn.loading { opacity: 0.25; pointer-events: none; }\n\n /* ============ Landing page ============ */\n .landing-box { width: 100%; max-width: 580px; }\n .landing-box .ascii-logo { font-size: 0.75rem; margin-bottom: 1rem; }\n\n /* Landing page flows from top, not vertically centered like login */\n .login-container:has(.landing-box) {\n align-items: flex-start; padding-top: 4rem;\n }\n\n .landing-tagline {\n text-align: center; font-size: 0.8125rem;\n color: var(--text-muted); margin-bottom: 1.5rem;\n }\n\n .landing-links {\n display: flex; flex-wrap: wrap; justify-content: center;\n gap: 0.5rem; margin-bottom: 2rem;\n }\n .landing-link {\n font-size: 0.6875rem; color: var(--text);\n text-decoration: none; padding: 0.25rem 0.625rem;\n border: 1px solid var(--border-bright);\n transition: border-color 0.15s, background 0.15s;\n }\n .landing-link:hover {\n border-color: var(--accent); background: var(--bg-raised);\n }\n\n .landing-features {\n display: flex; flex-direction: column; gap: 0.75rem;\n margin-top: 1rem;\n }\n .landing-feature {\n display: flex; gap: 0.75rem; align-items: baseline;\n font-size: 0.75rem;\n }\n .landing-feature-label {\n font-weight: 700; color: var(--text);\n white-space: nowrap; min-width: 10rem;\n }\n .landing-feature-desc { color: var(--text-muted); }\n\n .landing-steps { display: flex; flex-direction: column; gap: 0.75rem; }\n .landing-step {\n display: flex; gap: 0.75rem; align-items: flex-start;\n font-size: 0.75rem; line-height: 1.6;\n }\n .landing-step-num {\n display: inline-flex; align-items: center; justify-content: center;\n min-width: 1.5rem; height: 1.5rem;\n border: 1px solid var(--border-bright);\n font-size: 0.6875rem; font-weight: 700; color: var(--text);\n flex-shrink: 0;\n }\n .landing-step-hint {\n display: block; font-size: 0.6875rem; color: var(--text-dim);\n margin-top: 0.125rem;\n }\n\n .landing-footer {\n text-align: center; padding: 2rem 0 0;\n font-size: 0.6875rem; color: var(--text-dim);\n }\n .landing-footer a {\n color: var(--text-muted); text-decoration: none;\n }\n .landing-footer a:hover { color: var(--text); }\n .landing-footer-sep { margin: 0 0.375rem; }\n\n /* ============ Scrollbar ============ */\n ::-webkit-scrollbar { width: 6px; height: 6px; }\n ::-webkit-scrollbar-track { background: var(--bg-raised); }\n ::-webkit-scrollbar-thumb { background: var(--border-bright); }\n ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }\n\n /* ============ Responsive (small screens) ============ */\n @media (max-width: 480px) {\n .ascii-logo { font-size: 0.4rem; }\n .landing-box .ascii-logo { font-size: 0.5rem; }\n .login-container { padding: 1rem; }\n .card-inner { padding: 1rem; }\n .landing-feature { flex-direction: column; gap: 0.125rem; }\n .landing-feature-label { min-width: auto; }\n }\n";
11
11
  //# sourceMappingURL=styles.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,4skBA+mBzB,CAAC"}
1
+ {"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../../src/dashboard/styles.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,aAAa,yvqBAitBzB,CAAC"}