clearhand 0.1.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/LICENSE +202 -0
- package/README.md +146 -0
- package/SECURITY.md +77 -0
- package/bin/clearhand.js +230 -0
- package/dist/mcp/index.js +159 -0
- package/dist/server/connectors/crawl.js +52 -0
- package/dist/server/connectors/github.js +45 -0
- package/dist/server/connectors/mailbox.js +257 -0
- package/dist/server/connectors/stripe.js +252 -0
- package/dist/server/context.js +83 -0
- package/dist/server/db/index.js +324 -0
- package/dist/server/demo.js +194 -0
- package/dist/server/index.js +51 -0
- package/dist/server/routes/api.js +704 -0
- package/dist/server/secrets/redaction.js +62 -0
- package/dist/server/secrets/store.js +238 -0
- package/dist/server/services/approval.js +206 -0
- package/dist/server/services/connections.js +99 -0
- package/dist/server/services/executor.js +417 -0
- package/dist/server/services/lint.js +129 -0
- package/dist/server/services/onboarding.js +168 -0
- package/dist/server/services/overlay.js +65 -0
- package/dist/server/trust/egress.js +12 -0
- package/dist/shared/actions.js +136 -0
- package/dist/shared/config.js +33 -0
- package/dist/shared/paths.js +34 -0
- package/package.json +73 -0
- package/public/assets/index-BicJ8AKo.css +1 -0
- package/public/assets/index-DZSNZW8-.js +43 -0
- package/public/index.html +17 -0
- package/templates/CLEARHAND.md +79 -0
- package/templates/overlay/escalation.md +21 -0
- package/templates/overlay/policies.md +30 -0
- package/templates/overlay/products.md +25 -0
- package/templates/overlay/tone-lint.json +14 -0
- package/templates/overlay/voice.md +24 -0
- package/templates/skills/clearhand-cs/SKILL.md +75 -0
- package/templates/skills/clearhand-onboard/SKILL.md +126 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { now, audit } from '../db/index.js';
|
|
4
|
+
import { overlayDir, ensureDir } from '../../shared/paths.js';
|
|
5
|
+
/**
|
|
6
|
+
* The onboarding state machine — the single source of truth both drivers
|
|
7
|
+
* subscribe to. The agent advances stages and streams progress events via
|
|
8
|
+
* MCP; the dashboard renders them and collects the user's confirmations.
|
|
9
|
+
* Neither side ever asks for what the other is collecting.
|
|
10
|
+
*
|
|
11
|
+
* Stages: arrival → mailbox → stripe → reading{products,voice,policies}
|
|
12
|
+
* → style → calibration → first_batch → live
|
|
13
|
+
*/
|
|
14
|
+
export const ONBOARDING_STAGES = [
|
|
15
|
+
'arrival',
|
|
16
|
+
'mailbox',
|
|
17
|
+
'stripe',
|
|
18
|
+
'reading_products',
|
|
19
|
+
'reading_voice',
|
|
20
|
+
'reading_policies',
|
|
21
|
+
'style',
|
|
22
|
+
'calibration',
|
|
23
|
+
'first_batch',
|
|
24
|
+
'live',
|
|
25
|
+
];
|
|
26
|
+
function freshState() {
|
|
27
|
+
return {
|
|
28
|
+
stages: Object.fromEntries(ONBOARDING_STAGES.map((s) => [s, 'pending'])),
|
|
29
|
+
activity: {},
|
|
30
|
+
artifacts: {},
|
|
31
|
+
confirmations: {},
|
|
32
|
+
timestamps: {},
|
|
33
|
+
startedAt: null,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function getOnboardingState(db) {
|
|
37
|
+
const row = db.prepare(`SELECT state_json FROM onboarding WHERE id = 1`).get();
|
|
38
|
+
if (!row)
|
|
39
|
+
return freshState();
|
|
40
|
+
try {
|
|
41
|
+
return { ...freshState(), ...JSON.parse(row.state_json) };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return freshState();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function saveState(db, state) {
|
|
48
|
+
db.prepare(`INSERT INTO onboarding (id, state_json, updated_at) VALUES (1, ?, ?)
|
|
49
|
+
ON CONFLICT(id) DO UPDATE SET state_json = excluded.state_json, updated_at = excluded.updated_at`).run(JSON.stringify(state), now());
|
|
50
|
+
}
|
|
51
|
+
export function setStage(db, stage, status) {
|
|
52
|
+
const state = getOnboardingState(db);
|
|
53
|
+
if (!state.startedAt)
|
|
54
|
+
state.startedAt = now();
|
|
55
|
+
state.stages[stage] = status;
|
|
56
|
+
// The hand never lies: timestamps record real transitions only.
|
|
57
|
+
state.timestamps[`${stage}_${status}`] = now();
|
|
58
|
+
saveState(db, state);
|
|
59
|
+
audit(db, 'system', 'onboarding_stage', { detail: { stage, status } });
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
export function setActivity(db, stage, line) {
|
|
63
|
+
const state = getOnboardingState(db);
|
|
64
|
+
state.activity[stage] = line;
|
|
65
|
+
saveState(db, state);
|
|
66
|
+
}
|
|
67
|
+
export function setArtifact(db, stage, name, bytes) {
|
|
68
|
+
const state = getOnboardingState(db);
|
|
69
|
+
state.artifacts[stage] = { name, bytes };
|
|
70
|
+
saveState(db, state);
|
|
71
|
+
}
|
|
72
|
+
export function saveConfirmation(db, key, value) {
|
|
73
|
+
const state = getOnboardingState(db);
|
|
74
|
+
state.confirmations[key] = value;
|
|
75
|
+
saveState(db, state);
|
|
76
|
+
audit(db, 'human', 'onboarding_confirmation', { detail: { key } });
|
|
77
|
+
return state;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The progress event stream behind the Reading workbench and the first-batch
|
|
81
|
+
* view. kinds: activity | finding | artifact | conflict | batch_case |
|
|
82
|
+
* batch_done | note. Findings carry evidence — receipts, not theater.
|
|
83
|
+
*/
|
|
84
|
+
export function appendEvent(db, stage, kind, payload) {
|
|
85
|
+
const res = db
|
|
86
|
+
.prepare(`INSERT INTO onboarding_events (at, stage, kind, payload) VALUES (?, ?, ?, ?)`)
|
|
87
|
+
.run(now(), stage, kind, payload !== undefined ? JSON.stringify(payload) : null);
|
|
88
|
+
// Mirror one-line activities into state so a page refresh shows the latest.
|
|
89
|
+
if (kind === 'activity' && typeof payload?.line === 'string') {
|
|
90
|
+
setActivity(db, stage, payload.line);
|
|
91
|
+
}
|
|
92
|
+
if (kind === 'artifact') {
|
|
93
|
+
const a = payload;
|
|
94
|
+
if (a?.name)
|
|
95
|
+
setArtifact(db, stage, a.name, a.bytes ?? 0);
|
|
96
|
+
}
|
|
97
|
+
return Number(res.lastInsertRowid);
|
|
98
|
+
}
|
|
99
|
+
export function eventsAfter(db, afterId, limit = 500) {
|
|
100
|
+
const rows = db
|
|
101
|
+
.prepare(`SELECT * FROM onboarding_events WHERE id > ? ORDER BY id LIMIT ?`)
|
|
102
|
+
.all(afterId, limit);
|
|
103
|
+
return rows.map((r) => ({
|
|
104
|
+
id: r.id,
|
|
105
|
+
at: r.at,
|
|
106
|
+
stage: r.stage,
|
|
107
|
+
kind: r.kind,
|
|
108
|
+
payload: r.payload ? JSON.parse(r.payload) : null,
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
const STYLE_SECTION_START = '<!-- clearhand:handling-style:start -->';
|
|
112
|
+
const STYLE_SECTION_END = '<!-- clearhand:handling-style:end -->';
|
|
113
|
+
/**
|
|
114
|
+
* Writes the chosen handling style into overlay/policies.md as a marked
|
|
115
|
+
* section (idempotent replace on re-run). The contract paragraph goes in
|
|
116
|
+
* verbatim — the user's words are the policy.
|
|
117
|
+
*/
|
|
118
|
+
export function applyStyleSelection(db, sel) {
|
|
119
|
+
const dir = ensureDir(overlayDir());
|
|
120
|
+
const file = path.join(dir, 'policies.md');
|
|
121
|
+
let existing = '';
|
|
122
|
+
try {
|
|
123
|
+
existing = fs.readFileSync(file, 'utf8');
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
existing = '# Policies\n';
|
|
127
|
+
}
|
|
128
|
+
const styleNames = {
|
|
129
|
+
by_the_book: 'By the book',
|
|
130
|
+
track_record: 'Match my track record',
|
|
131
|
+
generous: 'Generous & fast',
|
|
132
|
+
};
|
|
133
|
+
const lines = [
|
|
134
|
+
STYLE_SECTION_START,
|
|
135
|
+
'',
|
|
136
|
+
'## Handling style (chosen during onboarding)',
|
|
137
|
+
'',
|
|
138
|
+
`Style: **${styleNames[sel.choice]}**`,
|
|
139
|
+
'',
|
|
140
|
+
sel.contractText.trim(),
|
|
141
|
+
'',
|
|
142
|
+
'### Escalation triggers',
|
|
143
|
+
'',
|
|
144
|
+
'- Legal or chargeback threats: always escalate (locked)',
|
|
145
|
+
`- Repeat refunders (2+ prior refunds): ${sel.escalation.repeatRefunders ? 'escalate' : 'handle per style'}`,
|
|
146
|
+
sel.escalation.amountOverCents !== null
|
|
147
|
+
? `- Amounts over $${(sel.escalation.amountOverCents / 100).toFixed(2)}: escalate`
|
|
148
|
+
: '- No amount ceiling set',
|
|
149
|
+
sel.escalation.vipList.length > 0
|
|
150
|
+
? `- VIP list (always loop me in): ${sel.escalation.vipList.join(', ')}`
|
|
151
|
+
: '- VIP list: none yet',
|
|
152
|
+
'',
|
|
153
|
+
`### Cancellation default`,
|
|
154
|
+
'',
|
|
155
|
+
`- ${sel.cancelDefault === 'end_of_period' ? 'End of period (customer keeps what they paid for)' : 'Immediate when requested'}`,
|
|
156
|
+
'',
|
|
157
|
+
STYLE_SECTION_END,
|
|
158
|
+
].join('\n');
|
|
159
|
+
const startIdx = existing.indexOf(STYLE_SECTION_START);
|
|
160
|
+
const endIdx = existing.indexOf(STYLE_SECTION_END);
|
|
161
|
+
const next = startIdx !== -1 && endIdx !== -1
|
|
162
|
+
? existing.slice(0, startIdx) + lines + existing.slice(endIdx + STYLE_SECTION_END.length)
|
|
163
|
+
: `${existing.trimEnd()}\n\n${lines}\n`;
|
|
164
|
+
fs.writeFileSync(file, next);
|
|
165
|
+
saveConfirmation(db, 'handling_style', sel);
|
|
166
|
+
setStage(db, 'style', 'done');
|
|
167
|
+
audit(db, 'human', 'handling_style_applied', { detail: { choice: sel.choice } });
|
|
168
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { overlayDir } from '../../shared/paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* Reference defaults, distilled in the ancestor system from longitudinal
|
|
6
|
+
* analysis of operator edit-diffs: forms operators consistently rewrote
|
|
7
|
+
* became lint rules. Tenants override via overlay/tone-lint.json.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_TONE_LINT = {
|
|
10
|
+
bannedGreetings: ['\\bDear\\b', '^\\s*Hello\\s+[A-Z]'],
|
|
11
|
+
bannedOpeners: [
|
|
12
|
+
'Thank you for reaching out to us',
|
|
13
|
+
'Thank you for contacting us',
|
|
14
|
+
'Thank you for your inquiry',
|
|
15
|
+
],
|
|
16
|
+
bannedSignoffs: ['\\.ai\\s+Support', 'Support\\s+Team', '客服团队'],
|
|
17
|
+
suggestions: {
|
|
18
|
+
greeting: 'Hi <FirstName>,',
|
|
19
|
+
opener: 'Thanks for reaching out.',
|
|
20
|
+
signoff: '<Product> Support',
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
export function loadToneLintConfig() {
|
|
24
|
+
try {
|
|
25
|
+
const raw = JSON.parse(fs.readFileSync(path.join(overlayDir(), 'tone-lint.json'), 'utf8'));
|
|
26
|
+
return {
|
|
27
|
+
bannedGreetings: raw.bannedGreetings ?? DEFAULT_TONE_LINT.bannedGreetings,
|
|
28
|
+
bannedOpeners: raw.bannedOpeners ?? DEFAULT_TONE_LINT.bannedOpeners,
|
|
29
|
+
bannedSignoffs: raw.bannedSignoffs ?? DEFAULT_TONE_LINT.bannedSignoffs,
|
|
30
|
+
suggestions: { ...DEFAULT_TONE_LINT.suggestions, ...raw.suggestions },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return DEFAULT_TONE_LINT;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export const OVERLAY_FILES = [
|
|
38
|
+
'voice.md',
|
|
39
|
+
'policies.md',
|
|
40
|
+
'products.md',
|
|
41
|
+
'escalation.md',
|
|
42
|
+
'tone-lint.json',
|
|
43
|
+
];
|
|
44
|
+
export function overlayStatus() {
|
|
45
|
+
return OVERLAY_FILES.map((name) => {
|
|
46
|
+
const p = path.join(overlayDir(), name);
|
|
47
|
+
try {
|
|
48
|
+
const st = fs.statSync(p);
|
|
49
|
+
return { name, path: p, exists: true, bytes: st.size, modifiedAt: st.mtime.toISOString() };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return { name, path: p, exists: false, bytes: 0, modifiedAt: null };
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
export function readOverlayFile(name) {
|
|
57
|
+
if (!OVERLAY_FILES.includes(name))
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
return fs.readFileSync(path.join(overlayDir(), name), 'utf8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function recordEgress(db, host, purpose) {
|
|
2
|
+
const now = new Date().toISOString();
|
|
3
|
+
db.prepare(`INSERT INTO egress_log (host, purpose, first_at, last_at, count)
|
|
4
|
+
VALUES (?, ?, ?, ?, 1)
|
|
5
|
+
ON CONFLICT(host, purpose)
|
|
6
|
+
DO UPDATE SET last_at = excluded.last_at, count = count + 1`).run(host, purpose, now, now);
|
|
7
|
+
}
|
|
8
|
+
export function listEgress(db) {
|
|
9
|
+
return db
|
|
10
|
+
.prepare(`SELECT host, purpose, first_at, last_at, count FROM egress_log ORDER BY last_at DESC`)
|
|
11
|
+
.all();
|
|
12
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Action Proposal v1 — the contract between brain and hands.
|
|
4
|
+
*
|
|
5
|
+
* The brain (any LLM/harness) may only emit these typed proposals. The hands
|
|
6
|
+
* (lint → approval → executor) validate, normalize, and execute them. Every
|
|
7
|
+
* historical money incident in the ancestor system was the brain reaching
|
|
8
|
+
* past this line, so:
|
|
9
|
+
*
|
|
10
|
+
* RULE, ENFORCED FOREVER: every new action_type lands a lint rule AND an
|
|
11
|
+
* executor guard in the same change, or the LLM regains a money-unsafe path.
|
|
12
|
+
*/
|
|
13
|
+
export const ACTION_TYPES = [
|
|
14
|
+
'send_reply',
|
|
15
|
+
'refund',
|
|
16
|
+
'cancel_subscription',
|
|
17
|
+
'escalate',
|
|
18
|
+
'set_case_status',
|
|
19
|
+
'create_github_issue',
|
|
20
|
+
];
|
|
21
|
+
export const FINANCIAL_ACTION_TYPES = new Set([
|
|
22
|
+
'refund',
|
|
23
|
+
'cancel_subscription',
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Execution order convention (lower first): money moves before words.
|
|
27
|
+
* 0 refund · 5 cancel · 10 send_reply · 50 create_github_issue · 99 escalate/status
|
|
28
|
+
*/
|
|
29
|
+
export const DEFAULT_EXECUTION_ORDER = {
|
|
30
|
+
refund: 0,
|
|
31
|
+
cancel_subscription: 5,
|
|
32
|
+
send_reply: 10,
|
|
33
|
+
create_github_issue: 50,
|
|
34
|
+
escalate: 99,
|
|
35
|
+
set_case_status: 99,
|
|
36
|
+
};
|
|
37
|
+
const stripeId = (prefix) => z
|
|
38
|
+
.string()
|
|
39
|
+
.refine((v) => isValidStripeId(v, prefix), { message: `must be a real ${prefix}… id` });
|
|
40
|
+
/** Anti-hallucination format check for externally supplied Stripe ids. */
|
|
41
|
+
export function isValidStripeId(id, prefix) {
|
|
42
|
+
return (typeof id === 'string' &&
|
|
43
|
+
id.startsWith(prefix) &&
|
|
44
|
+
id.length >= 15 &&
|
|
45
|
+
/^[A-Za-z0-9]+$/.test(id.slice(prefix.length)));
|
|
46
|
+
}
|
|
47
|
+
export const RefundProposalSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
charge_id: z.string().optional(),
|
|
50
|
+
subscription_id: z.string().optional(),
|
|
51
|
+
/**
|
|
52
|
+
* CENTS, always. The ancestor system accepted an ambiguous `amount` and
|
|
53
|
+
* carried a dollars-vs-cents heuristic that could 100x a small partial
|
|
54
|
+
* refund; ClearHand accepts only an explicit `amount_cents`. Absent =
|
|
55
|
+
* full refund of the charge.
|
|
56
|
+
*/
|
|
57
|
+
amount_cents: z.number().int().positive().optional(),
|
|
58
|
+
currency: z.string().optional(),
|
|
59
|
+
reason: z.string().optional(),
|
|
60
|
+
// display/audit fields, filled by server-side enrichment
|
|
61
|
+
charge_date: z.string().optional(),
|
|
62
|
+
charge_description: z.string().optional(),
|
|
63
|
+
product_name: z.string().optional(),
|
|
64
|
+
})
|
|
65
|
+
.passthrough();
|
|
66
|
+
export const CancelProposalSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
subscription_id: z.string().optional(),
|
|
69
|
+
cancel_mode: z.enum(['immediate', 'end_of_period', 'scheduled']).optional(),
|
|
70
|
+
/** YYYY-MM-DD; required for `scheduled` */
|
|
71
|
+
cancel_date: z.string().optional(),
|
|
72
|
+
customer_id: z.string().optional(),
|
|
73
|
+
reason: z.string().optional(),
|
|
74
|
+
// display/audit fields (server-filled)
|
|
75
|
+
product_name: z.string().optional(),
|
|
76
|
+
plan_amount: z.string().optional(),
|
|
77
|
+
current_period_start: z.string().optional(),
|
|
78
|
+
current_period_end: z.string().optional(),
|
|
79
|
+
interval: z.string().optional(),
|
|
80
|
+
amount_cents: z.number().optional(),
|
|
81
|
+
currency: z.string().optional(),
|
|
82
|
+
})
|
|
83
|
+
.passthrough();
|
|
84
|
+
export const SendReplyProposalSchema = z
|
|
85
|
+
.object({
|
|
86
|
+
/** The real body. Required at creation time — no "see draft" placeholders. */
|
|
87
|
+
body_text: z.string().optional(),
|
|
88
|
+
body_english: z.string().optional(),
|
|
89
|
+
body_translated: z.string().optional(),
|
|
90
|
+
target_language: z.string().optional(),
|
|
91
|
+
/** Summary for the approval card. NEVER sent to the customer. */
|
|
92
|
+
message: z.string().optional(),
|
|
93
|
+
})
|
|
94
|
+
.passthrough();
|
|
95
|
+
export const EscalateProposalSchema = z.object({ reason: z.string() }).passthrough();
|
|
96
|
+
export const SetCaseStatusProposalSchema = z
|
|
97
|
+
.object({
|
|
98
|
+
target_status: z.enum(['resolved', 'pending', 'ignored']),
|
|
99
|
+
reason: z.string().optional(),
|
|
100
|
+
})
|
|
101
|
+
.passthrough();
|
|
102
|
+
export const CreateGithubIssueProposalSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
title: z.string().optional(),
|
|
105
|
+
body: z.string().optional(),
|
|
106
|
+
labels: z.array(z.string()).optional(),
|
|
107
|
+
})
|
|
108
|
+
.passthrough();
|
|
109
|
+
export const PROPOSAL_SCHEMAS = {
|
|
110
|
+
refund: RefundProposalSchema,
|
|
111
|
+
cancel_subscription: CancelProposalSchema,
|
|
112
|
+
send_reply: SendReplyProposalSchema,
|
|
113
|
+
escalate: EscalateProposalSchema,
|
|
114
|
+
set_case_status: SetCaseStatusProposalSchema,
|
|
115
|
+
create_github_issue: CreateGithubIssueProposalSchema,
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* The effective proposal everywhere is modified_json ?? proposal_json, parsed
|
|
119
|
+
* defensively: double-stringified payloads are unwrapped once, and non-object
|
|
120
|
+
* results collapse to {}.
|
|
121
|
+
*/
|
|
122
|
+
export function effectiveProposal(action) {
|
|
123
|
+
const raw = action.modified_json ?? action.proposal_json;
|
|
124
|
+
let parsed = raw;
|
|
125
|
+
try {
|
|
126
|
+
parsed = JSON.parse(raw);
|
|
127
|
+
if (typeof parsed === 'string')
|
|
128
|
+
parsed = JSON.parse(parsed);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// fall through — treat unparseable as empty
|
|
132
|
+
}
|
|
133
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
134
|
+
? parsed
|
|
135
|
+
: {};
|
|
136
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { configPath } from './paths.js';
|
|
4
|
+
export const ClearHandConfigSchema = z.object({
|
|
5
|
+
port: z.number().int().min(1).max(65535).default(3117),
|
|
6
|
+
/** Storage backend for secrets. 'auto' = keychain if available, else refuse. */
|
|
7
|
+
secretStorage: z.enum(['auto', 'keychain', 'encrypted-file', 'plain']).default('auto'),
|
|
8
|
+
demoMode: z.boolean().default(false),
|
|
9
|
+
});
|
|
10
|
+
let cached = null;
|
|
11
|
+
export function loadConfig() {
|
|
12
|
+
if (cached)
|
|
13
|
+
return cached;
|
|
14
|
+
let raw = {};
|
|
15
|
+
try {
|
|
16
|
+
raw = JSON.parse(fs.readFileSync(configPath(), 'utf8'));
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
// missing or unreadable config falls back to defaults
|
|
20
|
+
}
|
|
21
|
+
cached = ClearHandConfigSchema.parse(raw);
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
24
|
+
export function saveConfig(update) {
|
|
25
|
+
const merged = { ...loadConfig(), ...update };
|
|
26
|
+
fs.writeFileSync(configPath(), JSON.stringify(merged, null, 2) + '\n');
|
|
27
|
+
cached = merged;
|
|
28
|
+
return merged;
|
|
29
|
+
}
|
|
30
|
+
/** Test hook / hot-reload: drop the config cache. */
|
|
31
|
+
export function resetConfigCache() {
|
|
32
|
+
cached = null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
/**
|
|
5
|
+
* ClearHand separates two storage scopes on purpose:
|
|
6
|
+
*
|
|
7
|
+
* - USER scope (~/.clearhand): credentials and secret metadata. Lives outside
|
|
8
|
+
* any repository tree so a `git add .` can never commit a key.
|
|
9
|
+
* - PROJECT scope (<cwd>/.clearhand): the case database, overlay policy book,
|
|
10
|
+
* and local config. Overlay files are designed to be committed; the
|
|
11
|
+
* database is gitignored by `clearhand init`.
|
|
12
|
+
*/
|
|
13
|
+
export function userHome() {
|
|
14
|
+
return process.env.CLEARHAND_USER_DIR ?? path.join(os.homedir(), '.clearhand');
|
|
15
|
+
}
|
|
16
|
+
export function projectRoot() {
|
|
17
|
+
return process.env.CLEARHAND_PROJECT_DIR ?? process.cwd();
|
|
18
|
+
}
|
|
19
|
+
export function projectDir() {
|
|
20
|
+
return path.join(projectRoot(), '.clearhand');
|
|
21
|
+
}
|
|
22
|
+
export function dbPath() {
|
|
23
|
+
return process.env.CLEARHAND_DB_PATH ?? path.join(projectDir(), 'clearhand.db');
|
|
24
|
+
}
|
|
25
|
+
export function overlayDir() {
|
|
26
|
+
return path.join(projectDir(), 'overlay');
|
|
27
|
+
}
|
|
28
|
+
export function configPath() {
|
|
29
|
+
return path.join(projectDir(), 'config.json');
|
|
30
|
+
}
|
|
31
|
+
export function ensureDir(dir, mode) {
|
|
32
|
+
fs.mkdirSync(dir, { recursive: true, ...(mode !== undefined ? { mode } : {}) });
|
|
33
|
+
return dir;
|
|
34
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clearhand",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Safe hands for your agent. ClearHand lets your existing coding agent (Claude Code, Codex) run your customer support — drafting in your voice, proposing refunds and subscription changes it can never execute without your click.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"customer-support",
|
|
7
|
+
"agent",
|
|
8
|
+
"claude-code",
|
|
9
|
+
"mcp",
|
|
10
|
+
"stripe",
|
|
11
|
+
"email",
|
|
12
|
+
"human-in-the-loop"
|
|
13
|
+
],
|
|
14
|
+
"license": "Apache-2.0",
|
|
15
|
+
"author": "honganai",
|
|
16
|
+
"homepage": "https://clearhand.help",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/honganai/clearhand-help.git"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/honganai/clearhand-help/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"clearhand": "bin/clearhand.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "dist/server/index.js",
|
|
32
|
+
"files": [
|
|
33
|
+
"bin",
|
|
34
|
+
"dist",
|
|
35
|
+
"public",
|
|
36
|
+
"templates",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"SECURITY.md"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -p tsconfig.json && npm run build:dashboard",
|
|
43
|
+
"build:server": "tsc -p tsconfig.json",
|
|
44
|
+
"build:dashboard": "cd dashboard && npm run build",
|
|
45
|
+
"dev": "tsx src/server/index.ts",
|
|
46
|
+
"dev:dashboard": "cd dashboard && npm run dev",
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
49
|
+
"prepublishOnly": "npm run build && npm test"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
53
|
+
"@napi-rs/keyring": "^1.1.6",
|
|
54
|
+
"better-sqlite3": "^11.10.0",
|
|
55
|
+
"commander": "^13.1.0",
|
|
56
|
+
"express": "^4.21.2",
|
|
57
|
+
"imapflow": "^1.0.187",
|
|
58
|
+
"mailparser": "^3.7.2",
|
|
59
|
+
"nodemailer": "^9.0.3",
|
|
60
|
+
"stripe": "^18.2.1",
|
|
61
|
+
"zod": "^3.25.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
65
|
+
"@types/express": "^4.17.22",
|
|
66
|
+
"@types/mailparser": "^3.4.6",
|
|
67
|
+
"@types/node": "^22.15.0",
|
|
68
|
+
"@types/nodemailer": "^6.4.17",
|
|
69
|
+
"tsx": "^4.19.4",
|
|
70
|
+
"typescript": "^5.8.3",
|
|
71
|
+
"vitest": "^3.1.4"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--paper: #faf7f2;--card: #ffffff;--ink: #1a1d1a;--ink-soft: #565d56;--ink-faint: #8b918a;--pine: #1e5c48;--pine-dark: #17483a;--pine-tint: rgba(30, 92, 72, .08);--sage: #7ba58f;--hairline: #e4ded4;--hairline-soft: #efeae0;--danger: #b3402e;--warn: #a8721c;--gold: #c9a24b;--radius: 10px;--shadow: 0 1px 2px rgba(26, 29, 26, .04), 0 6px 20px rgba(26, 29, 26, .05);--shadow-lift: 0 2px 6px rgba(26, 29, 26, .07), 0 12px 32px rgba(26, 29, 26, .09);--font-ui: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace}*{box-sizing:border-box}html,body{margin:0;padding:0}body{background:var(--paper);color:var(--ink);font-family:var(--font-ui);font-size:14px;line-height:1.55;-webkit-font-smoothing:antialiased}a{color:var(--pine);text-decoration:none}a:hover{text-decoration:underline}code,.mono{font-family:var(--font-mono);font-size:.92em}:focus-visible{outline:2px solid rgba(30,92,72,.45);outline-offset:2px;border-radius:4px}::selection{background:#7ba58f59}.app{display:flex;min-height:100vh}.sidebar{position:fixed;inset:0 auto 0 0;width:220px;padding:22px 14px 18px;border-right:1px solid var(--hairline);display:flex;flex-direction:column;gap:4px;background:var(--paper);z-index:10}.wordmark{display:flex;align-items:baseline;gap:6px;padding:2px 10px 4px;font-size:20px;font-weight:650;letter-spacing:.01em;color:var(--pine);text-decoration:none}.wordmark:hover{text-decoration:none}.wordmark-dot{width:6px;height:6px;border-radius:50%;background:var(--sage);transform:translateY(-1px)}.wordmark-sub{padding:0 10px 18px;font-size:11px;color:var(--ink-faint)}.nav-item{display:flex;align-items:center;gap:10px;padding:8px 10px;border-radius:8px;color:var(--ink-soft);font-size:13.5px;font-weight:500;text-decoration:none;transition:background .12s ease,color .12s ease}.nav-item .icon{opacity:.75}.nav-item:hover{background:#1e5c480f;color:var(--ink);text-decoration:none}.nav-item.active{background:var(--pine-tint);color:var(--pine);font-weight:600}.nav-item.active .icon{opacity:1}.nav-count{margin-left:auto;font-size:11px;font-weight:600;color:var(--pine);background:#1e5c481a;border-radius:999px;padding:1px 7px}.sidebar-foot{margin-top:auto;padding:12px 10px 0;border-top:1px solid var(--hairline-soft);font-size:11px;color:var(--ink-faint);display:flex;flex-direction:column;gap:6px}.main{flex:1;margin-left:220px;padding:34px 44px 90px;min-width:0}.content{max-width:1100px;margin:0 auto}.page-head{display:flex;align-items:flex-start;gap:14px;margin-bottom:22px;flex-wrap:wrap}.page-head h1{margin:0;font-size:24px;font-weight:650;letter-spacing:-.01em}.page-sub{margin-top:3px;color:var(--ink-soft);font-size:13px}.page-head-actions{margin-left:auto;display:flex;align-items:center;gap:10px}.section-title{font-size:12px;font-weight:650;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-faint);margin:30px 0 10px}.back-link{display:inline-flex;align-items:center;gap:6px;font-size:13px;color:var(--ink-soft);margin-bottom:12px}.back-link:hover{color:var(--pine)}.card{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow)}.card-pad{padding:18px 20px}.card-title{font-size:13px;font-weight:650;margin:0 0 12px;display:flex;align-items:center;gap:8px;color:var(--ink)}.card-title .icon{color:var(--sage)}.pill{display:inline-flex;align-items:center;gap:4px;font-size:11px;font-weight:600;line-height:1.5;padding:2px 9px;border-radius:999px;white-space:nowrap}.pill-green{background:#1e5c481a;color:var(--pine)}.pill-sage{background:#7ba58f2e;color:#47705c}.pill-amber{background:#a8721c1f;color:var(--warn)}.pill-red{background:#b3402e1a;color:var(--danger)}.pill-neutral{background:#1a1d1a0f;color:var(--ink-soft)}.pill-gold{background:#c9a24b29;color:#8a6a1f}.dot{width:8px;height:8px;border-radius:50%;display:inline-block}.dot-green{background:#2e8c66;box-shadow:0 0 0 3px #2e8c6626}.dot-faint{background:var(--ink-faint);opacity:.5}.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;border-radius:8px;padding:8px 15px;font-family:var(--font-ui);font-size:13px;font-weight:600;line-height:1.4;border:1px solid transparent;cursor:pointer;transition:background .13s ease,border-color .13s ease,color .13s ease,box-shadow .13s ease;white-space:nowrap}.btn:disabled{opacity:.45;cursor:default;pointer-events:none}.btn-primary{background:var(--pine);color:#fff}.btn-primary:hover{background:var(--pine-dark);box-shadow:0 2px 10px #1e5c4840}.btn-secondary{background:var(--card);border-color:var(--hairline);color:var(--ink)}.btn-secondary:hover{border-color:#cfc7b8;background:#f6f2ea}.btn-danger{background:transparent;border-color:#b3402e73;color:var(--danger)}.btn-danger:hover{background:#b3402e12;border-color:var(--danger)}.btn-sm{padding:4px 11px;font-size:12px;border-radius:7px}.btn-ghost{background:transparent;border:none;color:var(--ink-soft);padding:4px 8px}.btn-ghost:hover{color:var(--pine);background:#1e5c480f}.spin .icon{animation:spin .9s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.input,.select{font-family:var(--font-ui);font-size:13px;color:var(--ink);background:var(--card);border:1px solid var(--hairline);border-radius:8px;padding:8px 11px;width:100%;transition:border-color .13s ease,box-shadow .13s ease}.input:focus,.select:focus{outline:none;border-color:var(--sage);box-shadow:0 0 0 3px #7ba58f33}.input::placeholder{color:var(--ink-faint)}textarea.input{resize:vertical;min-height:150px;line-height:1.6}.select{width:auto;padding-right:28px;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23565d56' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 10px center}.field{display:flex;flex-direction:column;gap:5px;margin-bottom:12px}.field label{font-size:12px;font-weight:600;color:var(--ink-soft)}.field-hint{font-size:11.5px;color:var(--ink-faint);line-height:1.5}.form-error{color:var(--danger);font-size:12.5px;margin-top:6px}.filter-row{display:flex;align-items:center;gap:8px;margin-bottom:16px;flex-wrap:wrap}.filter-pill{border:1px solid var(--hairline);background:transparent;color:var(--ink-soft);font-size:12.5px;font-weight:550;padding:5px 13px;border-radius:999px;cursor:pointer;font-family:var(--font-ui);transition:all .12s ease}.filter-pill:hover{border-color:var(--sage);color:var(--pine)}.filter-pill.active{background:var(--pine);border-color:var(--pine);color:#fff}.filter-search{margin-left:auto;width:220px}.case-list{display:flex;flex-direction:column;gap:10px}.case-row{display:flex;align-items:center;gap:18px;padding:15px 18px;background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);color:inherit;transition:border-color .13s ease,box-shadow .13s ease,transform .13s ease}.case-row:hover{text-decoration:none;border-color:var(--sage);box-shadow:var(--shadow-lift);transform:translateY(-1px)}.case-row-main{flex:1;min-width:0}.case-row-top{display:flex;align-items:center;gap:8px;min-width:0}.case-row-subject{font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.case-row-sub{margin-top:3px;font-size:12.5px;color:var(--ink-soft);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.case-row-customer{color:var(--ink);font-weight:500}.case-row-side{display:flex;align-items:center;gap:10px;flex-shrink:0}.case-row-time{font-size:12px;color:var(--ink-faint);width:64px;text-align:right}.case-row-category{font-size:11.5px;color:var(--ink-faint)}.case-head{margin-bottom:20px}.case-head h1{margin:0 0 6px;font-size:21px;font-weight:650;letter-spacing:-.01em}.case-meta{display:flex;align-items:center;gap:10px;flex-wrap:wrap;font-size:13px;color:var(--ink-soft)}.case-meta .sep{color:var(--hairline)}.case-head-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}.case-head-main{flex:1;min-width:280px}.status-changer{display:flex;align-items:center;gap:8px}.case-grid{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:22px;align-items:start}.case-col-main,.case-col-side{display:flex;flex-direction:column;gap:20px;min-width:0}@media (max-width: 1020px){.case-grid{grid-template-columns:minmax(0,1fr)}}.thread{display:flex;flex-direction:column;gap:14px}.bubble{max-width:82%;border-radius:12px;padding:11px 14px 12px}.bubble-in{align-self:flex-start;background:var(--card);border:1px solid var(--hairline);border-bottom-left-radius:4px}.bubble-out{align-self:flex-end;background:#1e5c4812;border:1px solid rgba(30,92,72,.16);border-bottom-right-radius:4px}.bubble-meta{font-size:11.5px;color:var(--ink-faint);margin-bottom:6px;display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}.bubble-meta strong{color:var(--ink-soft);font-weight:600}.bubble-body{font-family:var(--font-mono);font-size:12.5px;line-height:1.6;white-space:pre-wrap;overflow-wrap:break-word;margin:0}.draft-actions{display:flex;align-items:center;gap:10px;margin-top:12px;flex-wrap:wrap}.draft-byline{font-size:11.5px;color:var(--ink-faint);margin-left:auto}.warn-chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}.warn-chip{display:inline-flex;align-items:center;gap:6px;background:#a8721c1a;border:1px solid rgba(168,114,28,.3);color:var(--warn);font-size:12px;font-weight:500;border-radius:8px;padding:4px 10px}.actions-head{display:flex;align-items:center;gap:12px;margin-bottom:4px;flex-wrap:wrap}.actions-head-note{font-size:12px;color:var(--ink-faint)}.action-list{display:flex;flex-direction:column;gap:12px}.action-card{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:14px 16px;position:relative}.action-card.financial{border-left:3px solid var(--gold)}.action-card.executed{background:#fcfbf8}.action-head{display:flex;align-items:center;gap:9px;flex-wrap:wrap}.action-check{width:16px;height:16px;accent-color:var(--pine);cursor:pointer}.action-icon{color:var(--pine);display:inline-flex}.action-title{font-weight:650;font-size:13.5px}.order-chip{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-faint);background:#1a1d1a0d;border-radius:5px;padding:1px 6px}.action-head-right{margin-left:auto;display:flex;align-items:center;gap:8px}.confidence{font-size:11.5px;color:var(--ink-faint)}.action-summary{margin:8px 0 0 25px;font-size:13.5px;color:var(--ink)}.action-body{margin-left:25px}.action-error{margin-top:10px;background:#b3402e12;border:1px solid rgba(179,64,46,.25);color:var(--danger);font-size:12.5px;border-radius:8px;padding:8px 11px}.action-result{margin-top:10px;background:#1e5c480f;border:1px solid rgba(30,92,72,.18);color:var(--pine);font-size:12.5px;border-radius:8px;padding:8px 11px}.action-foot{display:flex;align-items:center;margin-top:10px;gap:10px}.reply-preview{font-family:var(--font-mono);font-size:12px;line-height:1.6;white-space:pre-wrap;overflow-wrap:break-word;background:#faf8f4;border:1px solid var(--hairline-soft);border-radius:8px;padding:10px 12px;margin:8px 0 0;max-height:260px;overflow:auto}details.disclosure>summary{cursor:pointer;font-size:12px;font-weight:600;color:var(--ink-faint);-webkit-user-select:none;user-select:none;list-style:none;display:inline-flex;align-items:center;gap:5px}details.disclosure>summary::-webkit-details-marker{display:none}details.disclosure>summary:before{content:"▸";font-size:10px;transition:transform .12s ease}details.disclosure[open]>summary:before{transform:rotate(90deg)}details.disclosure>summary:hover{color:var(--pine)}details.disclosure{margin-top:10px}.json-view{font-family:var(--font-mono);font-size:11.5px;line-height:1.55;background:#f6f3ec;border:1px solid var(--hairline-soft);border-radius:8px;padding:10px 12px;margin:8px 0 0;overflow-x:auto;max-height:320px;overflow-y:auto;white-space:pre}.execute-bar{display:flex;align-items:center;gap:12px;padding:13px 16px;background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);position:sticky;bottom:16px;z-index:5}.execute-bar-note{font-size:12px;color:var(--ink-faint)}.results-panel{border:1px solid var(--hairline);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow);overflow:hidden}.results-panel-head{padding:12px 16px;font-weight:650;font-size:13px;border-bottom:1px solid var(--hairline-soft);display:flex;align-items:center;gap:8px}.result-row{display:flex;align-items:flex-start;gap:10px;padding:10px 16px;font-size:13px;border-bottom:1px solid var(--hairline-soft)}.result-row:last-child{border-bottom:none}.result-row .icon{margin-top:2px;flex-shrink:0}.result-ok .icon{color:var(--pine)}.result-fail .icon,.result-fail{color:var(--danger)}.guard-banner{display:flex;gap:14px;align-items:flex-start;background:linear-gradient(180deg,#fdf7ea,#fcf4e2);border:1px solid rgba(168,114,28,.4);border-left:4px solid var(--warn);border-radius:var(--radius);padding:16px 18px;box-shadow:var(--shadow)}.guard-banner .icon{color:var(--warn);flex-shrink:0;margin-top:1px}.guard-banner-title{font-weight:700;font-size:13.5px;color:#7c5413;margin-bottom:3px}.guard-banner-body{font-size:13px;color:#6b5426;line-height:1.55}.guard-banner-actions{display:flex;gap:8px;margin-top:10px}.result-warn,.result-warn .icon{color:var(--warn)}.case-alerts{display:flex;flex-direction:column;gap:14px;margin-bottom:20px}.micro-label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-faint);margin:12px 0 4px}.side-details>summary{padding:14px 18px;cursor:pointer;font-weight:650;font-size:13px;display:flex;align-items:center;gap:8px;list-style:none;-webkit-user-select:none;user-select:none}.side-details>summary::-webkit-details-marker{display:none}.side-details>summary .icon{color:var(--sage)}.side-details>summary:hover{color:var(--pine)}.side-details-body{padding:0 18px 16px}.side-stat{display:flex;align-items:baseline;justify-content:space-between;gap:10px;padding:6px 0;font-size:13px}.side-stat-label{color:var(--ink-soft);font-size:12px}.side-stat-value{font-weight:650}.side-list{display:flex;flex-direction:column;gap:8px;margin-top:4px}.side-item{border:1px solid var(--hairline-soft);border-radius:8px;padding:8px 10px;font-size:12.5px}.side-item-head{display:flex;align-items:center;gap:8px;justify-content:space-between}.side-item-sub{color:var(--ink-faint);font-size:11.5px;margin-top:2px}.timeline{display:flex;flex-direction:column}.tl-item{display:flex;gap:12px;position:relative;padding:0 0 16px}.tl-rail{display:flex;flex-direction:column;align-items:center;flex-shrink:0;width:26px}.tl-icon{width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;border:1px solid var(--hairline);background:var(--card);z-index:1}.tl-icon .icon{width:13px;height:13px}.tl-icon-human{color:var(--pine);border-color:#1e5c4859}.tl-icon-agent{color:#5d8871;border-color:#7ba58f80}.tl-icon-system{color:var(--ink-faint)}.tl-line{width:1px;flex:1;background:var(--hairline);margin-top:2px}.tl-item:last-child .tl-line{display:none}.tl-body{flex:1;min-width:0;padding-top:3px}.tl-head{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap;font-size:13px}.tl-event{font-weight:600}.tl-time{color:var(--ink-faint);font-size:11.5px;margin-left:auto;white-space:nowrap}.tl-detail{color:var(--ink-soft);font-size:12px;margin-top:2px;overflow-wrap:break-word}.timeline-compact .tl-item{padding-bottom:10px}.timeline-compact .tl-icon{width:20px;height:20px}.timeline-compact .tl-icon .icon{width:11px;height:11px}.timeline-compact .tl-rail{width:20px}.timeline-compact .tl-head{font-size:12.5px}.table-wrap{overflow-x:auto}.table{width:100%;border-collapse:collapse;font-size:13px}.table th{text-align:left;font-size:10.5px;font-weight:650;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-faint);padding:9px 14px;border-bottom:1px solid var(--hairline);white-space:nowrap}.table td{padding:9px 14px;border-bottom:1px solid var(--hairline-soft);vertical-align:top}.table tr:last-child td{border-bottom:none}.table .mono{font-size:12px}.table-caption{font-size:12px;color:var(--ink-faint);padding:10px 14px 12px;border-top:1px solid var(--hairline-soft)}.table-caption code{background:#1a1d1a0d;border-radius:4px;padding:1px 5px}.storage-banner{display:flex;align-items:flex-start;gap:14px;background:linear-gradient(180deg,#fff,#f7f4ed);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 20px;margin-bottom:22px}.storage-banner .icon{color:var(--pine);width:22px;height:22px;flex-shrink:0;margin-top:2px}.storage-banner-title{font-weight:650;font-size:14.5px}.storage-banner-title .mono{color:var(--pine)}.storage-banner-sub{font-size:12.5px;color:var(--ink-soft);margin-top:2px}.connector-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}.connector-head{display:flex;align-items:center;gap:10px;margin-bottom:12px}.connector-name{font-weight:650;font-size:14.5px}.connector-head .pill{margin-left:auto}.kv-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:5px 0;font-size:12.5px;border-bottom:1px dashed var(--hairline-soft)}.kv-row:last-of-type{border-bottom:none}.kv-key{color:var(--ink-faint);flex-shrink:0}.kv-val{text-align:right;overflow-wrap:anywhere}.connector-foot{margin-top:14px;display:flex;justify-content:flex-end}.warn-banner{display:flex;gap:10px;align-items:flex-start;background:#a8721c17;border:1px solid rgba(168,114,28,.35);color:#7c5413;border-radius:8px;padding:10px 13px;font-size:12.5px;line-height:1.5;margin-bottom:12px}.warn-banner .icon{color:var(--warn);flex-shrink:0;margin-top:1px}.telemetry-card{display:flex;gap:14px;align-items:flex-start;background:#1e5c480d;border:1px solid rgba(30,92,72,.2);border-radius:var(--radius);padding:16px 18px}.telemetry-card .icon{color:var(--pine);flex-shrink:0;margin-top:2px}.telemetry-card-title{font-weight:650;color:var(--pine);margin-bottom:2px}.telemetry-card-body{font-size:12.5px;color:var(--ink-soft)}.progress-track{height:6px;border-radius:999px;background:#1a1d1a12;overflow:hidden;width:200px}.progress-fill{height:100%;border-radius:999px;background:var(--pine);transition:width .3s ease}.cal-card{margin-bottom:18px}.cal-meta{display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ink-faint);margin-bottom:10px;flex-wrap:wrap}.cal-customer{background:#f7f4ed;border:1px solid var(--hairline-soft);border-radius:8px;padding:11px 14px;margin-bottom:14px}.cal-label{font-size:10.5px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:var(--ink-faint);margin-bottom:5px}.cal-text{font-size:13px;white-space:pre-wrap;overflow-wrap:break-word;margin:0;font-family:var(--font-ui);line-height:1.6}.cal-cols{display:grid;grid-template-columns:1fr 1fr;gap:14px}@media (max-width: 860px){.cal-cols{grid-template-columns:1fr}}.cal-col{border:1px solid var(--hairline);border-radius:8px;padding:11px 14px;min-width:0}.cal-col-clearhand{border-color:#1e5c4840;background:#1e5c4808}.cal-col-clearhand .cal-label{color:var(--pine)}.cal-review{display:flex;align-items:center;gap:8px;margin-top:14px;flex-wrap:wrap}.verdict-btn{border:1px solid var(--hairline);background:var(--card);color:var(--ink-soft);font-family:var(--font-ui);font-size:12px;font-weight:600;padding:5px 12px;border-radius:999px;cursor:pointer;transition:all .12s ease}.verdict-btn:hover{border-color:var(--sage);color:var(--pine)}.verdict-btn.v-fine:hover{background:#1e5c4812}.verdict-btn.v-bad:hover{border-color:#b3402e80;color:var(--danger);background:#b3402e0d}.cal-note-input{flex:1;min-width:180px}.cal-reviewed{display:flex;align-items:center;gap:10px;margin-top:14px;font-size:12.5px;color:var(--ink-soft)}.overlay-banner{display:flex;gap:12px;align-items:flex-start;background:#7ba58f1a;border:1px solid rgba(123,165,143,.35);border-radius:var(--radius);padding:14px 17px;font-size:13px;color:#3f6853;margin-bottom:20px}.overlay-banner .icon{color:var(--pine);flex-shrink:0;margin-top:1px}.overlay-banner code{background:#1e5c4814;border-radius:4px;padding:1px 5px}.overlay-grid{display:grid;grid-template-columns:280px minmax(0,1fr);gap:18px;align-items:start}@media (max-width: 900px){.overlay-grid{grid-template-columns:1fr}}.file-item{display:block;width:100%;text-align:left;background:var(--card);border:1px solid var(--hairline);border-radius:8px;padding:10px 13px;cursor:pointer;font-family:var(--font-ui);transition:border-color .12s ease,background .12s ease}.file-item+.file-item{margin-top:8px}.file-item:hover{border-color:var(--sage)}.file-item.active{border-color:var(--pine);background:#1e5c480a}.file-item.missing{opacity:.55}.file-name{font-family:var(--font-mono);font-size:12.5px;font-weight:600;color:var(--ink)}.file-meta{font-size:11px;color:var(--ink-faint);margin-top:2px}.doc-view{padding:22px 26px;overflow-x:auto}.doc-view h1,.doc-view h2,.doc-view h3,.doc-view h4{font-weight:650;letter-spacing:-.01em;margin:1.4em 0 .5em;line-height:1.3}.doc-view h1{font-size:20px;margin-top:.2em}.doc-view h2{font-size:16.5px}.doc-view h3{font-size:14.5px}.doc-view h4{font-size:13.5px}.doc-view p,.doc-view li{font-size:13.5px;line-height:1.65}.doc-view ul,.doc-view ol{padding-left:22px}.doc-view code{background:#1a1d1a0d;border-radius:4px;padding:1px 5px;font-size:12px}.doc-view pre{background:#f6f3ec;border:1px solid var(--hairline-soft);border-radius:8px;padding:12px 14px;overflow-x:auto;font-family:var(--font-mono);font-size:12px;line-height:1.55}.doc-view pre code{background:none;padding:0}.doc-view blockquote{margin:.8em 0;padding:2px 16px;border-left:3px solid var(--sage);color:var(--ink-soft)}.doc-view hr{border:none;border-top:1px solid var(--hairline);margin:1.6em 0}.signal-card{padding:13px 16px}.signal-head{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.signal-summary{font-size:13px;margin-top:6px;color:var(--ink-soft)}.signal-diff-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:8px}@media (max-width: 800px){.signal-diff-grid{grid-template-columns:1fr}}.state-block{display:flex;align-items:center;justify-content:center;gap:10px;color:var(--ink-faint);font-size:13px;padding:48px 0}.spinner{width:16px;height:16px;border-radius:50%;border:2px solid rgba(30,92,72,.2);border-top-color:var(--pine);animation:spin .8s linear infinite}.error-box{display:flex;align-items:flex-start;gap:12px;background:#b3402e0f;border:1px solid rgba(179,64,46,.3);border-radius:var(--radius);padding:14px 16px;color:var(--danger);font-size:13px;margin:12px 0}.error-box .icon{flex-shrink:0;margin-top:1px}.error-box-body{flex:1}.error-box-body strong{display:block;margin-bottom:2px}.empty-state{text-align:center;padding:52px 24px;background:var(--card);border:1px dashed var(--hairline);border-radius:var(--radius);color:var(--ink-soft)}.empty-state-title{font-weight:650;font-size:14.5px;color:var(--ink);margin-bottom:6px}.empty-state-body{font-size:13px;max-width:460px;margin:0 auto;line-height:1.6}.toast-stack{position:fixed;right:20px;bottom:20px;display:flex;flex-direction:column;gap:10px;z-index:100;max-width:380px}.toast{display:flex;align-items:flex-start;gap:10px;background:var(--card);border:1px solid var(--hairline);border-left-width:3px;border-radius:10px;box-shadow:var(--shadow-lift);padding:11px 15px;font-size:13px;animation:toast-in .22s ease}.toast .icon{flex-shrink:0;margin-top:2px}.toast-success{border-left-color:var(--pine)}.toast-success .icon{color:var(--pine)}.toast-error{border-left-color:var(--danger)}.toast-error .icon{color:var(--danger)}.toast-warn{border-left-color:var(--warn)}.toast-warn .icon{color:var(--warn)}.toast-info{border-left-color:var(--sage)}.toast-info .icon{color:var(--sage)}@keyframes toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}}.modal-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#1a1d1a61;display:flex;align-items:center;justify-content:center;z-index:90;padding:24px}.modal{background:var(--card);border-radius:12px;box-shadow:0 24px 64px #1a1d1a40;width:100%;max-width:500px;max-height:85vh;display:flex;flex-direction:column;animation:toast-in .18s ease}.modal-title{font-weight:650;font-size:15px;padding:18px 22px 0}.modal-body{padding:12px 22px 18px;overflow-y:auto;font-size:13.5px;color:var(--ink-soft)}.modal-footer{display:flex;justify-content:flex-end;gap:10px;padding:14px 22px 18px;border-top:1px solid var(--hairline-soft)}.run-list{margin:10px 0 0;padding:0;list-style:none;display:flex;flex-direction:column;gap:8px}.run-list li{display:flex;align-items:baseline;gap:10px;font-size:13.5px;color:var(--ink)}.run-num{font-family:var(--font-mono);font-size:11px;color:var(--ink-faint);background:#1a1d1a0d;border-radius:5px;padding:1px 6px;flex-shrink:0}.run-money{color:#8a6a1f;font-weight:600}.modal-note{margin-top:14px;font-size:12px;color:var(--ink-faint)}@media (max-width: 760px){.sidebar{position:static;width:auto;flex-direction:row;align-items:center;border-right:none;border-bottom:1px solid var(--hairline);overflow-x:auto;gap:2px;padding:12px 14px}.wordmark-sub,.sidebar-foot{display:none}.app{flex-direction:column}.main{margin-left:0;padding:22px 18px 60px}}.onb{--onb-pine-wash: rgba(30, 92, 72, .08);--onb-amber-wash: rgba(168, 114, 28, .09);--onb-serif: "Iowan Old Style", Palatino, Georgia, "Times New Roman", serif}.onb .quiet-link{color:var(--ink-faint);font-size:12.5px;cursor:pointer;background:none;border:none;padding:0;font-family:var(--font-ui);text-decoration:none}.onb .quiet-link:hover{color:var(--pine);text-decoration:underline}.onb .quiet-link:disabled{opacity:.55;cursor:default}@keyframes onb-land{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}.onb-top{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:24px;background:#faf7f2f2;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-bottom:1px solid var(--hairline-soft);margin:-34px 0 26px;padding:18px 2px 14px}.onb-top-title h1{margin:0;font-size:24px;font-weight:650;letter-spacing:-.01em}.onb-hand-caption{flex:1;text-align:right;padding-right:8px;opacity:0;transition:opacity .8s ease .3s;font-family:var(--onb-serif);font-size:15px;color:var(--pine-dark);min-width:0}.onb-hand-caption .hc-sub{display:block;font-family:var(--font-ui);font-size:11.5px;color:var(--ink-faint)}.onb-hand-caption.show{opacity:1}.onb-hand-cluster{display:flex;align-items:center;gap:16px;flex:none;margin-left:auto}.onb-hand{display:block}.onb-hand .h-outline{fill:none;stroke:var(--ink);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.onb-hand .h-fill{fill:var(--pine)}.onb-hand .palm-fill{fill:var(--pine);opacity:0;transition:opacity .8s ease .25s}.onb-hand.complete .palm-fill{opacity:1}.onb-foundations{list-style:none;margin:0;padding:0}.onb-foundations li{display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ink-faint);line-height:1.45;padding:1px 0}.onb-foundations li .fdot{flex:none;width:14px;height:14px;border-radius:50%;border:1.5px solid var(--hairline);display:inline-flex;align-items:center;justify-content:center;font-size:8.5px;font-weight:700;color:transparent;transition:background .4s ease,border-color .4s ease,color .4s ease}.onb-foundations li.active{color:var(--ink);font-weight:600}.onb-foundations li.done{color:var(--ink-soft)}.onb-foundations li.done .fdot{background:var(--pine);border-color:var(--pine);color:#fdfcf9}.onb-screen{padding:10px 0 34px;border-bottom:1px solid var(--hairline-soft);margin-bottom:26px}.onb-screen:last-of-type{border-bottom:none}.onb-screen-head{margin-bottom:20px}.onb-kicker{display:block;font-size:11px;font-weight:700;letter-spacing:.14em;text-transform:uppercase;color:var(--pine);margin-bottom:8px}.onb-head-row{display:flex;align-items:baseline;justify-content:space-between;gap:20px;flex-wrap:wrap}.onb-head-row h2{margin:0 0 6px;font-size:21px;font-weight:650;letter-spacing:-.01em}.onb-head-meta{display:flex;align-items:baseline;gap:14px;font-size:12px;color:var(--ink-faint)}.onb-lede{margin:4px 0 0;font-size:14px;color:var(--ink-soft);max-width:46rem}.onb-hint{font-size:13px;color:var(--ink-faint);margin:0 0 16px}.onb-chips{display:flex;flex-wrap:wrap;gap:10px;margin:18px 0 24px}.onb-chip{display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;letter-spacing:.03em;color:var(--pine-dark);background:var(--onb-pine-wash);border:1px solid rgba(30,92,72,.18);border-radius:999px;padding:4px 12px}.onb-cta-row{display:flex;align-items:center;gap:18px;flex-wrap:wrap}.onb-pledge-line{font-size:12px;color:var(--ink-faint);margin:14px 0 0}.onb-pledge-line b{color:var(--ink-soft);font-weight:600}.onb-demo-note{font-size:12px;color:var(--warn);margin:0 0 12px}.onb-connect-grid{display:grid;grid-template-columns:minmax(0,6fr) minmax(0,5fr);gap:22px;align-items:start}.onb-panel{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 22px}.onb-panel h4{font-size:13.5px;font-weight:650;margin:0 0 14px}.onb-form-hint{font-size:12.5px;color:var(--ink-soft);margin:2px 0 14px;line-height:1.55}.onb .field-hint .kc{color:var(--pine-dark);font-weight:600}details.onb-alt-imap{margin:-4px 0 14px}details.onb-alt-imap summary{cursor:pointer;font-size:12.5px;color:var(--ink-faint);list-style:none}details.onb-alt-imap summary::-webkit-details-marker{display:none}details.onb-alt-imap summary:after{content:" →"}details.onb-alt-imap[open] summary:after{content:""}details.onb-alt-imap .row2{display:grid;grid-template-columns:2fr 1fr;gap:10px;margin-top:10px}.onb-verify-row{display:flex;align-items:center;gap:14px;margin-top:4px}.onb-connected{animation:onb-land .15s ease-out}.onb-payoff{font-size:14px;color:var(--ink);line-height:1.6;margin:0 0 14px}.onb-payoff .ok{color:var(--pine);font-weight:700}.onb-payoff strong{font-weight:650}.onb-skipped{font-size:13px;color:var(--ink-faint);margin:0}.onb-key-warn{display:none;font-size:12.5px;color:var(--warn);background:var(--onb-amber-wash);border:1px solid rgba(168,114,28,.3);border-radius:8px;padding:8px 12px;margin-top:8px;line-height:1.5}.onb-key-warn.show{display:block}.onb-disclosure{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 22px}.onb-disclosure .d-block{margin-bottom:16px}.onb-disclosure .d-block:last-child{margin-bottom:0}.onb-disclosure .d-label{display:block;font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;margin-bottom:4px}.onb-disclosure .d-read .d-label{color:var(--pine)}.onb-disclosure .d-can .d-label{color:var(--warn)}.onb-disclosure .d-never .d-label{color:var(--danger)}.onb-disclosure p{margin:0;font-size:13px;color:var(--ink-soft);line-height:1.6}.onb-scopes{margin:0 0 6px;padding:0;list-style:none}.onb-scopes li{display:flex;align-items:center;gap:9px;font-family:var(--font-mono);font-size:12.5px;color:var(--ink);padding:5px 0}.onb-scopes li .tick{color:var(--pine);font-weight:700;font-size:12px}.onb-scopes li .why{font-family:var(--font-ui);color:var(--ink-faint);font-size:11.5px;margin-left:auto}.onb-scope-actions{display:flex;align-items:center;gap:14px;margin:10px 0 18px}.onb-tracks{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;align-items:start}.onb-tracks>*{min-width:0}.onb-track{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:14px 14px 10px;transition:border-color .4s ease}.onb-track.done{border-color:#1e5c4866}.onb-track .track-head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:10px}.onb-track .track-name{font-size:14px;font-weight:650}.onb-track .track-file{font-family:var(--font-mono);font-size:11px;color:var(--ink-faint);white-space:nowrap}.onb-track .track-activity{display:flex;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-soft);min-height:1.6em;margin-bottom:12px}.onb .pulse{flex:none;width:7px;height:7px;border-radius:50%;background:var(--hairline)}.onb-track.running .pulse{background:var(--pine);animation:onb-pulse 1.2s ease-in-out infinite}.onb-track.done .pulse{background:var(--pine);animation:none}@keyframes onb-pulse{0%,to{opacity:1}50%{opacity:.35}}.onb-track.done .track-activity{color:var(--pine-dark)}.onb-track .track-findings{display:flex;flex-direction:column;gap:8px;min-height:120px}.onb-track .track-foot{display:flex;align-items:baseline;gap:8px;margin-top:14px;padding-top:10px;border-top:1px solid var(--hairline-soft);font-family:var(--font-mono);font-size:11px;color:var(--ink-faint)}.onb-track .track-foot .fname{color:var(--pine-dark)}.onb-track .track-foot .bytes{margin-left:auto}.onb-finding{border:1px solid var(--hairline);border-radius:9px;background:var(--paper);padding:9px 12px;font-size:12.5px;line-height:1.5;color:var(--ink);animation:onb-land .15s ease-out both}.onb-finding .f-src{float:right;font-family:var(--font-mono);font-size:10px;color:var(--ink-faint);border:1px solid var(--hairline-soft);border-radius:5px;padding:1px 6px;margin-left:8px}.onb-finding .f-title{font-weight:650}.onb-finding .f-sub{color:var(--ink-soft);font-size:12px}.onb-finding .f-kv{display:flex;gap:8px;align-items:baseline}.onb-finding .f-kv+.f-kv{margin-top:3px}.onb-finding .f-k{font-family:var(--font-mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.04em;color:var(--ink-faint, var(--ink-soft));min-width:62px}.onb-finding.f-chip{font-family:var(--font-mono);font-size:11.5px;color:var(--ink)}.onb-finding.conflict{background:var(--onb-amber-wash);border-color:#a8721c59}.onb-finding.conflict .c-label{font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--warn);display:block;margin-bottom:2px}.onb-finding .c-btns{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}.onb-finding .c-btn{font-family:var(--font-ui);font-size:11.5px;font-weight:600;padding:5px 10px;border-radius:7px;border:1px solid rgba(168,114,28,.4);background:var(--card);color:var(--ink);cursor:pointer}.onb-finding .c-btn:hover{border-color:var(--warn)}.onb-finding .c-btn.picked{background:var(--pine);border-color:var(--pine);color:#fdfcf9}.onb-finding .c-btn:disabled{cursor:default}.onb-finding .c-note{display:none;font-size:11.5px;color:var(--pine-dark);margin-top:7px}.onb-finding.resolved{border-color:#1e5c4873}.onb-finding.resolved .c-note{display:block}.onb-finding .e-label{font-size:11.5px;color:var(--ink-faint);display:block;margin-bottom:5px}.onb-finding blockquote{margin:0 0 8px;padding:2px 0 2px 12px;border-left:2.5px solid var(--sage);font-family:var(--onb-serif);font-style:italic;font-size:13px;color:var(--ink-soft)}.onb-finding .redact{display:inline-block;width:3.2em;height:.72em;background:#1a1d1ab3;border-radius:2px;vertical-align:baseline}.onb-finding .vote-btns{display:flex;gap:8px;align-items:center}.onb-finding .vote{font-size:13px;padding:3px 10px;border-radius:7px;border:1px solid var(--hairline);background:var(--card);cursor:pointer;line-height:1.4}.onb-finding .vote:hover{border-color:var(--sage)}.onb-finding .vote.picked{border-color:var(--pine);background:var(--onb-pine-wash)}.onb-finding .vote:disabled{cursor:default}.onb-finding .vote-note{font-size:11.5px;color:var(--pine-dark)}.onb-finding.excerpt.dropped blockquote{opacity:.45}.onb-finding .seg{display:inline-flex;border:1px solid var(--hairline);border-radius:8px;overflow:hidden}.onb-finding .seg button{font-family:var(--font-ui);font-size:11.5px;font-weight:600;padding:4px 12px;border:none;background:var(--card);color:var(--ink-faint);cursor:pointer}.onb-finding .seg button.on{background:var(--pine);color:#fdfcf9}.onb-finding .p-row{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:8px}.onb-finding .p-seen{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-faint)}.onb-finding.contrast{border-color:#1e5c4859;background:var(--card)}.onb-finding .ct-row{margin-bottom:8px}.onb-finding .ct-label{display:block;font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase}.onb-finding .ct-said .ct-label{color:var(--ink-faint)}.onb-finding .ct-did .ct-label{color:var(--pine)}.onb-finding .ct-note{font-size:11.5px;color:var(--ink-faint);font-style:italic}.onb-finding .d-head{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-faint);margin-bottom:3px}.onb-finding .d-head .won{color:var(--pine);font-weight:700}.onb-finding .d-foot{font-size:11.5px;color:var(--ink-soft);margin-top:6px;font-style:italic}.onb-strip{display:flex;margin-top:22px;background:var(--onb-pine-wash);border:1px solid rgba(30,92,72,.25);border-radius:var(--radius);padding:14px 20px;align-items:center;gap:18px;flex-wrap:wrap;animation:onb-land .15s ease-out}.onb-strip p{margin:0;flex:1;min-width:240px;font-size:13.5px;color:var(--pine-dark)}.onb-style-cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:16px;margin-bottom:26px}.onb-style-card{text-align:left;font-family:var(--font-ui);background:var(--card);border:1.5px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:18px 18px 14px;cursor:pointer;transition:border-color .15s ease,box-shadow .15s ease;display:flex;flex-direction:column;gap:10px}.onb-style-card:hover{border-color:var(--sage)}.onb-style-card.selected{border-color:var(--pine);box-shadow:0 0 0 3px #1e5c481f}.onb-style-card .sc-head{font-size:15.5px;font-weight:650;display:flex;align-items:center;gap:8px;flex-wrap:wrap;color:var(--ink)}.onb-style-card .sc-rec{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--pine-dark);background:var(--onb-pine-wash);border-radius:999px;padding:2px 9px}.onb-style-card .sc-rule{margin:0;font-size:12.5px;color:var(--ink-soft);line-height:1.55;flex:1}.onb-style-card .sc-case{font-size:12.5px;color:var(--ink);border-top:1px solid var(--hairline-soft);padding-top:10px}.onb-style-card .sc-label{display:block;font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:2px}.onb-style-card .sc-cost{font-size:11px;color:var(--ink-faint);font-family:var(--font-mono)}.onb-tune-grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:22px;align-items:start;margin-bottom:26px}.onb-tune-list{list-style:none;margin:0;padding:0}.onb-tune-list li{display:flex;align-items:center;gap:10px;padding:8px 0;border-bottom:1px solid var(--hairline-soft);font-size:13px}.onb-tune-list li:last-child{border-bottom:none}.onb-tune-list input[type=checkbox]{accent-color:var(--pine);width:15px;height:15px;flex:none}.onb-tune-list .lock{margin-left:auto;font-size:10px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--pine-dark);background:var(--onb-pine-wash);border-radius:999px;padding:2px 8px;flex:none}.onb-slider-row{display:block;padding:10px 0;border-bottom:1px solid var(--hairline-soft)}.onb-slider-row .sl-label{display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px}.onb-slider-row .sl-note{font-size:11px;color:var(--ink-faint)}.onb-slider-row input[type=range]{width:100%;accent-color:var(--pine)}.onb-radio-row{display:flex;gap:20px;padding:8px 0;font-size:13px;flex-wrap:wrap}.onb-radio-row label{display:inline-flex;align-items:center;gap:7px;cursor:pointer}.onb-radio-row input{accent-color:var(--pine)}.onb-fine-note{margin:12px 0 0;font-size:12px;color:var(--ink-faint)}.onb-contract{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 22px}.onb-contract h4{font-size:13.5px;font-weight:650;margin:0 0 12px}.onb-contract textarea{width:100%;min-height:108px;resize:vertical;border:1px solid var(--hairline);border-radius:9px;background:var(--paper);padding:14px 16px;font-family:var(--onb-serif);font-size:15px;line-height:1.6;color:var(--ink)}.onb-contract textarea:focus{outline:2px solid rgba(30,92,72,.45);outline-offset:1px}.onb-contract .caption{font-size:12px;color:var(--ink-faint);margin:10px 0 0}.onb-contract .caption b{color:var(--ink-soft);font-weight:600}.onb-greet{display:flex;align-items:flex-start;gap:20px;background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);box-shadow:var(--shadow);padding:20px 24px;margin-bottom:20px}.onb-greet h3{font-size:19px;font-weight:650;margin:0 0 6px}.onb-greet p{margin:0;font-size:13.5px;color:var(--ink-soft);max-width:44rem}.onb-greet .btn{margin-left:auto;flex:none}.batch-counter{display:flex;align-items:center;gap:8px;font-family:var(--font-mono);font-size:11.5px;color:var(--ink-soft);margin-bottom:4px}.onb-batchwrap.running .batch-counter .pulse{background:var(--pine);animation:onb-pulse 1.2s ease-in-out infinite}.onb-batchwrap.done .batch-counter .pulse{background:var(--pine)}.bg-head{display:flex;align-items:baseline;gap:7px;font-size:12.5px;font-weight:650;margin:16px 0 0;animation:onb-land .15s ease-out both}.bg-count{color:var(--ink-faint);font-weight:500}.onb-brow{display:flex;align-items:center;gap:14px;background:var(--card);border:1px solid var(--hairline);border-radius:9px;padding:9px 14px;font-size:13px;margin-top:6px;animation:onb-land .15s ease-out both}.onb-brow .br-title{font-weight:550}.onb-brow .br-meta{margin-left:auto;color:var(--ink-faint);font-size:11.5px;text-align:right}.onb-brow.more{color:var(--ink-faint);border-style:dashed;background:transparent;font-size:12px}.onb-batch-end .be-cta{display:flex;flex-direction:column;align-items:center;gap:4px}.onb-batch-end .be-sub{font-size:11px;color:var(--ink-faint)}.onb-golive-block{margin-top:32px}.onb-golive-block h3{font-size:16px;font-weight:650;margin:0 0 12px}.onb-pmap-wrap{border:1px solid var(--hairline);border-radius:var(--radius);background:var(--card);box-shadow:var(--shadow);padding:20px 18px 8px;overflow-x:auto}.onb-pmap{display:block;min-width:760px}.onb-pmap .n-box{fill:var(--paper);stroke:#1a1d1a29;stroke-width:1;transition:stroke .15s ease}.onb-pmap g.node{cursor:pointer}.onb-pmap g.node:hover .n-box{stroke:var(--pine)}.onb-pmap .n-file .n-box{fill:var(--card);stroke:#1e5c4859}.onb-pmap .n-today .n-box{fill:var(--onb-pine-wash);stroke:#1e5c4880}.onb-pmap text{font-family:var(--font-ui);font-size:13px;fill:var(--ink)}.onb-pmap .t-mono{font-family:var(--font-mono);font-size:12px;fill:var(--pine-dark)}.onb-pmap .t-sub{font-family:var(--font-mono);font-size:10.5px;fill:var(--ink-faint)}.onb-pmap .edge{fill:none;stroke:#1a1d1a2e;stroke-width:1.2}.onb-pmap-caption{font-size:13px;color:var(--ink-soft);margin:12px 4px 10px;font-style:italic;font-family:var(--onb-serif)}.onb-enh{border-top:1px solid var(--hairline-soft);padding:24px 0 10px}.onb-enh .enh-head{font-size:11.5px;font-weight:700;letter-spacing:.13em;text-transform:uppercase;color:var(--ink-faint);margin-bottom:14px}.onb-enh .enh-head span{font-weight:500;letter-spacing:.02em;text-transform:none}.onb-enh .enh-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px;margin-bottom:14px}.onb-enh .enh-card{background:var(--card);border:1px solid var(--hairline);border-radius:var(--radius);padding:14px 16px;opacity:.55}.onb-enh .e-title{font-weight:650;font-size:13px;display:flex;align-items:baseline;gap:10px;justify-content:space-between}.onb-enh .e-tag{font-size:9.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);background:#1a1d1a12;border-radius:999px;padding:2px 9px;flex:none}.onb-enh .e-prov{font-family:var(--font-mono);font-size:10.5px;color:var(--ink-faint);margin:2px 0 8px}.onb-enh .enh-card p{margin:0;font-size:12.5px;color:var(--ink-soft);line-height:1.5}.nav-dot{margin-left:auto;width:7px;height:7px;border-radius:50%;background:var(--gold);flex:none}@media (max-width: 1160px){.onb-tracks,.onb-style-cards{grid-template-columns:1fr}}@media (max-width: 900px){.onb-connect-grid,.onb-tune-grid,.onb-enh .enh-grid{grid-template-columns:1fr}.onb-top{flex-wrap:wrap}.onb-hand-caption{order:3;text-align:left;flex-basis:100%}}.help-wrap{display:inline-flex;align-items:center}.help-term{display:inline-flex;align-items:center;gap:4px;cursor:help;border-bottom:1px dotted rgba(26,29,26,.35)}.help-term:has(.pill),.help-term:has(.order-chip),.help-term:has(.draft-byline),.help-term:has(.confidence){border-bottom:none}.help-dot{display:inline-flex;align-items:center;justify-content:center;width:14px;height:14px;border-radius:50%;border:1px solid var(--hairline);background:var(--card);color:var(--ink-faint);cursor:help;flex:none;vertical-align:1px}.help-dot:hover,.help-dot:focus-visible{border-color:var(--sage);color:var(--pine)}.help-i{font-family:Georgia,serif;font-style:italic;font-size:9.5px;font-weight:600;line-height:1}.help-pop{position:fixed;z-index:90;display:block;background:#21261f;color:#f2efe8;font-size:12px;font-weight:400;font-family:var(--font-ui);font-style:normal;letter-spacing:0;text-transform:none;line-height:1.55;padding:9px 12px;border-radius:8px;box-shadow:0 4px 18px #1a1d1a47;white-space:normal;pointer-events:none}.help-pop code{color:#cfe0d5;background:#ffffff14}.sort-control{display:inline-flex;align-items:center;gap:4px;margin-left:10px;padding-left:12px;border-left:1px solid var(--hairline)}.sort-label{font-size:10.5px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);margin-right:2px}.queue-foot{display:flex;align-items:center;gap:12px;margin-top:12px;padding:0 4px}.queue-count{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-faint)}.lr-card{padding:14px 18px 12px;margin-bottom:10px}.lr-card.lr-settled{opacity:.75}.lr-head{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;margin-bottom:8px;font-size:12px}.lr-category{color:var(--ink-soft);font-size:12px}.lr-target{color:var(--ink-faint);font-size:11.5px}.lr-rule{margin:0 0 10px;padding:4px 0 4px 14px;border-left:2.5px solid var(--sage);font-size:13.5px;line-height:1.6;color:var(--ink)}.lr-actions{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.lr-note{font-size:12px;color:var(--ink-faint)}.lr-note .mono{color:var(--ink-soft)}.action-policy{margin-top:6px;font-size:12px;color:var(--ink-soft)}.action-policy-label{font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:var(--pine);background:var(--pine-tint);border-radius:5px;padding:1px 7px}.action-policy-text{font-style:italic}.onb-agent-line{margin:-6px 0 6px;font-size:13.5px;color:var(--ink-soft);max-width:46rem}.onb-paste-chip{display:inline-flex;align-items:center;gap:8px;border:1px solid var(--hairline);border-radius:7px;background:var(--card);padding:2px 8px;cursor:pointer;font-size:12px;vertical-align:baseline;transition:border-color .13s ease}.onb-paste-chip:hover{border-color:var(--sage)}.onb-paste-chip code{font-family:var(--font-mono);font-size:11.5px;color:var(--pine-dark);background:none}.onb-paste-chip .pc-copy{font-size:10px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--ink-faint)}.onb-paste-chip:hover .pc-copy{color:var(--pine)}.onb-disclosure .d-footnote{margin-top:8px;font-size:11.5px;color:var(--ink-faint);line-height:1.55}.method-badge{font-family:var(--font-mono);font-size:10.5px;font-weight:500;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft);border:1px solid var(--line);border-radius:100px;padding:2px 9px;background:var(--paper-2, transparent);cursor:help}
|