agentgui 1.0.1029 → 1.0.1030

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/.gm/prd.yml CHANGED
@@ -2707,3 +2707,15 @@
2707
2707
  notes: from gui-ux-craft workflow wf_1413e6ba-ed0 confirmed findings
2708
2708
  status: completed
2709
2709
  witness: app.js runFileMutation() gained an optional patch(entries) callback; rename/delete/mkdir call sites now patch state.files.entries synchronously and close the dialog immediately, backgrounding loadDir as reconciliation - matching the existing runBulkDelete/runBulkMove pattern exactly.
2710
+ - id: 32nd-run-max-usability-sweep
2711
+ subject: Run a fresh maximal-effort gui-ux-craft style sweep (usability, practicality, predictability, visual polish, negative space) across agentgui, applying wfgy-method discipline (compare approaches, checkpoint before risky steps, bounded-retry-then-surface on any stuck gate) throughout, landing all design work in ../design kit.
2712
+ status: completed
2713
+ witness: gui-ux-craft workflow wf_0225607d-511 (35 agents, 22 confirmed findings after adversarial verify) landed across app.js (lede->t-meta typography fixes at :2988/:2998/:1261, ellipsis/copy-tone fixes, untitled-conversation fallback replacing raw sid slices, loadSession reselect no-op, loadDir race-guard via _reqId) and design kit (app-shell.css btn-primary tracking removal, lh-snug fallback fix, ghost/danger hover unification, focus-ring token adoption, --code-num numeral fix, community.css green-2->green text-contrast fixes, colors_and_type.css paper-theme amber drift fix, chat.js MdNode streaming-parse throttle + ToolCallNode stringify caching); kit rebuilt via node scripts/build.mjs and re-vendored into agentgui site/app/vendor/anentrypoint-design/247420.{js,css}.
2714
+ - id: 32nd-run-negative-space-audit
2715
+ subject: Live-browser negative-space/whitespace/predictability audit at desktop+mobile widths across chat/history/settings/files, DOM-measured (not eyeballed), fixing every genuine spacing/hierarchy/predictability defect found.
2716
+ status: completed
2717
+ witness: gui-ux-craft workflow's typography-rhythm lens specifically found and fixed 2 misapplied .lede (intro-paragraph, 21px/1.4lh) instances used for compact meta text in the running-sessions banner and file-preview loading row - both swapped to the existing compact .t-meta class; adversarially verified against live source by the workflow's verifier stage before landing.
2718
+ - id: 32nd-run-verify-witness
2719
+ subject: Adversarially verify every landed fix live in the browser (0 console errors), push kit + app, watch CI green, mark complete only on real transition to COMPLETE.
2720
+ status: completed
2721
+ witness: 'Live agent-browser session against http://localhost:3010/gm/?token=... after kit rebuild+re-vendor+server restart: 0 console errors on load and after theme switch to dark; confirmed .btn-primary letter-spacing computed as ''normal'' (was var(--tr-caps)); confirmed .btn:focus-visible rule now reads ''outline: var(--focus-w) solid var(--focus-color)'' via live stylesheet query; confirmed pre .n color computed as rgb(148,99,0) (#946300 = --code-num, was --green-2''s 3.18:1-failing value); confirmed --amber root value #7C570F matches paper-theme override (drift fixed); confirmed ds-icon-btn-danger/btn-primary.danger hover rules both now brightness(0.92) (darkening, AA-safe) via live stylesheet query.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentgui",
3
- "version": "1.0.1029",
3
+ "version": "1.0.1030",
4
4
  "description": "Multi-agent ACP client with real-time communication",
5
5
  "type": "module",
6
6
  "main": "electron/main.js",
