lazyclaw 5.4.4 → 6.0.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/channels/handoff.mjs +36 -0
- package/cli.mjs +73 -7399
- package/daemon.mjs +23 -2085
- package/mas/agent_turn.mjs +2 -1
- package/mas/index_db.mjs +82 -0
- package/mas/learning.mjs +17 -1
- package/mas/mention_router.mjs +38 -10
- package/mas/provider_adapters.mjs +28 -4
- package/mas/scrub_env.mjs +34 -0
- package/mas/tool_runner.mjs +23 -7
- package/mas/tools/bash.mjs +10 -5
- package/mas/tools/browser.mjs +18 -0
- package/mas/tools/learning.mjs +24 -14
- package/mas/tools/recall.mjs +5 -1
- package/mas/tools/web.mjs +47 -11
- package/mas/trajectory_store.mjs +7 -4
- package/package.json +3 -2
- package/providers/auth_store.mjs +22 -0
- package/providers/claude_cli.mjs +28 -2
- package/providers/claude_cli_detect.mjs +46 -0
- package/providers/custom_provider.mjs +70 -0
- package/providers/model_catalogue.mjs +86 -0
- package/providers/orchestrator.mjs +30 -9
- package/providers/rates.mjs +12 -2
- package/providers/registry.mjs +10 -7
- package/sandbox/confiners/landlock.mjs +14 -8
- package/sandbox/confiners/seatbelt.mjs +18 -2
- package/scripts/loop-worker.mjs +18 -7
- package/scripts/migrate-v5.mjs +5 -61
- package/sessions.mjs +0 -0
- package/tui/modal_filter.mjs +59 -0
- package/tui/modal_picker.mjs +12 -37
- package/tui/pickers.mjs +917 -0
- package/tui/provider_families.mjs +41 -0
- package/tui/repl.mjs +67 -36
- package/tui/slash_commands.mjs +2 -1
- package/tui/slash_dispatcher.mjs +717 -58
- package/tui/splash.mjs +5 -12
- package/tui/subcommands.mjs +17 -0
- package/tui/terminal_approve.mjs +37 -0
- package/web/dashboard.css +275 -0
- package/web/dashboard.html +2 -1685
- package/web/dashboard.js +1406 -0
- package/workflow/persistent.mjs +13 -6
- package/mas/tools/skill_view.mjs +0 -43
package/tui/splash.mjs
CHANGED
|
@@ -26,18 +26,11 @@ const WORDMARK_BREAKPOINT = 140; // drop wordmark below this
|
|
|
26
26
|
const MEDIUM_BREAKPOINT = 90; // side-by-side sloth+panel above this; stacked below
|
|
27
27
|
const NARROW_BREAKPOINT = 45; // headline-only fallback below this
|
|
28
28
|
|
|
29
|
-
// Subcommand catalog — grouped for the splash so a new user sees the
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
['config', ['config', 'auth', 'rates', 'providers', 'setup', 'onboard']],
|
|
35
|
-
['state', ['sessions', 'skills', 'workspace', 'memory', 'status', 'doctor']],
|
|
36
|
-
['runtime', ['daemon', 'cron', 'loop', 'loops', 'goal']],
|
|
37
|
-
['channels', ['slack', 'telegram', 'matrix', 'channels', 'message', 'pairing']],
|
|
38
|
-
['v5', ['sandbox', 'personality', 'migrate', 'hermes', 'openclaw', 'trajectories']],
|
|
39
|
-
['utility', ['browse', 'version', 'completion', 'help', 'export', 'import', 'nodes']],
|
|
40
|
-
];
|
|
29
|
+
// Subcommand catalog — grouped for the splash so a new user sees the surface
|
|
30
|
+
// area at a glance. Single source of truth lives in the react-free
|
|
31
|
+
// tui/subcommands.mjs so the in-chat /menu palette can share it.
|
|
32
|
+
export { SUBCOMMAND_GROUPS } from './subcommands.mjs';
|
|
33
|
+
import { SUBCOMMAND_GROUPS } from './subcommands.mjs';
|
|
41
34
|
|
|
42
35
|
function fit(text, max) {
|
|
43
36
|
if (stringWidth(text) <= max) return text.padEnd(max);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// tui/subcommands.mjs — the lazyclaw subcommand catalog, grouped. Pure data
|
|
2
|
+
// (no react/ink) so both the splash (tui/splash.mjs) and the in-chat command
|
|
3
|
+
// palette (/menu in tui/slash_dispatcher.mjs) can share it. The no-arg
|
|
4
|
+
// launcher menu used to be the home screen; defaulting to chat hid it behind
|
|
5
|
+
// `lazyclaw menu`, so /menu brings the discoverable subcommand list back into
|
|
6
|
+
// the chat.
|
|
7
|
+
|
|
8
|
+
export const SUBCOMMAND_GROUPS = [
|
|
9
|
+
['core', ['chat', 'agent', 'orchestrator', 'dashboard', 'menu']],
|
|
10
|
+
['workflow', ['run', 'resume', 'inspect', 'clear', 'validate', 'graph']],
|
|
11
|
+
['config', ['config', 'auth', 'rates', 'providers', 'setup', 'onboard']],
|
|
12
|
+
['state', ['sessions', 'skills', 'workspace', 'memory', 'status', 'doctor']],
|
|
13
|
+
['runtime', ['daemon', 'cron', 'loop', 'loops', 'goal']],
|
|
14
|
+
['channels', ['slack', 'telegram', 'matrix', 'channels', 'message', 'pairing']],
|
|
15
|
+
['v5', ['sandbox', 'personality', 'migrate', 'hermes', 'openclaw', 'trajectories']],
|
|
16
|
+
['utility', ['browse', 'version', 'completion', 'help', 'export', 'import', 'nodes']],
|
|
17
|
+
];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// terminal_approve.mjs — default human-in-the-loop approval hook for the
|
|
2
|
+
// bare (non-Ink) CLI path, e.g. `lazyclaw task tick` on a TTY.
|
|
3
|
+
//
|
|
4
|
+
// The tool runner is fail-closed: a sensitive tool (bash / write / web /
|
|
5
|
+
// browser / delegate) runs only behind an approve({tool,args,agent}) →
|
|
6
|
+
// {approved,reason} hook, or an explicit security.allowUnattendedSensitive
|
|
7
|
+
// opt-in. This module supplies the default hook: a y/N prompt on the
|
|
8
|
+
// controlling terminal. Default (bare Enter), timeout, EOF, and a
|
|
9
|
+
// non-TTY stdin ALL deny — approval is never granted by omission.
|
|
10
|
+
|
|
11
|
+
import readline from 'node:readline';
|
|
12
|
+
import { redactSecrets } from '../mas/redact.mjs';
|
|
13
|
+
|
|
14
|
+
export function makeReadlineApprove({ input = process.stdin, output = process.stderr, timeoutMs = 120_000 } = {}) {
|
|
15
|
+
return async function approve({ tool, args, agent }) {
|
|
16
|
+
if (!input.isTTY) return { approved: false, reason: 'no TTY for approval (fail-closed)' };
|
|
17
|
+
const raw = typeof args === 'object' ? JSON.stringify(args) : String(args ?? '');
|
|
18
|
+
const summary = redactSecrets(raw).slice(0, 400);
|
|
19
|
+
const rl = readline.createInterface({ input, output });
|
|
20
|
+
try {
|
|
21
|
+
const answer = await new Promise((resolve) => {
|
|
22
|
+
let settled = false;
|
|
23
|
+
const finish = (v) => { if (!settled) { settled = true; resolve(v); } };
|
|
24
|
+
const timer = setTimeout(() => finish(''), timeoutMs);
|
|
25
|
+
rl.on('close', () => { clearTimeout(timer); finish(''); });
|
|
26
|
+
rl.question(`\n⚠ agent "${agent}" wants to run sensitive tool "${tool}": ${summary}\n approve? [y/N] `, (a) => {
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
finish(a);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
const approved = /^\s*y(es)?\s*$/i.test(answer);
|
|
32
|
+
return { approved, reason: approved ? 'approved at terminal' : 'denied at terminal (default)' };
|
|
33
|
+
} finally {
|
|
34
|
+
rl.close();
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg: #0a0a0a;
|
|
3
|
+
--card: #14141c;
|
|
4
|
+
--border: #2a2a36;
|
|
5
|
+
--text: #e8e8ea;
|
|
6
|
+
--dim: #a8a8b8;
|
|
7
|
+
--accent: #d97757;
|
|
8
|
+
--ok: #4ade80;
|
|
9
|
+
--warn: #f59e0b;
|
|
10
|
+
--err: #ef4444;
|
|
11
|
+
}
|
|
12
|
+
* { box-sizing: border-box; }
|
|
13
|
+
body {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font: 14px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
16
|
+
background: var(--bg);
|
|
17
|
+
color: var(--text);
|
|
18
|
+
min-height: 100vh;
|
|
19
|
+
display: flex;
|
|
20
|
+
flex-direction: column;
|
|
21
|
+
}
|
|
22
|
+
header {
|
|
23
|
+
padding: 14px 22px;
|
|
24
|
+
border-bottom: 1px solid var(--border);
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: 14px;
|
|
28
|
+
}
|
|
29
|
+
.logo { font-weight: 700; font-size: 16px; color: var(--accent); display: flex; align-items: center; gap: 10px; }
|
|
30
|
+
.logo .mascot { width: 44px; height: 44px; flex: none; image-rendering: pixelated; image-rendering: crisp-edges; }
|
|
31
|
+
.ver { color: var(--dim); font-size: 11px; }
|
|
32
|
+
/* lazyclaw 16x16 pixel mascot — Claude Design handoff (mascot sheet
|
|
33
|
+
v0.1, "claude original" palette). Claude's asterisk star (#d97757)
|
|
34
|
+
worn under a crustacean helmet (#c33d2a) with two antenna-claws.
|
|
35
|
+
Idle pose (sleepy slits). Hover gently brightens the helmet. */
|
|
36
|
+
.mascot { transition: filter 0.2s ease; }
|
|
37
|
+
.logo:hover .mascot { filter: drop-shadow(0 0 6px rgba(217, 119, 87, 0.45)); }
|
|
38
|
+
nav.tabs {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-wrap: wrap;
|
|
41
|
+
gap: 2px;
|
|
42
|
+
padding: 0 14px;
|
|
43
|
+
border-bottom: 1px solid var(--border);
|
|
44
|
+
}
|
|
45
|
+
nav.tabs button {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
min-height: 44px;
|
|
49
|
+
background: none;
|
|
50
|
+
border: 0;
|
|
51
|
+
color: var(--dim);
|
|
52
|
+
padding: 10px 16px;
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
font-size: 13px;
|
|
55
|
+
border-bottom: 2px solid transparent;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
}
|
|
58
|
+
nav.tabs button:hover { color: var(--text); }
|
|
59
|
+
nav.tabs button.active {
|
|
60
|
+
color: var(--accent);
|
|
61
|
+
border-bottom-color: var(--accent);
|
|
62
|
+
}
|
|
63
|
+
main {
|
|
64
|
+
flex: 1;
|
|
65
|
+
padding: 22px;
|
|
66
|
+
overflow-y: auto;
|
|
67
|
+
}
|
|
68
|
+
section { display: none; }
|
|
69
|
+
section.active { display: block; }
|
|
70
|
+
h2 { margin: 0 0 14px; font-size: 18px; }
|
|
71
|
+
.card {
|
|
72
|
+
background: var(--card);
|
|
73
|
+
border: 1px solid var(--border);
|
|
74
|
+
border-radius: 8px;
|
|
75
|
+
padding: 14px 16px;
|
|
76
|
+
margin-bottom: 12px;
|
|
77
|
+
}
|
|
78
|
+
.row { display: flex; align-items: center; gap: 12px; padding: 6px 0; }
|
|
79
|
+
.row + .row { border-top: 1px solid var(--border); }
|
|
80
|
+
.name { font-weight: 600; }
|
|
81
|
+
.dim { color: var(--dim); font-size: 12px; }
|
|
82
|
+
button.btn {
|
|
83
|
+
background: var(--accent);
|
|
84
|
+
color: #fff;
|
|
85
|
+
border: 0;
|
|
86
|
+
border-radius: 6px;
|
|
87
|
+
padding: 8px 14px;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
font-size: 13px;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
}
|
|
92
|
+
button.btn:hover { filter: brightness(1.1); }
|
|
93
|
+
button.btn-secondary {
|
|
94
|
+
background: transparent;
|
|
95
|
+
border: 1px solid var(--border);
|
|
96
|
+
color: var(--text);
|
|
97
|
+
}
|
|
98
|
+
button.btn-secondary:hover { border-color: var(--accent); color: var(--accent); }
|
|
99
|
+
.empty { color: var(--dim); padding: 24px; text-align: center; font-style: italic; }
|
|
100
|
+
pre {
|
|
101
|
+
background: #06060c;
|
|
102
|
+
padding: 12px 14px;
|
|
103
|
+
border-radius: 6px;
|
|
104
|
+
overflow-x: auto;
|
|
105
|
+
font-size: 12px;
|
|
106
|
+
color: #cfcfd6;
|
|
107
|
+
border: 1px solid var(--border);
|
|
108
|
+
}
|
|
109
|
+
/* Chat */
|
|
110
|
+
#chat-stream {
|
|
111
|
+
background: var(--card);
|
|
112
|
+
border: 1px solid var(--border);
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
padding: 12px;
|
|
115
|
+
height: 50vh;
|
|
116
|
+
overflow-y: auto;
|
|
117
|
+
display: flex;
|
|
118
|
+
flex-direction: column;
|
|
119
|
+
gap: 14px;
|
|
120
|
+
}
|
|
121
|
+
.msg { padding: 8px 12px; border-radius: 6px; max-width: 90%; white-space: pre-wrap; word-wrap: break-word; }
|
|
122
|
+
.msg.user { align-self: flex-end; background: rgba(217, 119, 87, 0.15); border: 1px solid rgba(217, 119, 87, 0.3); }
|
|
123
|
+
.msg.assistant { align-self: flex-start; background: rgba(74, 222, 128, 0.06); border: 1px solid rgba(74, 222, 128, 0.18); }
|
|
124
|
+
.msg.error { align-self: stretch; background: rgba(239, 68, 68, 0.12); border: 1px solid rgba(239, 68, 68, 0.3); color: #ffd3d3; }
|
|
125
|
+
.input-row {
|
|
126
|
+
display: flex;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
margin-top: 12px;
|
|
129
|
+
}
|
|
130
|
+
.input-row textarea {
|
|
131
|
+
flex: 1;
|
|
132
|
+
background: var(--card);
|
|
133
|
+
border: 1px solid var(--border);
|
|
134
|
+
border-radius: 6px;
|
|
135
|
+
padding: 10px 12px;
|
|
136
|
+
color: var(--text);
|
|
137
|
+
resize: vertical;
|
|
138
|
+
min-height: 60px;
|
|
139
|
+
font: inherit;
|
|
140
|
+
}
|
|
141
|
+
.pill {
|
|
142
|
+
display: inline-block;
|
|
143
|
+
padding: 2px 8px;
|
|
144
|
+
border-radius: 999px;
|
|
145
|
+
font-size: 10px;
|
|
146
|
+
background: var(--border);
|
|
147
|
+
color: var(--dim);
|
|
148
|
+
margin-left: 6px;
|
|
149
|
+
}
|
|
150
|
+
.pill.ok { background: rgba(74, 222, 128, 0.15); color: var(--ok); }
|
|
151
|
+
.pill.warn { background: rgba(245, 158, 11, 0.15); color: var(--warn); }
|
|
152
|
+
.pill.err { background: rgba(239, 68, 68, 0.15); color: var(--err); }
|
|
153
|
+
.toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; }
|
|
154
|
+
.toolbar select {
|
|
155
|
+
background: var(--card);
|
|
156
|
+
border: 1px solid var(--border);
|
|
157
|
+
color: var(--text);
|
|
158
|
+
padding: 6px 10px;
|
|
159
|
+
border-radius: 6px;
|
|
160
|
+
font: inherit;
|
|
161
|
+
}
|
|
162
|
+
.toolbar input[type="text"], .toolbar input[type="search"] {
|
|
163
|
+
background: var(--card);
|
|
164
|
+
border: 1px solid var(--border);
|
|
165
|
+
color: var(--text);
|
|
166
|
+
padding: 6px 10px;
|
|
167
|
+
border-radius: 6px;
|
|
168
|
+
font: inherit;
|
|
169
|
+
min-width: 0;
|
|
170
|
+
}
|
|
171
|
+
.grid {
|
|
172
|
+
display: grid;
|
|
173
|
+
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
174
|
+
gap: 10px;
|
|
175
|
+
margin-bottom: 14px;
|
|
176
|
+
}
|
|
177
|
+
.stat {
|
|
178
|
+
background: var(--card);
|
|
179
|
+
border: 1px solid var(--border);
|
|
180
|
+
border-radius: 8px;
|
|
181
|
+
padding: 12px 14px;
|
|
182
|
+
}
|
|
183
|
+
.stat .label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
184
|
+
.stat .value { font-size: 22px; font-weight: 600; margin-top: 4px; word-break: break-all; }
|
|
185
|
+
.stat .sub { color: var(--dim); font-size: 11px; margin-top: 4px; }
|
|
186
|
+
table.tbl {
|
|
187
|
+
width: 100%;
|
|
188
|
+
border-collapse: collapse;
|
|
189
|
+
background: var(--card);
|
|
190
|
+
border: 1px solid var(--border);
|
|
191
|
+
border-radius: 8px;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
font-size: 13px;
|
|
194
|
+
}
|
|
195
|
+
table.tbl th, table.tbl td {
|
|
196
|
+
padding: 8px 12px;
|
|
197
|
+
text-align: left;
|
|
198
|
+
border-bottom: 1px solid var(--border);
|
|
199
|
+
vertical-align: top;
|
|
200
|
+
}
|
|
201
|
+
table.tbl th { color: var(--dim); font-weight: 500; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; background: rgba(255,255,255,0.02); }
|
|
202
|
+
table.tbl tr:last-child td { border-bottom: 0; }
|
|
203
|
+
table.tbl td.num { text-align: right; font-variant-numeric: tabular-nums; }
|
|
204
|
+
.banner {
|
|
205
|
+
padding: 10px 14px;
|
|
206
|
+
border-radius: 8px;
|
|
207
|
+
margin-bottom: 12px;
|
|
208
|
+
border: 1px solid var(--border);
|
|
209
|
+
background: var(--card);
|
|
210
|
+
display: flex;
|
|
211
|
+
flex-wrap: wrap;
|
|
212
|
+
align-items: center;
|
|
213
|
+
gap: 10px;
|
|
214
|
+
}
|
|
215
|
+
.banner.ok { border-color: rgba(74, 222, 128, 0.4); background: rgba(74, 222, 128, 0.06); }
|
|
216
|
+
.banner.warn { border-color: rgba(245, 158, 11, 0.4); background: rgba(245, 158, 11, 0.06); }
|
|
217
|
+
.banner.err { border-color: rgba(239, 68, 68, 0.4); background: rgba(239, 68, 68, 0.06); }
|
|
218
|
+
.banner ul { margin: 6px 0 0 18px; padding: 0; }
|
|
219
|
+
.banner li { font-size: 12px; }
|
|
220
|
+
/* Modal — used by session/workflow/skill detail views, the rate-card
|
|
221
|
+
editor, and the custom-provider form. One stacking layer; only one
|
|
222
|
+
open at a time. */
|
|
223
|
+
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.65); display: none; align-items: center; justify-content: center; z-index: 100; padding: 20px; }
|
|
224
|
+
.modal-backdrop.open { display: flex; }
|
|
225
|
+
.modal { background: var(--card); border: 1px solid var(--border); border-radius: 10px; max-width: min(720px, 96vw); width: 100%; max-height: 86vh; display: flex; flex-direction: column; box-shadow: 0 20px 60px rgba(0,0,0,0.5); }
|
|
226
|
+
.modal-head { padding: 14px 18px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 10px; }
|
|
227
|
+
.modal-head h3 { margin: 0; font-size: 16px; font-weight: 600; flex: 1; }
|
|
228
|
+
.modal-close { background: none; border: 0; color: var(--dim); cursor: pointer; font-size: 20px; line-height: 1; padding: 4px 8px; }
|
|
229
|
+
.modal-close:hover { color: var(--text); }
|
|
230
|
+
.modal-body { padding: 16px 18px; overflow-y: auto; flex: 1; }
|
|
231
|
+
.modal-foot { padding: 12px 18px; border-top: 1px solid var(--border); display: flex; gap: 8px; justify-content: flex-end; }
|
|
232
|
+
.clickable { cursor: pointer; }
|
|
233
|
+
.clickable:hover { background: rgba(217, 119, 87, 0.05); }
|
|
234
|
+
.turn { padding: 8px 12px; border-radius: 6px; margin-bottom: 8px; white-space: pre-wrap; word-wrap: break-word; font-size: 13px; }
|
|
235
|
+
.turn.user { background: rgba(217, 119, 87, 0.10); border: 1px solid rgba(217, 119, 87, 0.25); }
|
|
236
|
+
.turn.assistant { background: rgba(74, 222, 128, 0.06); border: 1px solid rgba(74, 222, 128, 0.18); }
|
|
237
|
+
.turn.system { background: rgba(245, 158, 11, 0.06); border: 1px solid rgba(245, 158, 11, 0.20); }
|
|
238
|
+
.turn .role-tag { display: block; color: var(--dim); font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 4px; }
|
|
239
|
+
.form-row { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }
|
|
240
|
+
.form-row label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; }
|
|
241
|
+
.form-row input, .form-row textarea { background: var(--bg); border: 1px solid var(--border); color: var(--text); padding: 8px 10px; border-radius: 6px; font: inherit; font-size: 13px; }
|
|
242
|
+
.form-row textarea { resize: vertical; min-height: 80px; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }
|
|
243
|
+
.row-actions { display: flex; gap: 6px; align-items: center; margin-left: auto; }
|
|
244
|
+
button.btn-sm { font-size: 12px; padding: 4px 10px; }
|
|
245
|
+
button.btn-danger { background: rgba(239,68,68,0.15); color: #ffb4b4; border: 1px solid rgba(239,68,68,0.3); }
|
|
246
|
+
button.btn-danger:hover { background: rgba(239,68,68,0.25); }
|
|
247
|
+
.markdown { font-size: 13px; line-height: 1.55; }
|
|
248
|
+
.markdown pre { font-size: 12px; }
|
|
249
|
+
/* a11y: a visible keyboard focus ring on every interactive control.
|
|
250
|
+
The styled buttons/inputs otherwise relied on the UA default ring,
|
|
251
|
+
which is easy to lose against the dark theme. */
|
|
252
|
+
a:focus-visible, button:focus-visible, input:focus-visible,
|
|
253
|
+
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible {
|
|
254
|
+
outline: 2px solid var(--accent);
|
|
255
|
+
outline-offset: 2px;
|
|
256
|
+
border-radius: 4px;
|
|
257
|
+
}
|
|
258
|
+
@media (prefers-reduced-motion: reduce) {
|
|
259
|
+
* { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
|
|
260
|
+
}
|
|
261
|
+
@media (max-width: 480px) {
|
|
262
|
+
main { padding: 14px; }
|
|
263
|
+
header { padding: 12px 16px; }
|
|
264
|
+
.grid { grid-template-columns: 1fr; }
|
|
265
|
+
table.tbl { font-size: 12px; }
|
|
266
|
+
table.tbl th, table.tbl td { padding: 6px 8px; }
|
|
267
|
+
}
|
|
268
|
+
footer {
|
|
269
|
+
padding: 10px 22px;
|
|
270
|
+
border-top: 1px solid var(--border);
|
|
271
|
+
color: var(--dim);
|
|
272
|
+
font-size: 11px;
|
|
273
|
+
display: flex;
|
|
274
|
+
justify-content: space-between;
|
|
275
|
+
}
|