kira-wallet 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.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +116 -0
  3. package/THIRD_PARTY_NOTICES.md +13 -0
  4. package/bin/kira.cjs +8 -0
  5. package/curve_pricing.py +86 -0
  6. package/docs/AGENT_TOOLS.md +53 -0
  7. package/docs/CLI.md +144 -0
  8. package/docs/JOB_CONTRACT.md +111 -0
  9. package/docs/KIRA_JOBS_BUILD_REPORT.md +92 -0
  10. package/docs/KIRA_REDESIGN_REPORT.md +125 -0
  11. package/docs/ONBOARDING_CHAT_CONTRACT.md +36 -0
  12. package/docs/RELEASE.md +45 -0
  13. package/docs/WALLET_CAPABILITIES.md +98 -0
  14. package/docs/character/brand-generation.json +20 -0
  15. package/docs/character/workspace-generation.json +29 -0
  16. package/docs/screenshots/kira-jobs-desktop.jpg +0 -0
  17. package/kira_agent.py +280 -0
  18. package/kira_cli.py +263 -0
  19. package/kira_config.py +98 -0
  20. package/kira_jobs.py +504 -0
  21. package/kira_tools.py +104 -0
  22. package/networks.json +214 -0
  23. package/onchain.cjs +34 -0
  24. package/package.json +78 -0
  25. package/pipeline-onchain.cjs +284 -0
  26. package/research.py +39 -0
  27. package/sources/job-envelope-v1.schema.json +469 -0
  28. package/sources/job-request-v1.schema.json +352 -0
  29. package/sources/mintclub-bond-abi.json +1457 -0
  30. package/sources/rpc-candidates.json +294 -0
  31. package/sources/uniswap-deployments.json +18261 -0
  32. package/viewer/README.md +119 -0
  33. package/viewer/chain_images.py +21 -0
  34. package/viewer/details.py +119 -0
  35. package/viewer/model.py +216 -0
  36. package/viewer/refresh-curve-prices.cjs +35 -0
  37. package/viewer/refresh_prices.py +97 -0
  38. package/viewer/server.py +153 -0
  39. package/viewer/static/agent.css +100 -0
  40. package/viewer/static/agent.js +203 -0
  41. package/viewer/static/app.js +252 -0
  42. package/viewer/static/favicon.svg +1 -0
  43. package/viewer/static/index.html +139 -0
  44. package/viewer/static/jobs.js +113 -0
  45. package/viewer/static/kira-attention.png +0 -0
  46. package/viewer/static/kira-explain.png +0 -0
  47. package/viewer/static/kira-logo.png +0 -0
  48. package/viewer/static/kira-research.png +0 -0
  49. package/viewer/static/kira-review.png +0 -0
  50. package/viewer/static/kira.png +0 -0
  51. package/viewer/static/style.css +51 -0
  52. package/viewer/static/watching-model.js +129 -0
  53. package/viewer/static/watching.js +167 -0
  54. package/viewer/static/workspace-model.js +43 -0
  55. package/viewer/static/workspace.css +463 -0
  56. package/viewer/static/workspace.js +102 -0
  57. package/viewer/token_images.py +137 -0
  58. package/wallet.py +389 -0
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+ (function (root) {
3
+ const pageSize = 50;
4
+ function catalog(tokens, filters = {}) {
5
+ const search = (filters.search || '').trim().toLowerCase();
6
+ const rows = tokens.filter(t => (filters.testnets || t.environment !== 'testnet')
7
+ && (!filters.network || filters.network === 'all' || String(t.chain_id) === String(filters.network))
8
+ && (filters.pricing !== 'priced' || (t.value_usd != null && !t.unpriced_count))
9
+ && (filters.pricing !== 'unpriced' || t.value_usd == null || t.unpriced_count > 0)
10
+ && (!search || [t.symbol, t.name, t.address || 'native'].some(v => String(v || '').toLowerCase().includes(search))));
11
+ const order = filters.sort || 'value';
12
+ rows.sort((a, b) => (order === 'name' ? String(a.name || a.symbol).localeCompare(String(b.name || b.symbol))
13
+ : order === 'wallets' ? b.wallet_count - a.wallet_count
14
+ : (b.value_usd ?? -Infinity) - (a.value_usd ?? -Infinity)) || String(a.id).localeCompare(String(b.id)));
15
+ const pages = Math.max(1, Math.ceil(rows.length / pageSize));
16
+ const page = Math.min(Math.max(1, filters.page || 1), pages);
17
+ return { rows: rows.slice((page - 1) * pageSize, page * pageSize), count: rows.length, page, pages };
18
+ }
19
+ const active = job => ['queued', 'running'].includes(job.state);
20
+ function jobPresentation(job, now = Date.now(), connected = true) {
21
+ const labels = { queued: 'Queued', running: 'Running', succeeded: 'Completed', partial: 'Saved with gaps', interrupted: 'Interrupted', failed: 'Failed', cancelled: 'Stopped' };
22
+ const start = job.state === 'queued' ? job.updated_at || job.created_at : job.started_at || job.created_at;
23
+ const end = active(job) ? now : Date.parse(job.updated_at);
24
+ const elapsed = Math.max(0, Math.floor((end - Date.parse(start)) / 1000));
25
+ const last = job.events?.at(-1)?.observed_at || job.updated_at;
26
+ const quiet = job.state === 'running' && now - Date.parse(last) > 60000;
27
+ return {
28
+ label: job.cancel_requested && active(job) ? 'Stopping' : labels[job.state] || 'Unknown',
29
+ active: active(job), spinner: connected && job.state === 'running', quiet,
30
+ elapsed: Number.isFinite(elapsed) ? elapsed : null,
31
+ timerLabel: job.state === 'queued' ? 'Waiting' : job.started_at ? 'Elapsed' : 'Since request',
32
+ freshness: !connected ? 'Connection lost. Last recorded state shown.' : quiet ? 'Awaiting a new stage update. Still marked running.' : '',
33
+ };
34
+ }
35
+ function duration(seconds) {
36
+ if (seconds == null) return 'Unknown';
37
+ const h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60), s = seconds % 60;
38
+ return (h ? h + 'h ' : '') + m + 'm ' + s + 's';
39
+ }
40
+ const api = { catalog, pageSize, active, jobPresentation, duration };
41
+ if (typeof module !== 'undefined' && module.exports) module.exports = api;
42
+ else root.KiraView = api;
43
+ })(typeof globalThis !== 'undefined' ? globalThis : this);
@@ -0,0 +1,463 @@
1
+ /* Workspace refinement: paper, ink and a lavender notebook.
2
+ Body and controls use the system UI face. Only scene captions use a serif. */
3
+ :root {
4
+ --bg: #f8f7f4;
5
+ --ink: #292733;
6
+ --secondary: #665e70;
7
+ --muted: #716779;
8
+ --line: #e4dfe8;
9
+ --accent: #69518b;
10
+ --accent-soft: #eee8f5;
11
+ --sidebar: #22232d;
12
+ --type-caption: 12px;
13
+ --type-body: 14px;
14
+ --type-label: 13px;
15
+ --control-height: 44px;
16
+ --icon-size: 18px;
17
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif;
18
+ }
19
+ body, main { background: var(--bg); color: var(--ink); }
20
+ body { font-size: var(--type-body); line-height: 1.5; letter-spacing: 0; }
21
+ p { letter-spacing: 0; }
22
+ button, input, textarea, select { font: inherit; letter-spacing: 0; }
23
+ button, a { touch-action: manipulation; }
24
+ :is(input, textarea, button, a, select, summary):focus-visible { outline: 2px solid #8064b1; outline-offset: 3px; }
25
+ h1, h2, h3 { letter-spacing: -.025em; }
26
+ .ui-icon { width: var(--icon-size); height: var(--icon-size); flex: none; display: block; stroke-width: 1.75; }
27
+ .primary-button, .quiet-button, .settings-button, .send-button, .icon-button, [data-close-dialog] {
28
+ display: inline-flex; align-items: center; justify-content: center; gap: 8px;
29
+ min-height: var(--control-height); border-radius: 10px; line-height: 20px;
30
+ font-size: var(--type-label); font-weight: 500; padding: 11px 14px;
31
+ }
32
+ .primary-button { background: var(--accent); border-color: var(--accent); color: #fff; }
33
+ .primary-button:hover { background: #594176; border-color: #594176; }
34
+ .quiet-button { background: #fcfafc; border-color: #d8cfdf; color: #594469; }
35
+ .quiet-button:hover { background: #eee8f5; border-color: #bbaeC9; }
36
+ button:disabled { opacity: .55; }
37
+ .icon-button, [data-close-dialog] { padding: 0; min-width: 44px; color: var(--secondary); }
38
+ .icon-button svg, [data-close-dialog] svg { width: 18px; height: 18px; }
39
+
40
+ /* Navigation. The icon and name occupy two stable columns. */
41
+ .sidebar { width: 232px; background: var(--sidebar); border: 0; color: #eeeaf4; padding: 28px 18px 20px; z-index: 3; }
42
+ .brand { padding: 0 6px; gap: 10px; font-size: 28px; font-weight: 600; letter-spacing: -.035em; line-height: 1.15; }
43
+ .brand-logo { width: 44px; height: 44px; border-radius: 12px; }
44
+ .brand-caption { color: #bdb6ca; font-size: 10px; font-weight: 500; letter-spacing: .07em; line-height: 1.4; margin-top: 4px; }
45
+ .brand[aria-current] { color: #fff; }
46
+ .workspace-label, .sidebar-label { color: #bdb6ca; font-size: 11px; letter-spacing: .07em; font-weight: 500; }
47
+ .workspace-label { margin: 32px 8px 16px; }
48
+ .sidebar-intro { display: flex; gap: 12px; margin: 0 8px 8px; }
49
+ .sidebar-intro p { font-size: 13px; color: #d0c8dc; line-height: 1.6; margin: 0; }
50
+ .sidebar-line { width: 2px; background: #a590ce; border-radius: 2px; }
51
+ .sidebar-label { margin: 30px 8px 12px; padding: 0; }
52
+ .sidebar-label span { background: #393644; color: #d4c9e5; font-size: 11px; }
53
+ #wallet-list { overflow: auto; min-height: 70px; max-height: calc(100dvh - 490px); scrollbar-width: thin; }
54
+ .wallet-button:not(.home-button) { grid-template-columns: 20px minmax(0, 1fr); padding: 12px 10px; gap: 10px; border-radius: 10px; min-height: 60px; }
55
+ .wallet-button { color: #e2dceb; }
56
+ .wallet-button:hover { background: #2f2d3b; }
57
+ .wallet-button.active { background: #40374f; border-color: #625075; color: #f4edff; }
58
+ .wallet-nav-name { font-size: 14px; font-weight: 500; line-height: 1.4; }
59
+ .wallet-nav-address { display: block; font-size: 12px; color: #b6aac6; line-height: 1.5; margin-top: 3px; }
60
+ .wallet-glyph { color: #b9a7d1; width: 20px; height: 20px; }
61
+ .wallet-glyph svg { width: 20px; height: 20px; }
62
+ .wallet-arrow { display: none; }
63
+ .user-controls { margin-top: auto; padding-top: 24px; display: grid; gap: 8px; }
64
+ .user-controls .primary-button { justify-content: flex-start; background: #bda9df; border-color: #bda9df; color: #2e253d; font-size: 13px; padding: 11px 12px; }
65
+ .user-controls .primary-button:hover { background: #cbb8e9; }
66
+ .settings-button { justify-content: flex-start; background: transparent; border: 1px solid #5b5366; color: #e0d7eb; font-size: 13px; padding: 11px 12px; }
67
+ .settings-button:hover { background: #332d40; }
68
+ .user-controls p { font-size: 11px; color: #bcb0cb; line-height: 1.6; text-align: left; margin: 4px 2px 16px; }
69
+ .sidebar-bottom { margin-top: 0; border-color: #4d445b; color: #c8bdd5; font-size: 11px; gap: 7px; flex-wrap: wrap; }
70
+ .read-only { color: #bcb0cb; font-size: 11px; }
71
+ .status-dot { background: #abcdb7; }
72
+
73
+ /* Page hierarchy and recorded financial values. */
74
+ main { margin-left: 232px; width: calc(100% - 232px); }
75
+ .topbar { height: 60px; background: #fbfaf7; border-color: var(--line); padding: 0 28px; gap: 12px; }
76
+ .breadcrumb { font-size: 13px; }
77
+ .breadcrumb a { min-height: 44px; display: flex; align-items: center; }
78
+ .breadcrumb [aria-current] { color: #51465d; }
79
+ .sync-status { font-size: 12px; color: var(--secondary); }
80
+ .sync-status .sync-dot { background: #806692; }
81
+ .demo-badge { font-size: 10px; letter-spacing: .06em; color: #69527b; }
82
+ .workspace-tabs { min-height: 56px; border-bottom: 1px solid var(--line); display: flex; align-items: stretch; gap: 28px; padding: 0 28px; background: #fbfaf7; }
83
+ .workspace-tabs > a { position: relative; display: flex; align-items: center; gap: 8px; color: var(--secondary); font-size: 14px; white-space: nowrap; min-height: 56px; }
84
+ .workspace-tabs > a[aria-current] { color: #302639; font-weight: 600; }
85
+ .workspace-tabs > a[aria-current]::after { content: ""; position: absolute; bottom: -1px; left: 0; right: 0; height: 2px; background: #806299; }
86
+ .workspace-tabs > a > span:not(:empty) { font-size: 11px; background: #eae2f1; color: #624c79; padding: 1px 6px; border-radius: 5px; line-height: 18px; }
87
+ .mobile-pane-control { display: none; }
88
+ .content { padding: 28px; max-width: 1800px; }
89
+ .workspace { display: grid; grid-template-columns: minmax(0, 1fr) minmax(340px, 38%); gap: 28px; align-items: start; }
90
+ .evidence-pane { min-width: 0; }
91
+ .home-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; padding: 4px 0; }
92
+ .home-heading h1 { font-size: clamp(32px, 2.7vw, 42px); font-weight: 600; line-height: 1.2; letter-spacing: -.035em; margin: 0 0 14px; }
93
+ .home-heading p { font-size: 14px; color: var(--secondary); line-height: 1.6; margin: 0; }
94
+ .eyebrow, .home-heading .eyebrow, .view-heading .eyebrow { font-size: 11px; line-height: 1.5; font-weight: 600; color: #705a80; letter-spacing: .075em; margin-bottom: 12px; }
95
+ .edition-mark { font: italic 28px Georgia, serif; color: #9a83aa; margin-bottom: 8px; white-space: nowrap; }
96
+ .home-summary { display: block; margin-top: 28px; padding: 24px; border: 1px solid #dfd6e8; border-radius: 16px; background: #efeaf5; }
97
+ .total-value, .wallet-card-value, .account-value { font-variant-numeric: tabular-nums; letter-spacing: -.035em; }
98
+ .home-summary .total-value { font-size: clamp(36px, 3.2vw, 48px); color: #372b43; font-weight: 500; line-height: 1.15; margin-top: 10px; }
99
+ .home-summary .valuation-note, .valuation-note { color: var(--secondary); font-size: 12px; line-height: 1.6; margin-top: 12px; max-width: none; }
100
+ .home-stats { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin-top: 22px; padding-top: 20px; border-top: 1px solid #dcd1e6; }
101
+ .home-stats .summary-stat { padding-left: 0; border: none; gap: 6px; }
102
+ .summary-label, .home-stats .summary-label { font-size: 12px; color: #685571; line-height: 1.4; }
103
+ .stat-note, .home-stats .stat-note { font-size: 12px; color: var(--secondary); line-height: 1.5; white-space: normal; }
104
+ .home-stats strong { font-size: 26px; font-weight: 500; color: #463550; letter-spacing: -.025em; }
105
+ .dashboard-grid { grid-template-columns: 1fr; gap: 28px; margin-top: 28px; }
106
+ .section-heading { margin-bottom: 14px; gap: 12px; align-items: baseline; flex-wrap: wrap; }
107
+ .section-heading h2 { font-size: 18px; font-weight: 600; line-height: 1.4; }
108
+ .section-note { font-size: 12px; color: var(--secondary); line-height: 1.5; white-space: normal; }
109
+ .wallet-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
110
+ .wallet-card { padding: 18px; background: #fcfbfa; border-color: #e3dce7; border-radius: 12px; min-width: 0; }
111
+ .wallet-card-heading { gap: 10px; align-items: center; }
112
+ .wallet-card-identity { min-width: 0; overflow: hidden; }
113
+ .wallet-card-identity strong { font-size: 14px; font-weight: 500; line-height: 1.4; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
114
+ .wallet-card-identity > span { font-size: 12px; color: var(--secondary); line-height: 1.5; }
115
+ .wallet-card-value { font-size: 28px; font-weight: 500; line-height: 1.2; margin: 20px 0 10px; }
116
+ .wallet-card-meta { font-size: 12px; color: var(--secondary); line-height: 1.6; display: block; }
117
+ .wallet-card-meta > span:last-child { display: none; }
118
+ .card-wallet-icon { width: 36px; height: 36px; border-radius: 10px; flex: none; background: #eee7f5; }
119
+ .card-wallet-icon svg { width: 20px; height: 20px; }
120
+ .card-arrow { font-size: 18px; line-height: 1; color: #796188; }
121
+ .card-arrow, .detail-arrow { display: inline-flex; align-items: center; flex: none; }
122
+ .card-arrow .ui-icon, .detail-arrow .ui-icon { width: 16px; height: 16px; }
123
+ .allocation-progress { height: 4px; }
124
+ .network-allocation { padding: 4px 18px; border-color: #e3dce7; border-radius: 12px; }
125
+ .network-row { padding: 18px 0; }
126
+ .network-label, .network-value { font-size: 14px; line-height: 1.4; }
127
+ .network-row-meta { font-size: 12px; color: var(--secondary); margin: 9px 0; }
128
+ .largest-holdings { margin-top: 28px; }
129
+ .home-holdings { display: block; border-radius: 12px; border-color: #e3dce7; padding: 0 18px; }
130
+ .home-holding { padding: 16px 0; border: 0; border-bottom: 1px solid #e7e0eb; background: transparent; }
131
+ .home-holding:last-child { border: 0; }
132
+ .home-holding:hover { background: transparent; }
133
+ .holding-value { font-size: 14px; }
134
+ .coin-icon { width: 32px; height: 32px; color: #66556e; background: #f0edf4; }
135
+ .chain-icon { color: #fff; background: #796489; }
136
+ .chain-fallback { font-size: 11px; }
137
+ .symbol { font-size: 14px; font-weight: 500; line-height: 1.4; color: #45354f; }
138
+ .token-description { font-size: 12px; line-height: 1.5; color: var(--secondary); }
139
+ .home-holding .token-description { font-size: 12px; color: var(--secondary); }
140
+ .empty p { font-size: 14px; color: var(--secondary); }
141
+ .page-footer { font-size: 12px; color: var(--secondary); flex-wrap: wrap; line-height: 1.6; margin-top: 24px; gap: 8px; }
142
+
143
+ /* Kira keeps a visible composer while her recorded notes scroll. */
144
+ .kira-panel { position: sticky; top: 24px; height: calc(100dvh - 168px); min-height: 500px; max-height: calc(100dvh - 48px); display: flex; flex-direction: column; background: #f0edf5; border: 1px solid #dfd6e8; border-radius: 16px; overflow: hidden; box-shadow: 0 6px 24px #3a294708; }
145
+ .kira-panel-heading { display: flex; justify-content: space-between; align-items: center; padding: 22px 22px 0; gap: 12px; flex: none; }
146
+ .kira-panel-heading .eyebrow { font-size: 10px; margin-bottom: 6px; color: #725781; letter-spacing: .065em; }
147
+ .kira-panel-heading h2 { font-size: 27px; font-weight: 600; color: #443050; gap: 9px; line-height: 1.2; }
148
+ .kira-spark { color: #9076a4; font-size: 20px; font-weight: 400; }
149
+ .local-badge { padding: 5px 8px; border: 1px solid #d6c8e2; border-radius: 7px; font-size: 11px; line-height: 1.5; color: #695076; white-space: nowrap; }
150
+ .kira-scene { height: 205px; position: relative; flex: none; overflow: hidden; margin: 8px 0 0; background: radial-gradient(ellipse at 78% 75%, #e1d5ea, transparent 67%); }
151
+ .kira-scene img { position: absolute; right: 4px; bottom: -17px; width: 180px; height: 228px; object-fit: contain; object-position: center bottom; }
152
+ .scene-caption { position: relative; z-index: 1; max-width: 47%; padding: 46px 0 0 22px; display: flex; flex-direction: column; gap: 12px; }
153
+ .scene-caption > span:first-child { font: italic 25px/1.2 Georgia, serif; color: #70537f; letter-spacing: -.02em; }
154
+ .scene-rule { width: 30px; height: 1px; background: #b3a0c4; }
155
+ .scene-caption small { font-size: 12px; line-height: 1.5; color: #705a7e; max-width: 145px; }
156
+ .kira-conversation { padding: 18px 22px 20px; overflow: auto; flex: 1; min-height: 80px; border-top: 1px solid #dfd5e8; scrollbar-width: thin; }
157
+ .briefing-meta { display: flex; align-items: center; gap: 7px; color: #6e537d; font-size: 12px; line-height: 1.5; margin-bottom: 14px; flex-wrap: wrap; }
158
+ .briefing-meta > span:last-child { margin-left: auto; font-size: 10px; letter-spacing: .035em; color: #71617d; }
159
+ .briefing-dot { height: 5px; width: 5px; border-radius: 50%; background: #8d72a2; flex: none; }
160
+ .kira-note { display: block; background: #fbfafc; border-color: #dfd5e8; border-radius: 12px; padding: 18px; margin: 0; color: #5f4b6e; }
161
+ .kira-note-label { font-size: 10px; letter-spacing: .065em; line-height: 1.5; color: #775b85; margin-bottom: 10px; }
162
+ .kira-note p { font-size: 14px; line-height: 1.65; letter-spacing: 0; color: #5e4b6a; }
163
+ .kira-note summary { font-size: 12px; line-height: 1.5; margin-top: 14px; color: #735481; padding: 6px 0; }
164
+ .kira-note details p { font-size: 13px; line-height: 1.65; color: #685374; }
165
+ .kira-quick-actions { display: grid; gap: 0; margin-top: 12px; }
166
+ .kira-quick-actions button { min-height: 44px; padding: 11px 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; background: none; border: 0; border-bottom: 1px solid #ddd2e7; color: #6d507d; font-size: 13px; line-height: 20px; text-align: left; }
167
+ .kira-quick-actions button:hover { color: #4c2e60; }
168
+ .kira-quick-actions button:last-child { border: 0; }
169
+ .kira-quick-actions .ui-icon { width: 16px; height: 16px; }
170
+ .composer { background: #f8f5fb; border-top: 1px solid #dfd2e8; padding: 16px 22px; flex: none; }
171
+ .composer > label { font-size: 12px; color: #71547f; font-weight: 500; }
172
+ .composer textarea { width: 100%; resize: vertical; min-height: 56px; max-height: 100px; background: none; border: 0; display: block; padding: 10px 0 4px; color: #493254; font-size: 14px; line-height: 1.5; }
173
+ .composer textarea::placeholder { color: #7c6889; opacity: 1; }
174
+ .composer-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
175
+ .composer-footer > span { font-size: 12px; line-height: 1.5; color: #766081; }
176
+ .send-button { border: 0; background: #e2d8ed; color: #745981; width: 44px; height: 44px; padding: 0; opacity: .65; }
177
+ .send-button .ui-icon { width: 20px; height: 20px; }
178
+ .composer > p { font-size: 12px; color: #765c83; margin: 8px 0 0; line-height: 1.5; }
179
+ .text-button { background: none; border: 0; padding: 6px 2px; color: #654378; font-size: inherit; line-height: 1.5; text-decoration: underline; text-underline-offset: 3px; }
180
+ .view-heading h1 { font-size: 36px; font-weight: 600; line-height: 1.2; letter-spacing: -.035em; }
181
+ .view-heading > p { font-size: 14px; line-height: 1.65; color: var(--secondary); max-width: 560px; margin-bottom: 24px; }
182
+
183
+ /* Wallets, token details and filters share the same type scale. */
184
+ .wallet-heading { flex-wrap: wrap; }
185
+ .wallet-heading h1 { font-size: 32px; line-height: 1.2; overflow-wrap: anywhere; }
186
+ .wallet-address { flex-wrap: wrap; font-size: 12px; overflow-wrap: anywhere; color: var(--secondary); }
187
+ #address { max-width: 100%; overflow-wrap: anywhere; }
188
+ .wallet-actions { background: #eeebf2; padding: 6px; border: 1px solid #dfd6e7; border-radius: 12px; justify-content: flex-start; gap: 6px; margin-bottom: 24px; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
189
+ .wallet-actions button { background: #fcfafc; border: 0; white-space: normal; min-height: 44px; font-size: 13px; padding: 10px 8px; }
190
+ .portfolio-summary { grid-template-columns: 1fr 1fr; gap: 20px; margin-top: 24px; padding-bottom: 24px; }
191
+ .portfolio-summary .valuation { grid-column: 1 / -1; }
192
+ .portfolio-summary .total-value { font-size: 42px; }
193
+ .portfolio-summary .summary-stat { border: 0; padding-left: 0; }
194
+ .summary-stat strong { font-size: 26px; }
195
+ .holdings-toolbar { flex-wrap: wrap; gap: 16px; margin-bottom: 20px; }
196
+ .holdings-toolbar .filters { flex-wrap: wrap; width: 100%; gap: 8px; }
197
+ .holdings h2 { font-size: 18px; }
198
+ .token-search { flex: 1; }
199
+ .token-search input { width: 100%; max-width: none; font-size: 14px; min-height: 44px; }
200
+ .chain-select select { font-size: 13px; min-height: 44px; }
201
+ .chain-select svg { top: 16px; }
202
+ .segment button { min-height: 38px; font-size: 13px; }
203
+ .chain-heading { gap: 12px; }
204
+ .chain-name { flex-wrap: wrap; font-size: 15px; }
205
+ .chain-assets, .chain-total, .filter-note, .tag, .count-badge { font-size: 12px; color: var(--secondary); }
206
+ .detail-heading h1 { font-size: 32px; line-height: 1.2; }
207
+ .detail-heading p { font-size: 14px; color: var(--secondary); line-height: 1.6; }
208
+ .detail-facts, .account-grid { grid-template-columns: 1fr; gap: 14px; }
209
+ .fact-panel, .account-card { padding: 22px; border-radius: 12px; }
210
+ .fact-panel h2 { font-size: 17px; }
211
+ .fact-panel dt, .account-facts dt, .account-meta, .account-footnote { color: var(--secondary); font-size: 12px; line-height: 1.5; }
212
+ .fact-panel dd, .account-facts dd { font-size: 13px; color: #5e4b6a; line-height: 1.5; }
213
+ .detail-note { font-size: 12px; line-height: 1.6; color: var(--secondary); }
214
+ .account-value { font-size: 32px; }
215
+ .table-wrap { border-radius: 12px; }
216
+ .token-table td, .token-table th { padding: 16px 12px; }
217
+ .token-table td { font-size: 14px; }
218
+ .amount, .token-price, .token-value { font-size: 14px; line-height: 1.5; }
219
+ .token-table th { font-size: 12px; color: var(--secondary); }
220
+ .coin-title { min-width: 0; overflow: hidden; }
221
+ .token-description, .symbol { max-width: 180px; }
222
+ .market-link { font-size: 12px; display: inline-flex; align-items: center; min-height: 32px; }
223
+ .price-basis, .native-label, .unpriced { font-size: 12px; line-height: 1.5; color: var(--secondary); }
224
+ .token-catalog-controls { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
225
+ .catalog-search { grid-column: 1 / -1; }
226
+ .token-catalog-controls input, .token-catalog-controls select, .activity-toolbar select { width: 100%; min-width: 0; border: 1px solid #d5cadd; background: #fdfbfe; color: #604a6c; border-radius: 10px; padding: 11px 12px; font-size: 13px; line-height: 20px; min-height: 44px; }
227
+ .catalog-meta { font-size: 12px; display: flex; justify-content: space-between; gap: 12px; align-items: center; margin: 20px 0 16px; color: var(--secondary); flex-wrap: wrap; }
228
+ .checkbox-label { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--secondary); min-height: 36px; }
229
+ .catalog-table { table-layout: fixed; }
230
+ .catalog-table th:first-child { width: 43%; }
231
+ .catalog-table th:nth-child(2) { width: 25%; }
232
+ .catalog-table th:nth-child(3) { width: 10%; }
233
+ .catalog-table th:nth-child(4) { width: 22%; text-align: right; }
234
+ .catalog-table td:last-child { text-align: right; }
235
+ .pagination { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 18px; color: var(--secondary); font-size: 12px; }
236
+ .pagination button { min-height: 44px; padding: 11px 12px; font-size: 13px; }
237
+
238
+ /* Job states remain evidence, with readable timing and stage details. */
239
+ .active-work { border: 1px solid #d1c2e0; background: #ece5f5; border-radius: 12px; padding: 14px 16px; margin-bottom: 24px; color: #665073; display: grid; gap: 8px; }
240
+ .active-work a { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 13px; flex-wrap: wrap; }
241
+ .active-work strong { font-weight: 500; color: #5d4472; }
242
+ .active-work p { margin: 6px 0 0; font-size: 12px; line-height: 1.6; }
243
+ .active-work small { font-size: 12px; }
244
+ .active-work .spinner { margin-right: 7px; }
245
+ .research-jobs { padding: 20px; border-radius: 12px; background: #fcfafc; margin-bottom: 0; }
246
+ .activity-toolbar { display: flex; align-items: center; gap: 12px; margin: 18px 0; flex-wrap: wrap; }
247
+ .activity-toolbar select { width: 155px; }
248
+ .activity-toolbar > span { font-size: 12px; color: var(--secondary); line-height: 1.6; }
249
+ .job-row { align-items: flex-start; padding: 20px 0; gap: 12px; flex-wrap: wrap; }
250
+ .job-identity { font-size: 14px; flex: 1; min-width: min(100%, 200px); }
251
+ .job-identity p { font-size: 13px; line-height: 1.6; }
252
+ .job-identity strong { font-size: 14px; font-weight: 500; }
253
+ .job-state { font-size: 12px; border-radius: 6px; padding: 4px 8px; line-height: 20px; white-space: nowrap; }
254
+ .job-state.running, .job-state.queued { background: #ece4f5; color: #705087; }
255
+ .job-state.cancelled { background: #eeebf0; color: #6d5c7a; }
256
+ .job-state.failed { background: #f7e9e5; color: #93513f; }
257
+ .job-state.interrupted, .job-state.partial { background: #f8eddb; color: #806029; }
258
+ .job-state.succeeded { background: #e9f0eb; color: #446950; }
259
+ .job-actions { margin-left: auto; }
260
+ .job-actions .quiet-button { font-size: 13px; min-height: 44px; padding: 11px 12px; }
261
+ .job-identity small { font-size: 12px; line-height: 1.7; color: var(--secondary); }
262
+ .job-identity a { font-size: 13px; line-height: 1.5; }
263
+ .job-identity .job-freshness { color: #87662e; }
264
+ .job-stages { font-size: 13px; }
265
+ .job-stages summary { font-size: 13px; color: #755385; padding: 6px 0; }
266
+ .job-stages li span { font-size: 12px; line-height: 1.6; }
267
+ .job-timer { font-variant-numeric: tabular-nums; }
268
+ .spinner { display: inline-block; width: 12px; height: 12px; border: 1.5px solid #c7b5d9; border-top-color: #785295; border-radius: 50%; vertical-align: -1px; margin-right: 5px; animation: research-spin 1s linear infinite; }
269
+ @keyframes research-spin { to { transform: rotate(360deg); } }
270
+ .research-dialog { padding: 28px; }
271
+ .dialog-heading h2 { font-size: 21px; letter-spacing: -.025em; }
272
+ .dialog-heading button { min-width: 44px; min-height: 44px; flex: none; }
273
+ .research-dialog p, .research-dialog label { font-size: 14px; line-height: 1.6; }
274
+ .research-dialog .form-note, .assistant-gate p { font-size: 12px; line-height: 1.6; }
275
+ .research-dialog input:not([type=checkbox]), .research-dialog textarea, .research-dialog select { font-size: 14px; min-height: 44px; }
276
+ .history-row { font-size: 14px; line-height: 1.6; }
277
+ .history-row small, .comparison-result small { font-size: 12px; }
278
+
279
+ /* Watching connection keeps selection, session and saved addresses distinct. */
280
+ .watching-dialog { width: min(520px, calc(100% - 32px)); }
281
+ .watching-dialog [hidden], #wallet-session-panel[hidden] { display: none; }
282
+ .watching-dialog .text-button { min-height: 44px; padding: 10px 2px; margin-right: 12px; }
283
+ .wallet-entry-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 4px; background: #eee9f3; padding: 4px; border-radius: 12px; margin: 22px 0; }
284
+ .wallet-entry-modes button { min-height: 44px; padding: 9px 8px; border: 0; background: transparent; color: var(--secondary); border-radius: 9px; font: inherit; font-weight: 500; cursor: pointer; }
285
+ .wallet-entry-modes button[aria-pressed=true] { color: var(--ink); background: #fff; box-shadow: 0 1px 4px #30213e12; }
286
+ .watching-dialog.is-review .wallet-entry-modes { margin: 16px 0; }
287
+ .watching-dialog.is-review .wallet-preview { padding: 16px; }
288
+ .watching-dialog h3 { font-size: 14px; font-weight: 600; letter-spacing: -.01em; margin: 0 0 8px; }
289
+ .wallet-choices { display: grid; gap: 8px; margin: 16px 0; }
290
+ .wallet-choice { display: flex; align-items: center; gap: 12px; padding: 14px; width: 100%; min-height: 64px; text-align: left; color: var(--ink); background: #fff; border: 1px solid #ded6e6; border-radius: 12px; font: inherit; cursor: pointer; }
291
+ .wallet-choice:hover, .wallet-choice[aria-pressed=true] { border-color: #9475aa; background: #f6f1fa; }
292
+ .wallet-choice img { width: 32px; height: 32px; border-radius: 8px; object-fit: contain; flex: none; }
293
+ .wallet-choice > span:not(.wallet-choice-arrow) { flex: 1; min-width: 0; }
294
+ .wallet-choice strong, .wallet-choice small { display: block; overflow-wrap: anywhere; }
295
+ .wallet-choice strong { font-size: 14px; font-weight: 500; line-height: 1.45; }
296
+ .wallet-choice small { color: var(--secondary); font-size: 12px; line-height: 1.5; margin-top: 3px; }
297
+ .wallet-choice-arrow { display: flex; flex: none; color: #76578e; }
298
+ .wallet-account img { display: none; }
299
+ .wallet-account small { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.7; }
300
+ .wallet-accounts-section { border-top: 1px solid #e2dce8; padding-top: 20px; margin-top: 20px; }
301
+ .research-dialog .wallet-connection-status { font-size: 13px; color: #5f4474; }
302
+ .wallet-connection-status:empty { display: none; }
303
+ .wallet-preview { background: #f0edf5; border: 1px solid #ddd2e6; border-radius: 12px; padding: 18px; margin: 18px 0; }
304
+ .wallet-preview dl { margin: 12px 0 0; }
305
+ .wallet-preview dt { color: var(--secondary); font-size: 12px; line-height: 1.5; margin-top: 12px; }
306
+ .wallet-preview dd { margin: 4px 0 0; font-size: 14px; line-height: 1.6; overflow-wrap: anywhere; white-space: pre-wrap; }
307
+ #wallet-preview-address, #wallet-submission-address { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
308
+ .wallet-submission-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
309
+ .wallet-submission-actions .quiet-button { min-height: 44px; padding: 10px 12px; }
310
+ .wallet-preview p { margin-bottom: 0; }
311
+ .wallet-demo-note, .wallet-unavailable { padding: 14px 16px; background: #f2edf6; border-radius: 10px; }
312
+ .wallet-existing a { color: #684484; text-underline-offset: 3px; }
313
+ #wallet-register { width: 100%; }
314
+ .wallet-session-panel { border-top: 1px solid #4b4058; padding-top: 8px; margin-bottom: 14px; }
315
+ #wallet-session { justify-content: flex-start; text-align: left; color: #d8cce4; font-size: 12px; min-height: 44px; }
316
+ #wallet-session-name { display: block; font-size: 12px; color: #c2b5cc; overflow-wrap: anywhere; }
317
+ #wallet-session-name:empty { display: none; }
318
+
319
+ /* Compact panes use the same card layout on every viewport. */
320
+ @media (max-width: 1400px) {
321
+ .wallet-cards { grid-template-columns: 1fr; }
322
+ .edition-mark { display: none; }
323
+ .token-catalog-controls { grid-template-columns: repeat(2, minmax(0, 1fr)); }
324
+ .token-catalog-controls > label:last-child { grid-column: 1 / -1; }
325
+ .evidence-pane .token-table, .evidence-pane .token-table tbody { display: block; }
326
+ .evidence-pane .token-table thead, .evidence-pane .token-table colgroup { display: none; }
327
+ .evidence-pane .token-table tr { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 12px; padding: 16px; border-bottom: 1px solid var(--line); }
328
+ .evidence-pane .token-table tr:last-child { border-bottom: 0; }
329
+ .evidence-pane .token-table td { display: block; padding: 0; border: 0; text-align: right; }
330
+ .evidence-pane .token-table td:first-child { grid-column: 1; grid-row: 1; text-align: left; }
331
+ .evidence-pane .token-table td:nth-child(4) { grid-column: 2; grid-row: 1; align-self: center; }
332
+ .evidence-pane .token-table td:nth-child(2) { grid-column: 1; grid-row: 2; text-align: left; margin-left: 42px; }
333
+ .evidence-pane .token-table td:nth-child(3) { grid-column: 2; grid-row: 2; }
334
+ .evidence-pane .token-table td:nth-child(5) { grid-column: 1 / -1; grid-row: 3; text-align: left; margin-left: 42px; }
335
+ .evidence-pane .mobile-label { display: block; font-size: 11px; color: var(--secondary); margin-bottom: 3px; }
336
+ .evidence-pane .symbol, .evidence-pane .token-description { max-width: 100%; }
337
+ }
338
+ @media (min-width: 1550px) {
339
+ .workspace { grid-template-columns: minmax(0, 1fr) minmax(390px, 36%); gap: 36px; }
340
+ .content { padding: 36px 40px; }
341
+ .topbar, .workspace-tabs { padding-left: 40px; padding-right: 40px; }
342
+ .dashboard-grid { grid-template-columns: 1.12fr 1fr; gap: 24px; }
343
+ .wallet-cards { grid-template-columns: 1fr; }
344
+ .kira-scene { height: 260px; }
345
+ .kira-scene img { height: 280px; width: 220px; right: 16px; }
346
+ .scene-caption { padding-top: 70px; }
347
+ .scene-caption > span:first-child { font-size: 30px; }
348
+ }
349
+ @media (min-width: 981px) and (max-width: 1150px) {
350
+ .sidebar { width: 216px; padding-left: 14px; padding-right: 14px; }
351
+ main { margin-left: 216px; width: calc(100% - 216px); }
352
+ .content { padding: 24px; }
353
+ .workspace { gap: 20px; grid-template-columns: minmax(0, 1fr) 320px; }
354
+ .topbar, .workspace-tabs { padding-left: 24px; padding-right: 24px; }
355
+ .home-summary { padding: 20px; }
356
+ .home-summary .total-value { font-size: 36px; }
357
+ .home-stats { gap: 8px; }
358
+ .kira-panel-heading, .kira-conversation, .composer { padding-left: 18px; padding-right: 18px; }
359
+ .kira-panel-heading .eyebrow { max-width: 150px; }
360
+ .scene-caption { padding-left: 18px; }
361
+ .scene-caption > span:first-child { font-size: 23px; }
362
+ }
363
+ @media (min-width: 981px) and (max-height: 800px) {
364
+ .kira-panel { min-height: 0; }
365
+ .kira-scene { height: 150px; }
366
+ .kira-scene img { height: 178px; width: 150px; bottom: -18px; }
367
+ .scene-caption { padding-top: 26px; gap: 8px; }
368
+ .scene-caption > span:first-child { font-size: 23px; }
369
+ .kira-panel-heading { padding-top: 16px; }
370
+ .kira-panel-heading h2 { font-size: 25px; }
371
+ .composer { padding-top: 12px; padding-bottom: 12px; }
372
+ .kira-conversation { padding-top: 14px; }
373
+ }
374
+ /* Let short desktop windows scroll the complete panel, including its composer. */
375
+ @media (min-width: 981px) and (max-height: 650px) {
376
+ .kira-panel { position: static; height: auto; max-height: none; }
377
+ .kira-conversation { flex: none; overflow: visible; }
378
+ }
379
+ @media (max-width: 980px) {
380
+ .sidebar { position: static; width: 100%; padding: 16px 20px 12px; display: flex; flex-direction: row; flex-wrap: wrap; align-items: center; gap: 12px; }
381
+ .brand { padding: 0; font-size: 25px; }
382
+ .brand-logo { width: 40px; height: 40px; }
383
+ .brand-caption { font-size: 10px; }
384
+ .workspace-label, .sidebar-intro, .sidebar-label, .sidebar-bottom { display: none; }
385
+ #wallet-list { width: 100%; order: 3; display: flex; overflow: auto; max-height: none; min-height: 0; gap: 8px; margin: 0; }
386
+ .wallet-button:not(.home-button) { width: auto; min-width: 164px; padding: 9px 12px; min-height: 56px; flex-shrink: 0; border-color: #51445e; }
387
+ .user-controls { display: flex; gap: 8px; margin-left: auto; margin-top: 0; padding: 0; }
388
+ .user-controls .primary-button { min-height: 44px; padding: 11px 12px; white-space: nowrap; }
389
+ .settings-button { min-height: 44px; min-width: 44px; width: 44px; padding: 0; justify-content: center; }
390
+ .settings-label, .user-controls p { display: none; }
391
+ .wallet-session-panel { order: 4; width: 100%; display: flex; align-items: center; gap: 12px; margin: 0; padding: 0; }
392
+ #wallet-session { flex: none; }
393
+ #wallet-session-name { min-width: 0; }
394
+ .app { display: block; }
395
+ main { width: 100%; margin: 0; }
396
+ .topbar { height: 44px; padding: 0 20px; }
397
+ .sync-status { display: none; }
398
+ .demo-badge { font-size: 10px; margin: 0; }
399
+ .workspace-tabs { height: auto; min-height: 52px; padding: 0 20px; gap: 24px; flex-wrap: wrap; }
400
+ .workspace-tabs > a { min-height: 52px; }
401
+ .mobile-pane-control { display: flex; width: 100%; border-top: 1px solid var(--line); gap: 6px; padding: 10px 0; }
402
+ .mobile-pane-control button { font-size: 14px; min-height: 44px; border: 1px solid transparent; border-radius: 9px; padding: 10px 16px; background: none; color: #776384; flex: 1; font-weight: 500; }
403
+ .mobile-pane-control button[aria-pressed=true] { border-color: #d5c4e2; background: #eee6f5; color: #66427b; }
404
+ .content { padding: 26px 20px; }
405
+ .workspace { display: block; }
406
+ .kira-panel { display: none; position: static; height: max(600px, calc(100dvh - 370px)); min-height: 0; max-height: none; }
407
+ .workspace[data-pane=kira] .evidence-pane { display: none; }
408
+ .workspace[data-pane=kira] .kira-panel { display: flex; }
409
+ .home-heading h1 { font-size: 34px; }
410
+ .home-summary { padding: 24px; margin-top: 24px; }
411
+ .home-summary .total-value { font-size: 42px; }
412
+ .wallet-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); }
413
+ .edition-mark { display: block; }
414
+ .kira-scene { height: 170px; }
415
+ .kira-scene img { width: 170px; height: 195px; right: 12px; bottom: -15px; }
416
+ .scene-caption { padding-top: 30px; max-width: 46%; }
417
+ .scene-caption > span:first-child { font-size: 25px; }
418
+ .job-row { flex-direction: column; }
419
+ .job-actions { align-self: flex-end; }
420
+ .job-identity a { display: inline; margin-left: 8px; }
421
+ .research-jobs { padding: 18px; }
422
+ .research-dialog { padding: 22px; }
423
+ .research-dialog input:not([type=checkbox]), .research-dialog textarea, .research-dialog select,
424
+ .composer textarea, .token-search input, .token-catalog-controls input { font-size: 16px; }
425
+ }
426
+ @media (max-width: 520px) {
427
+ .brand-caption { display: none; }
428
+ .watching-label { display: none; }
429
+ .wallet-cards { grid-template-columns: 1fr; }
430
+ .edition-mark { display: none; }
431
+ .home-heading h1, .view-heading h1 { font-size: 32px; }
432
+ .home-summary { padding: 22px 20px; }
433
+ .home-stats { gap: 10px; }
434
+ .home-stats strong { font-size: 24px; }
435
+ .token-catalog-controls { grid-template-columns: 1fr; }
436
+ .token-catalog-controls > label { grid-column: 1 / -1; }
437
+ .kira-panel-heading, .kira-conversation, .composer { padding-left: 18px; padding-right: 18px; }
438
+ .scene-caption { padding-left: 18px; }
439
+ .kira-panel-heading .eyebrow { max-width: 150px; }
440
+ .kira-scene img { right: 4px; width: 160px; }
441
+ .wallet-heading h1, .detail-heading h1 { font-size: 30px; }
442
+ }
443
+ @media (max-width: 360px) {
444
+ .sidebar { padding: 14px 16px 12px; gap: 8px; }
445
+ .brand { font-size: 23px; gap: 7px; }
446
+ .brand-logo { width: 32px; height: 32px; }
447
+ .user-controls .primary-button { padding: 11px 9px; font-size: 12px; gap: 6px; }
448
+ .user-controls .primary-button .ui-icon { width: 16px; height: 16px; }
449
+ .topbar, .workspace-tabs { padding-left: 16px; padding-right: 16px; }
450
+ .workspace-tabs { gap: 20px; }
451
+ .workspace-tabs > a { font-size: 13px; gap: 5px; }
452
+ .content { padding: 24px 16px; }
453
+ .home-heading h1 { font-size: 29px; }
454
+ .home-summary .total-value { font-size: 36px; }
455
+ .home-stats { gap: 8px; }
456
+ .home-summary { padding: 20px 16px; }
457
+ .kira-panel-heading { gap: 8px; }
458
+ .kira-panel-heading .eyebrow { max-width: 138px; }
459
+ .local-badge { font-size: 10px; padding: 4px 6px; }
460
+ .scene-caption > span:first-child { font-size: 23px; }
461
+ .scene-caption small { max-width: 112px; }
462
+ }
463
+ @media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+ let catalogPage = 1;
3
+ let briefingMode = 'view';
4
+ let catalogTimer;
5
+ function catalogFilters() {
6
+ return { search: $('all-token-search').value, network: $('all-token-network').value,
7
+ pricing: $('all-token-pricing').value, sort: $('all-token-sort').value,
8
+ testnets: $('all-token-testnets').checked, page: catalogPage };
9
+ }
10
+ function renderCatalog() {
11
+ if (!state) return;
12
+ const result = KiraView.catalog(state.details.tokens, catalogFilters());
13
+ catalogPage = result.page;
14
+ $('all-token-result').textContent = result.count + (result.count === 1 ? ' token · ' : ' tokens · ') + (result.count ? ((result.page - 1) * 50 + 1) + '–' + Math.min(result.page * 50, result.count) + ' shown' : 'none shown');
15
+ $('tokens-page').textContent = result.page + ' / ' + result.pages;
16
+ $('tokens-prev').disabled = result.page === 1;
17
+ $('tokens-next').disabled = result.page === result.pages;
18
+ $('all-token-empty').hidden = result.count > 0;
19
+ $('all-token-body').innerHTML = result.rows.map(a => {
20
+ const network = state.details.networks.find(n => n.id === a.chain_id);
21
+ return `<tr><td><a class="token-name token-link" href="${tokenHref(a.id)}">${coinIcon(a)}<div class="coin-title"><div class="symbol">${escapeHTML(a.symbol)}</div><div class="token-description">${escapeHTML(network?.name || 'Chain ' + a.chain_id)}${a.environment === 'testnet' ? ' · Testnet' : ''}</div></div></a></td><td><span class="mobile-label">Total balance</span><span class="amount" title="${escapeHTML(a.balance)}">${quantity(a.balance)}</span></td><td><span class="mobile-label">Wallets</span>${a.wallet_count}</td><td><span class="token-value">${a.environment === 'testnet' ? 'Testnet' : money(a.value_usd)}</span>${a.unpriced_count ? '<div class="price-basis">' + a.unpriced_count + ' unpriced</div>' : ''}</td></tr>`;
22
+ }).join('');
23
+ bindImages();
24
+ }
25
+ function renderWorkspace() {
26
+ if (!state) return;
27
+ $('tokens-view').hidden = selectedView !== 'tokens';
28
+ $('activity-view').hidden = selectedView !== 'activity';
29
+ document.querySelectorAll('[data-view]').forEach(link => {
30
+ const current = link.dataset.view === selectedView || (link.dataset.view === 'home' && ['wallet', 'token', 'network'].includes(selectedView));
31
+ if (current) link.setAttribute('aria-current', 'page'); else link.removeAttribute('aria-current');
32
+ });
33
+ if (selectedView === 'home') $('brand-home').setAttribute('aria-current', 'page'); else $('brand-home').removeAttribute('aria-current');
34
+ $('all-token-count').textContent = state.details.tokens.filter(t => t.environment !== 'testnet').length;
35
+ if (selectedView === 'tokens') {
36
+ renderBreadcrumb([{ label: 'Home', href: '#/home' }, { label: 'All tokens' }]);
37
+ document.title = 'All tokens · Kira Wallet';
38
+ const network = $('all-token-network').value;
39
+ const recordedChains = new Set(state.details.tokens.map(t => t.chain_id));
40
+ $('all-token-network').innerHTML = '<option value="all">All networks</option>' + state.details.networks.filter(n => recordedChains.has(n.id)).map(n => `<option value="${n.id}">${escapeHTML(n.name)}${n.environment === 'testnet' ? ' · Testnet' : ''}</option>`).join('');
41
+ $('all-token-network').value = [...$('all-token-network').options].some(o => o.value === network) ? network : 'all';
42
+ $('sync-label').textContent = 'Latest recorded holdings'; renderCatalog();
43
+ } else if (selectedView === 'activity') {
44
+ renderBreadcrumb([{ label: 'Home', href: '#/home' }, { label: 'Activity' }]);
45
+ document.title = 'Activity · Kira Wallet'; $('sync-label').textContent = 'Local research history';
46
+ }
47
+ renderBriefing();
48
+ }
49
+ function renderBriefing() {
50
+ if (!state) return;
51
+ renderKiraNote();
52
+ const active = typeof jobList !== 'undefined' && jobList.some(KiraView.active);
53
+ const gaps = state.wallets.reduce((sum, w) => sum + w.chains.filter(c => c.environment === 'mainnet' && (!c.complete || !c.rpc_available)).length, 0);
54
+ let art = 'welcome', caption = 'A little perspective.', note = 'Here to make sense of it.';
55
+ if (active) { art = 'research'; caption = 'Following the evidence.'; note = 'Research is in progress.'; }
56
+ else if (gaps && briefingMode === 'coverage') { art = 'attention'; caption = 'A closer look.'; note = 'Some facts are still missing.'; }
57
+ else if (selectedView === 'activity') { art = 'review'; caption = 'Every step, recorded.'; note = 'Good research leaves a trail.'; }
58
+ else if (['token', 'network', 'tokens', 'wallet'].includes(selectedView)) { art = 'explain'; caption = 'Let’s look a little closer.'; note = 'Context makes the difference.'; }
59
+ const src = art === 'welcome' ? '/kira.png' : '/kira-' + art + '.png';
60
+ if ($('kira-scene-art').getAttribute('src') !== src) $('kira-scene-art').src = src;
61
+ $('kira-scene-art').alt = { welcome: 'Kira holding her research notebook', research: 'Kira writing while researching', explain: 'Kira explaining a finding', review: 'Kira reviewing saved notes', attention: 'Kira carefully checking incomplete evidence' }[art];
62
+ $('kira-mood').textContent = caption; $('kira-scene-note').textContent = note;
63
+ $('kira-context').textContent = selectedView === 'wallet' ? currentWallet()?.name || 'Wallet' : selectedView === 'token' ? state.details.tokens.find(t => t.id === selectedToken)?.symbol || 'Token' : selectedView === 'network' ? state.details.networks.find(n => n.id === selectedNetwork)?.name || 'Network' : { home: 'Portfolio overview', tokens: 'All tokens', activity: 'Research activity' }[selectedView];
64
+ if (briefingMode === 'coverage') {
65
+ const selected = selectedView === 'wallet' ? [currentWallet()].filter(Boolean) : state.wallets;
66
+ const missing = selected.flatMap(w => w.chains.filter(c => c.environment === 'mainnet' && (!c.complete || !c.rpc_available)).map(c => w.name + ' · ' + c.name + (!c.rpc_available ? ': RPC unavailable' : ': incomplete discovery')));
67
+ $('kira-note-text').textContent = missing.length ? 'These recorded coverage gaps need a closer look: ' + missing.slice(0, 6).join('; ') + (missing.length > 6 ? '; and ' + (missing.length - 6) + ' more.' : '.') + ' Missing data does not mean zero holdings.' : 'No mainnet coverage gaps are flagged in this recorded view. This does not prove that every possible token has been discovered.';
68
+ } else if (selectedView === 'tokens') {
69
+ const tokens = state.details.tokens.filter(t => t.environment !== 'testnet');
70
+ $('kira-note-text').textContent = `There are ${tokens.length} recorded mainnet tokens. ${tokens.filter(t => t.value_usd == null || t.unpriced_count > 0).length} have unpriced amounts. I keep tokens on different networks separate, even when their symbols match.`;
71
+ } else if (selectedView === 'activity') {
72
+ const running = typeof jobList === 'undefined' ? 0 : jobList.filter(KiraView.active).length;
73
+ $('kira-note-text').textContent = running ? `${running} local research ${running === 1 ? 'job is' : 'jobs are'} in progress. The timeline shows the last recorded stage, elapsed time and freshness. A quiet stage is not proof that a worker stopped.` : 'There is no recorded job in progress. Completed work stays in the timeline, along with partial results, interruptions and failed attempts.';
74
+ }
75
+ }
76
+ for (const id of ['all-token-network', 'all-token-pricing', 'all-token-sort', 'all-token-testnets']) $(id).addEventListener('change', () => { catalogPage = 1; renderCatalog(); });
77
+ $('all-token-search').addEventListener('input', () => { clearTimeout(catalogTimer); catalogTimer = setTimeout(() => { catalogPage = 1; renderCatalog(); }, 120); });
78
+ $('tokens-prev').addEventListener('click', () => { catalogPage--; renderCatalog(); });
79
+ $('tokens-next').addEventListener('click', () => { catalogPage++; renderCatalog(); });
80
+ function setWorkspacePane(pane) {
81
+ document.querySelector('.workspace').dataset.pane = pane;
82
+ document.querySelectorAll('.mobile-pane-control [data-pane]').forEach(button => button.setAttribute('aria-pressed', String(button.dataset.pane === pane)));
83
+ }
84
+ document.querySelectorAll('.mobile-pane-control [data-pane]').forEach(button => button.addEventListener('click', () => {
85
+ setWorkspacePane(button.dataset.pane);
86
+ if (button.dataset.pane === 'kira' && matchMedia('(max-width: 980px)').matches) {
87
+ document.querySelector('.workspace').scrollIntoView({ block: 'start', behavior: 'instant' });
88
+ }
89
+ }));
90
+ // Explicit navigation must reveal the selected evidence, including same-route clicks.
91
+ document.addEventListener('click', event => {
92
+ if (event.target.closest('a[href^="#/"]')) setWorkspacePane('portfolio');
93
+ });
94
+ document.querySelectorAll('[data-brief]').forEach(button => button.addEventListener('click', () => {
95
+ if (button.dataset.brief === 'coverage') { briefingMode = 'coverage'; renderBriefing(); }
96
+ else if (button.dataset.brief === 'unpriced') { briefingMode = 'view'; $('all-token-pricing').value = 'unpriced'; catalogPage = 1; navigate('#/tokens'); renderCatalog(); }
97
+ else { briefingMode = 'view'; navigate('#/activity'); }
98
+ if (button.dataset.brief !== 'coverage') document.querySelector('.mobile-pane-control [data-pane="portfolio"]').click();
99
+ }));
100
+ window.addEventListener('hashchange', () => { briefingMode = 'view'; setWorkspacePane('portfolio'); renderWorkspace(); });
101
+ try { localStorage.removeItem('kira-draft'); } catch { /* Drafts are now memory-only. */ }
102
+ renderWorkspace();