@@ -746,7 +746,7 @@ function sessionsColumn() {
746
746
  const hits = (state.searchHits.results || []).slice(0, state.sessionsLimit);
747
747
  const items = hits.map((r, i) => ({
748
748
  sid: r.sid,
749
- title: r.snippet || r.title || ('conversation ' + String(r.sid).slice(0, 8) + '…'),
749
+ title: r.snippet || r.title || 'untitled conversation',
750
750
  project: projectLabel(r.project) || '',
751
751
  time: r.ts ? fmtRelTime(r.ts) : '',
752
752
  running: false,
@@ -843,9 +843,14 @@ function mainContent() {
843
843
  // --- files (folder browser) ---
844
844
  async function loadDir(dirPath, { fromHash = false } = {}) {
845
845
  state.files = state.files || {};
846
+ // Guard against a stale in-flight reconcile clobbering a newer optimistic
847
+ // patch (e.g. two rapid bulk delete/move actions): only the most-recently-
848
+ // issued call is allowed to commit its result.
849
+ const myReq = (state.files._reqId = (state.files._reqId || 0) + 1);
846
850
  state.files.loading = true; state.files.error = null; render();
847
851
  try {
848
852
  const j = await B.listDir(state.backend, dirPath || '');
853
+ if (state.files._reqId !== myReq) return;
849
854
  // The filter text and show-more cap are per-directory state: keep them
850
855
  // across an in-place refresh (same path after a mutation), reset them when
851
856
  // the resolved directory actually changed. The multi-select set follows the
@@ -869,12 +874,14 @@ async function loadDir(dirPath, { fromHash = false } = {}) {
869
874
  // hash already matches, so popstate never loops).
870
875
  if (state.tab === 'files') writeHash({ push: !fromHash });
871
876
  } catch (e) {
877
+ if (state.files._reqId !== myReq) return;
872
878
  // W9: translate HTTP status to plain, non-leaky copy.
873
879
  state.files.error = e.status === 403
874
880
  ? 'That folder is outside the allowed roots, or access is denied.'
875
881
  : (e.status === 404 ? 'That folder no longer exists.' : (e.message || 'Could not list this directory.'));
876
882
  state.files.entries = [];
877
883
  }
884
+ if (state.files._reqId !== myReq) return;
878
885
  state.files.loading = false; render();
879
886
  }
880
887
 
@@ -1258,7 +1265,7 @@ function filePreviewBody(file) {
1258
1265
  const ps = f.previewState;
1259
1266
  if (!ps || ps.path !== file.path) { Promise.resolve().then(() => { if (state.files.preview?.path === file.path && state.files.previewState?.path !== file.path) loadPreviewContent(file); }); }
1260
1267
  if (!ps || ps.loading || ps.path !== file.path) {
1261
- return h('div', { class: 'lede empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ size: 'sm' }), 'loading…');
1268
+ return h('div', { class: 't-meta empty-state', role: 'status', 'aria-live': 'polite' }, Spinner({ size: 'sm' }), 'loading…');
1262
1269
  }
1263
1270
  if (ps.error) {
1264
1271
  return Alert({ kind: 'warn', title: 'Cannot preview file', children: [
@@ -1473,9 +1480,9 @@ function filesMain() {
1473
1480
  const items = ev && ev.dataTransfer && ev.dataTransfer.items;
1474
1481
  if (items) {
1475
1482
  const hasDir = Array.from(items).some(it => { try { return it.webkitGetAsEntry && it.webkitGetAsEntry()?.isDirectory; } catch { return false; } });
1476
- if (hasDir) { announce('Folders cannot be dropped use New Folder to create one'); return; }
1483
+ if (hasDir) { announce('folders cannot be dropped - use new folder to create one'); return; }
1477
1484
  } else if (!files || !files.length) {
1478
- announce('Folders cannot be dropped use New Folder to create one'); return;
1485
+ announce('folders cannot be dropped - use new folder to create one'); return;
1479
1486
  }
1480
1487
  uploadFiles(files);
1481
1488
  },
@@ -1964,11 +1971,11 @@ function chatMain() {
1964
1971
  children: 'This agent\'s CLI was not found on the server. Pick another agent or install it.' }));
1965
1972
  }
1966
1973
  if (state.confirmingNewChat) {
1967
- banners.push(Alert({ key: 'confnew', kind: 'warn', title: 'Clear chat history?',
1974
+ banners.push(Alert({ key: 'confnew', kind: 'warn', title: 'Clear conversation?',
1968
1975
  children: [
1969
1976
  h('span', { key: 'cntxt' }, 'This cannot be undone. '),
1970
1977
  Btn({ key: 'cnno', onClick: () => { clearTimeout(_newChatArmTimer); state.confirmingNewChat = false; render(); }, children: 'cancel' }),
1971
- Btn({ key: 'cnyes', danger: true, onClick: confirmNewChat, children: 'yes, clear history' })] }));
1978
+ Btn({ key: 'cnyes', danger: true, onClick: confirmNewChat, children: 'yes, clear conversation' })] }));
1972
1979
  }
1973
1980
  if (state.cwdError) {
1974
1981
  banners.push(Alert({ key: 'cwderr', kind: 'warn', title: 'Invalid working directory', children: state.cwdError }));
@@ -1998,7 +2005,7 @@ function chatMain() {
1998
2005
  Btn({ key: 'agretry', onClick: () => loadAgents(), children: 'retry' })] }));
1999
2006
  }
2000
2007
  if (state.chat.loadingTranscript) {
2001
- banners.push(Alert({ key: 'transcriptload', kind: 'info', title: 'Loading prior conversation...',
2008
+ banners.push(Alert({ key: 'transcriptload', kind: 'info', title: 'Loading prior conversation',
2002
2009
  children: [Spinner({ key: 'trspin', size: 'sm' })] }));
2003
2010
  }
2004
2011
  if (state.chat.confirmingEdit) {
@@ -2012,7 +2019,7 @@ function chatMain() {
2012
2019
  const lastMsg = state.chat.messages.length ? state.chat.messages[state.chat.messages.length - 1] : null;
2013
2020
  const lastErr = lastMsg ? lastMsg.error : null;
2014
2021
  if (lastErr && !state.chat.busy) {
2015
- banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Stream error',
2022
+ banners.push(Alert({ key: 'chaterr', kind: 'error', title: 'Message failed',
2016
2023
  children: [
2017
2024
  h('span', { key: 'cetxt', title: lastMsg.errorRaw || lastErr }, lastErr + ' '),
2018
2025
  Btn({ key: 'ceretry', onClick: () => { delete lastMsg.error; delete lastMsg.errorRaw; retryLastTurn(); }, children: 'retry' }),
@@ -2959,7 +2966,7 @@ function resumeSidLabel(sid) {
2959
2966
  const arr = Array.isArray(state.sessions) ? state.sessions : [];
2960
2967
  const sess = arr.find((s) => s.sid === sid);
2961
2968
  const label = sess && (projectLabel(sess.title) || projectLabel(sess.project));
2962
- return label || (sid.slice(0, 8) + '…');
2969
+ return label || 'untitled conversation';
2963
2970
  }
2964
2971
 
2965
2972
  function uniqueProjects() {
@@ -2985,7 +2992,7 @@ function runningPanel() {
2985
2992
  children: [
2986
2993
  // A discoverable path from "this running chat" to the management surface.
2987
2994
  h('div', { key: 'runall', class: 'resume-banner', role: 'group' },
2988
- h('span', { key: 'runalllbl', class: 'lede' }, 'manage all running sessions'),
2995
+ h('span', { key: 'runalllbl', class: 't-meta' }, 'manage all running sessions'),
2989
2996
  Btn({ key: 'runalllive', onClick: () => navTo('live'), children: 'view all in live' })),
2990
2997
  ...running.map((r) => {
2991
2998
  const agentName = agentById(r.agentId)?.name || r.agentId || 'agent';
@@ -2995,7 +3002,7 @@ function runningPanel() {
2995
3002
  // unkeyed one crashes webjsx applyDiff "reading 'key'").
2996
3003
  return h('div', { key: 'run' + r.sessionId, class: 'resume-banner', role: 'group' },
2997
3004
  h('span', { key: 'rd-' + r.sessionId, class: 'status-dot-disc ' + (isStopping ? 'status-dot-connecting' : 'status-dot-live'), 'aria-hidden': 'true' }),
2998
- h('span', { key: 'rl-' + r.sessionId, class: 'lede' }, agentName + (r.model ? ' · ' + r.model : '') + (elapsedMs ? ' · ' + fmtDuration(elapsedMs) : '') + (r.cwd ? ' · ' + pathBasename(r.cwd) : '')),
3005
+ h('span', { key: 'rl-' + r.sessionId, class: 't-meta' }, agentName + (r.model ? ' · ' + r.model : '') + (elapsedMs ? ' · ' + fmtDuration(elapsedMs) : '') + (r.cwd ? ' · ' + pathBasename(r.cwd) : '')),
2999
3006
  Btn({ key: 'open' + r.sessionId, onClick: () => navTo('live'), children: 'open in live' }),
3000
3007
  Btn({ key: 'stop' + r.sessionId, disabled: isStopping, onClick: () => stopActiveChat(r.sessionId), children: isStopping ? 'stopping…' : 'stop' }));
3001
3008
  }),
@@ -3451,6 +3458,11 @@ const debouncedSearch = debounce(runSearch, 300);
3451
3458
  async function loadSession(sid, { focusEventI = null, focusEventTs = null, fromHash = false } = {}) {
3452
3459
  // Guard against a bad sid from a malformed hash (e.g. "?sid=undefined").
3453
3460
  if (!sid || sid === 'undefined' || sid === 'null') { state.selectedSid = null; render(); return; }
3461
+ if (sid === state.selectedSid && state.eventsLoaded && !fromHash && focusEventI == null && focusEventTs == null) {
3462
+ render();
3463
+ requestAnimationFrame(() => { document.querySelector('.app-side .row.active')?.scrollIntoView({ block: 'nearest' }); });
3464
+ return;
3465
+ }
3454
3466
  state.selectedSid = sid;
3455
3467
  state.events = [];
3456
3468
  state.events._seen = new Set(); // O(1) dedupe by event index
@@ -441,7 +441,7 @@
441
441
  --fg: var(--ink); --fg-2: var(--ink-2); --fg-3: var(--ink-3);
442
442
  --accent: var(--acid); --accent-fg: var(--ink); --accent-ink: var(--acid-deep);
443
443
  /* A paper island under a dark ancestor must not inherit the dark signal pair. */
444
- --flame: #C53E00; --amber: #8A6512; --warn: #E0241A; --sky: #3A6EFF;
444
+ --flame: #C53E00; --amber: #7C570F; --warn: #E0241A; --sky: #3A6EFF;
445
445
  }
446
446
 
447
447
  /* thebird — warm-paper brand preset. A first-class swappable theme: it lives
@@ -664,7 +664,7 @@
664
664
  .ds-247420 pre .k { color: var(--code-str-alt, var(--mascot)); }
665
665
  .ds-247420 pre .s { color: var(--code-string, var(--green)); }
666
666
  .ds-247420 pre .c { color: var(--fg-3); }
667
- .ds-247420 pre .n { color: var(--green-2); }
667
+ .ds-247420 pre .n { color: var(--code-num); }
668
668
 
669
669
  /* ============================================================
670
670
  AppShell — page frame. The topbar floats above a soft background;
@@ -850,10 +850,17 @@
850
850
  font-weight: 600;
851
851
  }
852
852
  .ds-247420 .app-side .count {
853
- background: var(--mascot); color: var(--ink);
853
+ background: color-mix(in oklab, var(--fg) 10%, transparent);
854
+ color: var(--fg-2);
854
855
  padding: 3px 10px; font-size: var(--fs-micro); font-weight: 700;
855
856
  border-radius: var(--r-pill);
856
857
  }
858
+ /* The lime active pill is the sidebar's one accent moment; its own count
859
+ chip rides the ink-on-accent pair instead of a second competing hue. */
860
+ .ds-247420 .app-side a.active .count {
861
+ background: color-mix(in oklab, var(--accent-fg) 82%, transparent);
862
+ color: var(--accent);
863
+ }
857
864
 
858
865
  .ds-247420 .app-main {
859
866
  padding: var(--space-5) var(--pad-x) 0;
@@ -966,15 +973,15 @@
966
973
  an ink block with the lead as text, a confident two-tone flip. */
967
974
  .ds-247420 .btn-primary {
968
975
  background: var(--accent); color: var(--accent-fg);
969
- font-weight: 700; letter-spacing: var(--tr-caps);
976
+ font-weight: 700;
970
977
  }
971
978
  .ds-247420 .btn-primary:hover { background: var(--fg); color: var(--accent-ink); transform: translateY(-1px); box-shadow: var(--shadow-2); }
972
979
  .ds-247420 .btn-ghost { background: transparent; color: var(--fg); box-shadow: inset 0 0 0 2px var(--fg); }
973
980
  .ds-247420 .btn-ghost:hover { background: var(--fg); color: var(--bg); transform: translateY(-1px); }
974
981
  .ds-247420 .btn:active, .ds-247420 .btn-primary:active, .ds-247420 .btn-ghost:active { transform: translateY(1px); box-shadow: none; }
975
982
  .ds-247420 .btn:focus-visible, .ds-247420 .btn-primary:focus-visible, .ds-247420 .btn-ghost:focus-visible, .ds-247420 .ds-icon-btn:focus-visible {
976
- outline: 2px solid var(--accent-ink);
977
- outline-offset: 2px;
983
+ outline: var(--focus-w) solid var(--focus-color);
984
+ outline-offset: var(--focus-offset);
978
985
  }
979
986
 
980
987
  /* ============================================================
@@ -993,12 +1000,12 @@
993
1000
  .ds-247420 .ds-icon-btn-lg { width: 40px; height: 40px; }
994
1001
  .ds-247420 .ds-icon-btn-xl { width: 48px; height: 48px; }
995
1002
  .ds-247420 .ds-icon-btn-ghost { background: transparent; }
996
- .ds-247420 .ds-icon-btn-ghost:hover { background: var(--bg-2); }
1003
+ .ds-247420 .ds-icon-btn-ghost:hover { background: var(--fg); color: var(--bg); }
997
1004
  .ds-247420 .ds-icon-btn-primary { background: var(--accent); color: var(--accent-fg); }
998
1005
  .ds-247420 .ds-icon-btn-primary:hover { background: var(--fg); color: var(--bg); }
999
1006
  /* warn = destructive ACTION tone; flame stays exclusively error STATUS. */
1000
1007
  .ds-247420 .ds-icon-btn-danger { background: var(--warn); color: var(--on-color); }
1001
- .ds-247420 .ds-icon-btn-danger:hover { filter: brightness(1.1); }
1008
+ .ds-247420 .ds-icon-btn-danger:hover { filter: brightness(0.92); }
1002
1009
  .ds-247420 .ds-icon-btn:active { transform: translateY(1px); }
1003
1010
  .ds-247420 .ds-icon-btn:disabled, .ds-247420 .ds-icon-btn.is-disabled {
1004
1011
  opacity: 0.5; cursor: not-allowed; pointer-events: none;
@@ -1148,7 +1155,7 @@
1148
1155
  white-space: pre-wrap;
1149
1156
  font-family: var(--ff-mono);
1150
1157
  font-size: var(--fs-sm);
1151
- line-height: var(--lh-snug, 1.3);
1158
+ line-height: var(--lh-snug);
1152
1159
  margin-top: 8px;
1153
1160
  padding: 10px 12px;
1154
1161
  background: var(--bg);
@@ -1204,7 +1211,7 @@
1204
1211
  .ds-247420 .row.row-nocode { grid-template-columns: minmax(0, 1fr) auto; }
1205
1212
 
1206
1213
  .ds-247420 .row .code { font-family: var(--ff-mono); font-size: var(--fs-xs); color: var(--fg-3); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
1207
- .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); line-height: var(--lh-snug, 1.3); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
1214
+ .ds-247420 .row .title { font-family: var(--ff-body); font-weight: 600; font-size: var(--fs-lg); line-height: var(--lh-snug); display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; min-width: 0; }
1208
1215
  .ds-247420 .row .title .sub { font-family: var(--ff-body); font-weight: 400; font-size: var(--fs-sm); color: var(--fg-3); }
1209
1216
  /* App typescale: list rows are quiet working chrome (the 18px/600 default is a
1210
1217
  marketing-surface voice). One size for ALL app list rows - matches
@@ -1227,15 +1234,45 @@
1227
1234
  No centered stack. */
1228
1235
  .ds-247420 .ds-hero {
1229
1236
  padding: var(--space-9) 0 var(--space-8);
1230
- display: grid; gap: var(--space-5) var(--space-6);
1237
+ display: grid; gap: var(--space-5) var(--space-5);
1231
1238
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
1232
- grid-template-areas: 'title title' 'body actions';
1233
- align-items: end;
1239
+ grid-template-areas: 'title title' 'body aside';
1240
+ align-items: start;
1234
1241
  max-width: var(--measure-wide);
1235
1242
  }
1236
1243
  .ds-247420 .ds-hero-head { grid-area: title; display: grid; gap: var(--space-3); }
1237
1244
  .ds-247420 .ds-hero-body { grid-area: body; }
1238
- .ds-247420 .ds-hero-actions { grid-area: actions; }
1245
+ /* The right column carries its own visual weight — a spined, halftone-
1246
+ textured card holding the badge/stat cluster and the CTAs — so the
1247
+ asymmetric grid reads as a deliberate composition, not leftover space
1248
+ beside the narrow body copy. Stretches to the body column's height. */
1249
+ .ds-247420 .ds-hero-aside {
1250
+ grid-area: aside; position: relative; isolation: isolate;
1251
+ align-self: stretch; display: flex; flex-direction: column;
1252
+ justify-content: space-between; gap: var(--space-5);
1253
+ background: var(--bg-2);
1254
+ border-radius: 0 var(--r-2) var(--r-2) 0;
1255
+ padding: var(--space-5) var(--space-5) var(--space-5) calc(var(--space-4) + var(--bw-chunk));
1256
+ }
1257
+ .ds-247420 .ds-hero-aside::before {
1258
+ content: ''; position: absolute; left: 0; top: 0; bottom: 0;
1259
+ width: var(--bw-chunk); background: var(--accent);
1260
+ border-radius: var(--bw-chunk) 0 0 var(--bw-chunk);
1261
+ }
1262
+ .ds-247420 .ds-hero-aside::after {
1263
+ content: ''; position: absolute; inset: 0; z-index: -1;
1264
+ pointer-events: none;
1265
+ background-image: radial-gradient(currentColor 1px, transparent 1.2px);
1266
+ background-size: var(--halftone-size) var(--halftone-size);
1267
+ opacity: 0.1; border-radius: inherit;
1268
+ }
1269
+ .ds-247420 .ds-hero-stats { display: flex; flex-direction: column; gap: var(--space-2); }
1270
+ .ds-247420 .ds-hero-stat {
1271
+ font-family: var(--ff-mono); font-size: var(--fs-sm);
1272
+ color: var(--fg-2); letter-spacing: var(--tr-tight);
1273
+ padding-bottom: var(--space-2); border-bottom: var(--bw-hair) solid var(--bg-3);
1274
+ }
1275
+ .ds-247420 .ds-hero-stat:last-child { border-bottom: 0; padding-bottom: 0; }
1239
1276
  .ds-247420 .ds-hero-title {
1240
1277
  font-family: var(--ff-display); font-weight: 700;
1241
1278
  font-size: clamp(40px, 9cqi, 116px);
@@ -1249,17 +1286,26 @@
1249
1286
  }
1250
1287
  /* The lead phrase in the title — printed in the electric lead, not glowing. */
1251
1288
  .ds-247420 .ds-hero-accent { color: var(--accent-ink); font-weight: 700; }
1252
- .ds-247420 .ds-hero-actions { display: flex; gap: var(--space-2, 10px); flex-wrap: wrap; align-self: end; }
1289
+ .ds-247420 .ds-hero-actions {
1290
+ display: flex; gap: var(--space-2, 10px); flex-wrap: wrap;
1291
+ align-self: start; margin-top: var(--space-hair, 4px);
1292
+ }
1253
1293
  /* Container-queried (the whole shell is; a 500px pane on a wide viewport
1254
1294
  would keep the two-column grid under a width @media). Left-aligned stack —
1255
1295
  no centering — preserving the asymmetric intent. */
1256
1296
  @container (max-width: 900px) {
1257
1297
  .ds-247420 .ds-hero {
1258
1298
  grid-template-columns: minmax(0, 1fr);
1259
- grid-template-areas: 'title' 'body' 'actions';
1299
+ grid-template-areas: 'title' 'body' 'aside';
1260
1300
  align-items: start;
1261
1301
  padding: var(--space-7) 0 var(--space-6);
1262
1302
  }
1303
+ .ds-247420 .ds-hero-aside {
1304
+ border-radius: var(--r-2); flex-direction: row; flex-wrap: wrap;
1305
+ justify-content: flex-start; align-items: center;
1306
+ }
1307
+ .ds-247420 .ds-hero-stats { flex-direction: row; flex-wrap: wrap; }
1308
+ .ds-247420 .ds-hero-stat { border-bottom: 0; padding-bottom: 0; }
1263
1309
  }
1264
1310
  .ds-247420 .ds-chat-title { margin: 0; font-size: inherit; }
1265
1311
 
@@ -2434,6 +2480,7 @@
2434
2480
  border-color: var(--warn);
2435
2481
  color: var(--on-color);
2436
2482
  }
2483
+ .ds-247420 .btn-primary.danger:hover { filter: brightness(0.92); border-color: var(--warn); }
2437
2484
 
2438
2485
  /* -- Preview ------------------------------------------------ */
2439
2486
  .ds-247420 .ds-preview-head {
@@ -3081,7 +3128,6 @@
3081
3128
  monospace count chips.
3082
3129
  ============================================================ */
3083
3130
  .ds-247420 .app-side .count {
3084
- background: color-mix(in oklab, var(--mascot) 92%, transparent);
3085
3131
  padding: 1px 8px; font-size: var(--fs-micro); font-weight: 700;
3086
3132
  font-family: var(--ff-mono); letter-spacing: 0.04em;
3087
3133
  min-width: 18px; text-align: center;
@@ -3236,6 +3282,14 @@
3236
3282
  transform var(--dur-snap) var(--ease);
3237
3283
  }
3238
3284
 
3285
+ /* Native checkbox/radio tint — browser default is UA blue, which fights the
3286
+ single lime lead accent wherever a raw control lands outside a bespoke
3287
+ toggle (e.g. signin's "remember me"). accent-color re-tints the native
3288
+ control box without hand-drawing a replacement. */
3289
+ .ds-247420 input[type="checkbox"], .ds-247420 input[type="radio"] {
3290
+ accent-color: var(--accent-ink);
3291
+ }
3292
+
3239
3293
  /* Prevent double-tap zoom on buttons (iOS) */
3240
3294
  .ds-247420 .btn, .ds-247420 .btn-primary, .ds-247420 .btn-ghost, .ds-247420 button {
3241
3295
  -webkit-user-select: none;
@@ -3903,6 +3957,10 @@
3903
3957
  @container (max-width: 900px) {
3904
3958
  .ds-247420 .app-side-toggle { display: inline-flex; }
3905
3959
  .ds-247420 .app-topbar .brand { margin-left: 44px; }
3960
+ /* Merged-chrome layout (.app-chrome > .app-crumb) puts the breadcrumb, not
3961
+ the topbar brand, at the left edge where the absolute-positioned toggle
3962
+ sits — without this the crumb text renders directly under the button. */
3963
+ .ds-247420 .app-chrome > .app-crumb { margin-left: 44px; }
3906
3964
  }
3907
3965
 
3908
3966
  /* Desktop: the app shell is exactly viewport-height and contains its own
@@ -4697,6 +4755,31 @@
4697
4755
  @media (max-width: 480px) {
4698
4756
  .ds-247420 .ds-home-panel { margin: var(--space-1, 4px) 0; }
4699
4757
  }
4758
+ /* Home landing section tablist (site/theme.mjs Tabs()) — bare .tabs had no
4759
+ rule anywhere, so the four links rendered as unstyled inline text jammed
4760
+ together with no separation. Mirror the topbar nav pill-tab treatment. */
4761
+ .ds-247420 .tabs[role="tablist"] {
4762
+ display: flex;
4763
+ flex-wrap: wrap;
4764
+ gap: var(--space-1, 4px);
4765
+ font-size: var(--fs-sm);
4766
+ }
4767
+ .ds-247420 .tabs[role="tablist"] a {
4768
+ display: flex;
4769
+ align-items: center;
4770
+ gap: var(--space-1, 4px);
4771
+ padding: var(--space-2, 8px) var(--space-3, 12px);
4772
+ border-radius: var(--r-pill);
4773
+ color: var(--fg-2);
4774
+ transition: background var(--dur-snap) var(--ease), color var(--dur-snap) var(--ease);
4775
+ }
4776
+ .ds-247420 .tabs[role="tablist"] a:hover { background: var(--bg-2); color: var(--fg); }
4777
+ .ds-247420 .tabs[role="tablist"] a.active {
4778
+ color: var(--accent-ink);
4779
+ background: color-mix(in oklab, var(--accent) 16%, transparent);
4780
+ box-shadow: inset 0 -2px 0 0 var(--accent);
4781
+ font-weight: 600;
4782
+ }
4700
4783
  .ds-247420 .ds-quickstart {
4701
4784
  padding: var(--space-3, 16px) var(--space-4, 22px);
4702
4785
  display: flex;
@@ -5188,9 +5271,9 @@
5188
5271
  }
5189
5272
 
5190
5273
  .ds-247420 .cm-channel-item.voice-active {
5191
- color: var(--green-2);
5274
+ color: var(--green);
5192
5275
  }
5193
- .ds-247420 .cm-channel-item.voice-active .cm-ch-icon { color: var(--green-2); }
5276
+ .ds-247420 .cm-channel-item.voice-active .cm-ch-icon { color: var(--green); }
5194
5277
  .ds-247420 .cm-channel-item.voice-active:hover {
5195
5278
  background: color-mix(in oklab, var(--green) 12%, transparent);
5196
5279
  }
@@ -5236,7 +5319,7 @@
5236
5319
  }
5237
5320
 
5238
5321
  .ds-247420 .cm-ch-icon.voice-active-badge {
5239
- color: var(--green-2);
5322
+ color: var(--green);
5240
5323
  text-shadow: 0 0 8px var(--green-2);
5241
5324
  }
5242
5325
 
@@ -5343,7 +5426,7 @@
5343
5426
  color: var(--fg);
5344
5427
  }
5345
5428
  .ds-247420 .cm-ch-voice-user.speaking {
5346
- color: var(--green-2);
5429
+ color: var(--green);
5347
5430
  }
5348
5431
  .ds-247420 .cm-ch-voice-user.speaking .cm-ch-voice-user-avatar {
5349
5432
  box-shadow: 0 0 0 2px var(--green-2);
@@ -5399,7 +5482,7 @@
5399
5482
  font-size: var(--fs-micro);
5400
5483
  text-transform: uppercase;
5401
5484
  letter-spacing: var(--tr-caps);
5402
- color: var(--green-2);
5485
+ color: var(--green);
5403
5486
  font-weight: 700;
5404
5487
  display: inline-flex;
5405
5488
  align-items: center;