@gotcos/glasses-server 6.99.25 → 6.99.33
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/CHANGELOG.md +224 -0
- package/package.json +1 -1
- package/server/index.ts +46 -4
- package/server/lib/agent-session-output-images.ts +280 -0
- package/server/lib/agent-session-store.ts +3 -1
- package/server/lib/agent-session-turns.ts +18 -7
- package/server/lib/client-liveness.ts +38 -3
- package/server/lib/codex-subagent-activity.ts +36 -0
- package/server/lib/g2-turn-provenance.ts +679 -0
- package/server/lib/media-store.ts +41 -2
- package/server/lib/permission-broker.ts +94 -17
- package/server/lib/queue-turn-evidence.ts +19 -2
- package/server/lib/session-halt.ts +15 -1
- package/server/lib/session-stream-events.ts +4 -0
- package/server/lib/session-subagents.ts +155 -0
- package/server/lib/session-turn-attachments.ts +245 -0
- package/server/lib/thread-turn-queue-deliver.ts +17 -2
- package/server/lib/thread-turn-queue.ts +10 -0
- package/server/lib/today-brief.ts +731 -0
- package/server/lib/turn-file-store.ts +202 -0
- package/server/lib/turn-footer.ts +81 -0
- package/server/routes/agent-session-bindings.ts +164 -16
- package/server/routes/agent-sessions.ts +77 -8
- package/server/routes/client-instance.ts +12 -1
- package/server/routes/cursor-stop-followup.ts +30 -1
- package/server/routes/g2-turns.ts +46 -0
- package/server/routes/health.ts +9 -0
- package/server/routes/media.ts +32 -8
- package/server/routes/permission-broker.ts +47 -4
- package/server/routes/thread-turn-queue.ts +148 -8
- package/server/routes/today-brief.ts +148 -0
- package/server/web-control/app.css +42 -9
- package/server/web-control/app.js +975 -114
- package/server/web-control/control.css +12 -6
- package/server/web-control/index.html +6 -1
- package/server/web-control/panes/cos-chat.css +27 -3
- package/server/web-control/panes/cos-chat.js +240 -46
- package/server/web-control/panes/meetings.css +5 -2
- package/server/web-control/panes/meetings.js +21 -10
- package/server/web-control/panes/memories.css +1 -1
- package/server/web-control/panes/memories.js +3 -8
- package/server/web-control/panes/questions.css +56 -2
- package/server/web-control/panes/questions.js +454 -56
- package/server/web-control/panes/reader-media.js +154 -0
- package/server/web-control/panes/sessions.css +20 -7
- package/server/web-control/panes/settings.js +34 -2
- package/server/web-control/panes/speakers.css +3 -5
- package/server/web-control/panes/speakers.js +7 -4
- package/server/web-control/panes/threads.js +3 -2
- package/server/web-control/panes/today-actions.css +81 -3
- package/server/web-control/panes/today-actions.js +527 -62
- package/server/web-control/panes/work.css +1 -1
- package/server/web-control/shell.js +3 -0
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
// "2026-11-03 · 9:05 PM · P · 60 minutes · Mac System Audio"
|
|
103
103
|
function subtitle(m) {
|
|
104
104
|
var parts = [];
|
|
105
|
-
if (m.date) parts.push(m.date);
|
|
105
|
+
if (m.date) parts.push(W.plainDay ? W.plainDay(m.date) : m.date); // "Today" or "Nov 7", never 2026-11-07 (6.99.30)
|
|
106
106
|
if (m.time) parts.push(fmtTime(m.time));
|
|
107
107
|
var d = domainLabel(m); if (d) parts.push(d);
|
|
108
108
|
if (m.duration) parts.push(m.duration); else if (num(m.durationMinutes) > 0) parts.push(num(m.durationMinutes) + ' min');
|
|
@@ -2352,7 +2352,13 @@
|
|
|
2352
2352
|
function prepCapsNow() { var cc = W.cosChat; return cc && typeof cc.prepCaps === 'function' ? cc.prepCaps() : null; }
|
|
2353
2353
|
function livePrep(sid) { if (!S.lprep || S.lprep.sid !== sid) S.lprep = { sid: sid, asked: false, state: 'idle', drafts: [], busy: '', done: null, note: '', changed: '' }; return S.lprep; }
|
|
2354
2354
|
function notePrepInstance(b) { var cc = W.cosChat; if (b && cc && typeof cc.notePrepInstance === 'function') cc.notePrepInstance(b.serverInstanceId); }
|
|
2355
|
-
function prepBoundTo(d, sid) { return ((d && d.bindings) || []).some(function (b) { return b && b.sessionId === sid && !b.removedAt; }); }
|
|
2355
|
+
function prepBoundTo(d, sid) { return ((d && d.bindings) || []).some(function (b) { return b && b.sessionId === sid && !b.removedAt && b.state !== 'applying'; }); }
|
|
2356
|
+
// An interrupted bind owns frozen rows at its original revision. A later editable draft is not its retry.
|
|
2357
|
+
function prepPendingBind(d, sid) {
|
|
2358
|
+
return ((d && d.bindings) || []).filter(function (b) {
|
|
2359
|
+
return b && b.sessionId === sid && !b.removedAt && b.state === 'applying' && Number.isSafeInteger(b.revision) && b.revision >= 0;
|
|
2360
|
+
})[0] || null;
|
|
2361
|
+
}
|
|
2356
2362
|
function prepReadyFiles(d) { return ((d && d.files) || []).filter(function (x) { return x && x.status === 'ready'; }); }
|
|
2357
2363
|
function prepHasNotes(d) { return !!String((d && d.steer) || '').trim(); }
|
|
2358
2364
|
// Whether this live view may offer a prep at all: the area is reserved exactly when this is true.
|
|
@@ -2376,7 +2382,7 @@
|
|
|
2376
2382
|
if (S.lprep !== p) return;
|
|
2377
2383
|
notePrepInstance(r.body);
|
|
2378
2384
|
p.drafts = r.status === 200 && Array.isArray(r.body.drafts) ? r.body.drafts.filter(function (d) {
|
|
2379
|
-
return d && typeof d.id === 'string' && !prepBoundTo(d, f.sid) && (prepHasNotes(d) || prepReadyFiles(d).length > 0);
|
|
2385
|
+
return d && typeof d.id === 'string' && !prepBoundTo(d, f.sid) && (prepPendingBind(d, f.sid) || prepHasNotes(d) || prepReadyFiles(d).length > 0);
|
|
2380
2386
|
}).slice(0, 5) : [];
|
|
2381
2387
|
p.state = r.status === 200 ? 'ready' : 'failed';
|
|
2382
2388
|
paintLivePrep();
|
|
@@ -2388,7 +2394,7 @@
|
|
|
2388
2394
|
return prepHasNotes(d) ? (n ? 'its notes and ' + files : 'its notes') : 'its ' + files;
|
|
2389
2395
|
}
|
|
2390
2396
|
function prepAddBtn(p, d) {
|
|
2391
|
-
return '<button type="button" class="cx-btn quiet" data-mt="prep-bind" data-id="' + esc(d.id) + '"' + (p.busy ? ' disabled' : '') + '>' + (p.busy === d.id ? 'Adding…' : 'Add to this meeting') + '</button>';
|
|
2397
|
+
return '<button type="button" class="cx-btn quiet" data-mt="prep-bind" data-id="' + esc(d.id) + '"' + (p.busy ? ' disabled' : '') + '>' + (p.busy === d.id ? 'Adding…' : prepPendingBind(d, p.sid) ? 'Retry add prep' : 'Add to this meeting') + '</button>';
|
|
2392
2398
|
}
|
|
2393
2399
|
function prepQuiet(text, warn) { return '<div class="mt-prepline" role="status"><span class="mt-prepline-text' + (warn ? ' mt-amber' : '') + '">' + esc(text) + '</span></div>'; }
|
|
2394
2400
|
function livePrepHtml() {
|
|
@@ -2402,14 +2408,14 @@
|
|
|
2402
2408
|
else if (!p.drafts.length) html = prepQuiet('No prep for this meeting. Make one from a Prep me card in COS chat.');
|
|
2403
2409
|
else if (p.drafts.length === 1) {
|
|
2404
2410
|
var d = p.drafts[0], title = cleanTitle(d.title) || 'this meeting';
|
|
2405
|
-
var text = p.note || 'Prep for ' + title + (p.changed === d.id ? ' changed since it was offered: add ' : ' is ready: add ') + prepWhat(d) + ' to this meeting';
|
|
2411
|
+
var text = p.note || (prepPendingBind(d, p.sid) ? 'Finish adding the saved prep for ' + title + '. Later edits are not included.' : 'Prep for ' + title + (p.changed === d.id ? ' changed since it was offered: add ' : ' is ready: add ') + prepWhat(d) + ' to this meeting');
|
|
2406
2412
|
html = '<div class="mt-prepline"><span class="mt-prepline-text' + (p.note ? ' mt-amber' : '') + '" title="' + esc(text) + '">' + esc(text) + '</span>' + prepAddBtn(p, d) + '</div>';
|
|
2407
2413
|
} else {
|
|
2408
2414
|
// Several: the person picks. Each row says what it adds; the list scrolls inside the same box.
|
|
2409
2415
|
html = '<div class="mt-prepline list"><ul class="mt-preplist" aria-label="' + esc(p.drafts.length + ' preps are ready for this meeting') + '">' +
|
|
2410
2416
|
(p.note ? '<li class="mt-prepnote"><span class="mt-prepline-text mt-amber">' + esc(p.note) + '</span></li>' : '') +
|
|
2411
2417
|
p.drafts.map(function (d) {
|
|
2412
|
-
var label = (cleanTitle(d.title) || 'Prep') + (p.changed === d.id ? ' (changed)' : '') + ': ' + prepWhat(d).replace(/^its /, '');
|
|
2418
|
+
var label = (cleanTitle(d.title) || 'Prep') + (prepPendingBind(d, p.sid) ? ': finish adding the saved prep. Later edits are not included.' : (p.changed === d.id ? ' (changed)' : '') + ': ' + prepWhat(d).replace(/^its /, ''));
|
|
2413
2419
|
return '<li><span class="mt-prepline-text" title="' + esc(label) + '">' + esc(label) + '</span>' + prepAddBtn(p, d) + '</li>';
|
|
2414
2420
|
}).join('') + '</ul></div>';
|
|
2415
2421
|
}
|
|
@@ -2432,16 +2438,21 @@
|
|
|
2432
2438
|
// v1.1: the bind names the revision the offer described ({revision, sessionId, serverInstanceId, surface}), so a newer
|
|
2433
2439
|
// version nobody has seen is never attached. The server id is the newest one read: capabilities.prep or the most
|
|
2434
2440
|
// recent Prep answer (W.cosChat.prepInstance), else Meeting Ask's, which is the same id.
|
|
2435
|
-
function bindOnce(sid, d) {
|
|
2441
|
+
function bindOnce(sid, d, retry) {
|
|
2436
2442
|
var cc = W.cosChat, known = cc && typeof cc.prepInstance === 'function' ? cc.prepInstance() : '';
|
|
2437
2443
|
return (known ? Promise.resolve(known) : loadConfig().then(function (conf) { return conf && conf.serverInstanceId; })).then(function (inst) {
|
|
2438
2444
|
if (!inst) throw new Error(S.conf.notice || 'Meeting assistance is not available on this server.');
|
|
2439
2445
|
// Amendment 19: the header is what authorizes the write; the body carries the same id.
|
|
2440
|
-
return answer('/api/prep/drafts/' + encodeURIComponent(d.id) + '/bind', { method: 'POST', headers: { 'X-COS-Expected-Server-Instance': inst }, body: { revision: d.revision, sessionId: sid, serverInstanceId: inst, surface: 'web' } });
|
|
2446
|
+
return answer('/api/prep/drafts/' + encodeURIComponent(d.id) + '/bind', { method: 'POST', headers: { 'X-COS-Expected-Server-Instance': inst }, body: { revision: retry ? retry.revision : d.revision, sessionId: sid, serverInstanceId: inst, surface: 'web' } });
|
|
2441
2447
|
}).then(function (a) {
|
|
2442
2448
|
var b = a.body || {}, code = typeof b.error === 'string' ? b.error : (b.error && b.error.code) || b.reason || '';
|
|
2443
2449
|
if (a.status >= 200 && a.status < 300 || a.status === 409) notePrepInstance(b);
|
|
2444
|
-
if (a.status >= 200 && a.status < 300)
|
|
2450
|
+
if (a.status >= 200 && a.status < 300) {
|
|
2451
|
+
if (retry && !((b.draft && b.draft.bindings) || []).some(function (bound) {
|
|
2452
|
+
return bound && bound.sessionId === sid && bound.revision === retry.revision && bound.state === 'applied' && !bound.removedAt;
|
|
2453
|
+
})) throw new Error('The server has not confirmed adding this prep. Retry add prep.');
|
|
2454
|
+
return b;
|
|
2455
|
+
}
|
|
2445
2456
|
// A different server than the one that offered it: the prep is read again (amendment 19) and the line says so.
|
|
2446
2457
|
// Adding it takes another tap, against the server that answers now.
|
|
2447
2458
|
if (a.status === 409 && code === 'server_instance_mismatch') {
|
|
@@ -2469,7 +2480,7 @@
|
|
|
2469
2480
|
if (!f || !p || p.sid !== f.sid || p.busy || p.done || f.endedAt) return;
|
|
2470
2481
|
var d = p.drafts.filter(function (x) { return x.id === id; })[0]; if (!d) return;
|
|
2471
2482
|
p.busy = id; p.note = ''; paintLivePrep();
|
|
2472
|
-
bindOnce(f.sid, d).then(function (b) {
|
|
2483
|
+
bindOnce(f.sid, d, prepPendingBind(d, f.sid)).then(function (b) {
|
|
2473
2484
|
p.busy = ''; p.done = (b && b.draft) || d;
|
|
2474
2485
|
var a = A(f.sid);
|
|
2475
2486
|
// The meeting's files as the bind answered them, when it answered Meeting Ask files; and read again either way.
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
.mm-try { margin: 8px 0 2px; }
|
|
129
129
|
.mm-tries { margin: 0; padding: 0; list-style: none; display: grid; gap: 4px; }
|
|
130
130
|
.mm-tries .mm-link { font-size: 13.5px; overflow-wrap: anywhere; }
|
|
131
|
-
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; overflow-y: auto; }
|
|
131
|
+
.mm-answer { margin: 12px 0 8px; padding: 12px 14px; border-radius: 10px; background: var(--qopen); font-size: 14.5px; line-height: 1.55; overflow-wrap: anywhere; max-height: 60vh; max-height: 60dvh; overflow-y: auto; }
|
|
132
132
|
.mm-answer h4 { margin: 10px 0 4px; font: 600 14.5px/1.3 var(--body); }
|
|
133
133
|
.mm-answer p { margin: 0 0 8px; }
|
|
134
134
|
.mm-answer ul { margin: 0 0 8px; padding-left: 1.2em; }
|
|
@@ -109,19 +109,14 @@
|
|
|
109
109
|
// Copy stays in this browser: the same copy the rest of Control Web uses (plain-http fallback included).
|
|
110
110
|
if (op === 'copy') return W.copyText(String(a.text || '')).then(function () { return {}; });
|
|
111
111
|
var limit = READ_LIMIT[op] != null ? READ_LIMIT[op] : (isRead(op, a) ? 45000 : 0);
|
|
112
|
-
return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(
|
|
112
|
+
return W.api('/api/web-bridge/memories', { method: 'POST', body: { id: ++seq, op: op, args: a }, timeoutMs: limit }).then(function (r) {
|
|
113
113
|
if (r && r.ok) return r.details || {};
|
|
114
114
|
var raw = (r && r.message) || 'Your Mac did not answer that.';
|
|
115
115
|
var err = new Error(PLAIN[raw] || raw); err.code = raw; throw err;
|
|
116
116
|
});
|
|
117
117
|
}
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
var UNREACHABLE = 'Could not reach COS on your Mac. Check that the Mac is awake and on Tailscale, then try again.';
|
|
121
|
-
function plainNetError(err) {
|
|
122
|
-
if (err && !err.status && err.code !== 'timeout') { var e = new Error(UNREACHABLE); e.code = 'unreachable'; throw e; }
|
|
123
|
-
throw err;
|
|
124
|
-
}
|
|
118
|
+
// A request that never reached COS is said in plain words by W.api itself (app.js, 6.99.30: code 'unreachable'),
|
|
119
|
+
// as a timeout is: nothing to translate here.
|
|
125
120
|
function codeOf(e) { return String((e && (e.code || e.message)) || ''); }
|
|
126
121
|
|
|
127
122
|
// ── state ────────────────────────────────────────────────
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* COS Control Web: session questions and approvals (panes/questions.js), server 6.99.12. */
|
|
1
|
+
/* COS Control Web: session questions and approvals (panes/questions.js), from server 6.99.12. */
|
|
2
2
|
.sq-host { display: grid; gap: 12px; margin: 0 0 16px; }
|
|
3
3
|
.sq-host[hidden] { display: none; }
|
|
4
4
|
.sq-card { background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); border-radius: 16px; padding: 14px 16px 12px; min-width: 0; }
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
.sq-title { margin: 0; font: 600 16px/1.3 var(--body); color: var(--ink); overflow-wrap: anywhere; }
|
|
7
7
|
.sq-secs { font: 500 12.5px/1.3 var(--body); color: var(--hot); white-space: nowrap; }
|
|
8
8
|
.sq-q { border: 0; margin: 12px 0 0; padding: 0; min-width: 0; }
|
|
9
|
-
.sq-q-text { padding: 0; font:
|
|
9
|
+
.sq-q-text { padding: 0; font: 700 15.5px/1.4 var(--body); color: var(--ink); overflow-wrap: anywhere; }
|
|
10
10
|
.sq-q-tag { font-size: 12.5px; font-weight: 500; color: var(--tertiary); }
|
|
11
11
|
.sq-fine { margin: 2px 0 0; font-size: 12.5px; color: var(--tertiary); }
|
|
12
12
|
.sq-options { display: grid; gap: 2px; margin-top: 8px; }
|
|
@@ -27,6 +27,60 @@
|
|
|
27
27
|
.sq-leave { min-height: 44px; font-size: 14px; font-weight: 500; color: var(--ink); }
|
|
28
28
|
.sq-note { margin: 8px 0 0; font-size: 13px; color: var(--hot); }
|
|
29
29
|
.sq-note.hidden { display: none; }
|
|
30
|
+
/* 6.99.26: read like Claude Code's question in the terminal (Miles 11/07): the strip, numbered options, Other as the
|
|
31
|
+
last number, the answers to review, Submit. */
|
|
32
|
+
.sq-steps { display: flex; flex-wrap: wrap; gap: 0 14px; margin: 8px 0 0; padding-bottom: 6px; border-bottom: 1px solid var(--row-border); }
|
|
33
|
+
.sq-step { min-height: 36px; padding: 0; font: 500 13.5px/1.2 var(--mono); color: var(--sub); white-space: nowrap; }
|
|
34
|
+
.sq-step:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
|
|
35
|
+
.sq-num { flex: none; min-width: 1.6em; font: 500 14px/1.45 var(--mono); color: var(--sub); }
|
|
36
|
+
.sq-option-other { align-items: center; cursor: text; }
|
|
37
|
+
.sq-option-other .sq-other { flex: 1; width: auto; min-width: 0; margin-top: 0; }
|
|
38
|
+
.sq-review { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: var(--qopen); }
|
|
39
|
+
.sq-review-head { margin: 0 0 4px; font: 600 13px/1.3 var(--body); color: var(--sub); }
|
|
40
|
+
.sq-review-list { margin: 0; padding-left: 1.4em; display: grid; gap: 4px; font-size: 14px; line-height: 1.4; }
|
|
41
|
+
.sq-review-q { font-weight: 600; color: var(--ink); margin-right: 8px; }
|
|
42
|
+
.sq-review-a { color: var(--ink); overflow-wrap: anywhere; }
|
|
43
|
+
.sq-review-a.empty { color: var(--tertiary); }
|
|
44
|
+
/* 6.99.27 (Miles's phone test): what each question needs, and a footer that keeps the progress and the answer button
|
|
45
|
+
on screen while the card is, at every Duo size. Questions clear the top bar and the footer when scrolled to. */
|
|
46
|
+
.sq-need { margin: 2px 0 0; font-size: 13px; line-height: 1.4; }
|
|
47
|
+
.sq-need[data-state="todo"] { color: var(--hot); font-weight: 600; }
|
|
48
|
+
.sq-need[data-state="done"] { color: var(--tertiary); }
|
|
49
|
+
.sq-q { scroll-margin-top: calc(var(--topbar-h, 48px) + var(--safe-top, 0px) + 12px); scroll-margin-bottom: 150px; }
|
|
50
|
+
.ta-q .sq-q { scroll-margin-top: 8px; }
|
|
51
|
+
.sq-review { scroll-margin-bottom: 150px; }
|
|
52
|
+
.sq-foot { position: sticky; bottom: var(--safe-bottom, 0px); z-index: 2; margin: 12px -16px -12px; padding: 10px 16px 12px; background: var(--panel);
|
|
53
|
+
border-top: 1px solid var(--row-border); border-radius: 0 0 16px 16px; }
|
|
54
|
+
.sq-foot .sq-acts { margin-top: 0; }
|
|
55
|
+
/* QA 6.99.27: on the page, the footer rides above whatever covers the bottom (questions.js sets --sq-kb from app.js
|
|
56
|
+
viewportFit: a keyboard, its strip, a browser's bottom bar). In Today's sheet the layer is itself the visible part of
|
|
57
|
+
the page (today-actions.js applyFit), so the footer sits at the bottom of the sheet with no gap. */
|
|
58
|
+
.sq-host .sq-foot { bottom: max(var(--safe-bottom, 0px), var(--sq-kb, 0px)); }
|
|
59
|
+
.ta-q .sq-foot { bottom: 0; }
|
|
60
|
+
/* 6.99.30 (canary): with a keyboard up on the page (questions.js sets sq-kb-up) the footer scrolls with the card, so
|
|
61
|
+
the options it would cover stay readable. */
|
|
62
|
+
.sq-host.sq-kb-up .sq-foot { position: static; }
|
|
63
|
+
/* The Mac not answering: Submit, Allow and Deny wait, and say why (questions.js offline). */
|
|
64
|
+
.sq-offline { margin: 8px 0 0; font-size: 13px; color: var(--sub); }
|
|
65
|
+
/* 6.99.30 (canary): folded, a waiting question is one line over the pane (it filled the first screen of every pane);
|
|
66
|
+
a tap opens the cards. From 600 px the cards show as before and the line does not. */
|
|
67
|
+
.sq-banner { display: none; }
|
|
68
|
+
@media (max-width: 599px) {
|
|
69
|
+
.sq-host .sq-banner { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 10px 14px; border-radius: 14px;
|
|
70
|
+
background: var(--panel); border: 1px solid color-mix(in srgb, var(--hot) 45%, var(--panel-border)); color: var(--ink); font: 600 15px/1.3 var(--body); text-align: left; }
|
|
71
|
+
.sq-host .sq-banner-text { flex: 1; min-width: 0; }
|
|
72
|
+
.sq-host .sq-banner-act { flex: none; color: var(--accent); }
|
|
73
|
+
.sq-host .sq-cards:not(.open) { display: none; }
|
|
74
|
+
}
|
|
75
|
+
.sq-cards { display: grid; gap: 12px; }
|
|
76
|
+
.sq-progress { margin: 0 0 8px; font: 500 13.5px/1.35 var(--body); color: var(--sub); }
|
|
77
|
+
.sq-progress[data-alert] { color: var(--hot); font-weight: 600; }
|
|
78
|
+
.sq-keys { display: none; margin-top: 10px; }
|
|
79
|
+
@media (hover: hover) and (pointer: fine) { .sq-keys { display: block; } }
|
|
80
|
+
/* 6.99.26: a question the Mac's own prompt waits on. Read-only, so it does not take the waiting colour. */
|
|
81
|
+
.sq-card.sq-mac { border-color: var(--panel-border); }
|
|
82
|
+
.sq-mac-q { margin: 10px 0 0; }
|
|
83
|
+
.sq-mac-line { margin: 10px 0 0; font-size: 13.5px; line-height: 1.4; color: var(--sub); }
|
|
30
84
|
@media (max-width: 480px) {
|
|
31
85
|
.sq-acts .cx-btn { flex: 1; justify-content: center; }
|
|
32
86
|
}
|