clauddy 1.15.0 → 1.17.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.15.0",
4
+ "version": "1.17.0",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {
@@ -240,6 +240,7 @@
240
240
  <div class="divider"></div>
241
241
 
242
242
  <!-- session + weekly (real usage; needs login) -->
243
+ <div id="home-success" hidden>✓ Logged in — your real usage is live</div>
243
244
  <div id="limits">
244
245
  <div id="limits-meters">
245
246
  <div class="meter">
@@ -303,91 +304,52 @@
303
304
 
304
305
  <!-- settings panel -->
305
306
  <div id="settings">
306
- <div class="set-title">Settings</div>
307
-
308
307
  <div id="account">
309
308
  <div id="acc-disconnected">
310
- <div class="set-hint">Connect your account to show your real usage %.</div>
309
+ <div class="set-hint set-hint-left">Connect your Claude account to show your real usage.</div>
311
310
  <button id="acc-connect" class="acc-btn">Log in with browser</button>
312
311
  <div id="acc-paste">
313
- <div class="set-hint">After logging in, paste the code from the browser page:</div>
312
+ <div class="set-hint set-hint-left">Paste the code from the browser page:</div>
314
313
  <input id="acc-code" type="text" placeholder="authentication code" />
315
314
  <button id="acc-confirm" class="acc-btn">Connect</button>
316
315
  <div id="acc-msg"></div>
317
316
  </div>
318
317
  </div>
319
318
  <div id="acc-connected">
320
- <span class="acc-ok">● Connected · live data</span>
319
+ <span class="acc-ok" id="acc-ok">● Connected</span>
321
320
  <button id="acc-logout" class="acc-link">Disconnect</button>
322
321
  </div>
323
322
  </div>
324
323
 
325
- <div class="set-section">
326
- <div class="set-sec-title">Display</div>
327
- <span class="seg" id="set-mode">
328
- <button type="button" class="seg-btn" data-mode="floating">Floating pet</button>
329
- <button type="button" class="seg-btn" data-mode="menubar">Menu bar</button>
330
- </span>
331
- </div>
332
-
333
- <div class="set-section">
334
- <div class="set-sec-title">Zoom</div>
335
- <div class="set-thresholds">
336
- <span class="thr">
337
- <span class="thr-cap">size</span>
338
- <span class="num">
339
- <input id="set-zoom" type="number" min="100" max="200" step="25" />
340
- <span class="num-spin">
341
- <button type="button" class="num-btn" data-for="set-zoom" data-step="25">▲</button>
342
- <button type="button" class="num-btn" data-for="set-zoom" data-step="-25">▼</button>
343
- </span>
344
- </span>
324
+ <div id="set-rows">
325
+ <div class="set-row">
326
+ <span class="set-row-label">Display</span>
327
+ <span class="seg" id="set-mode">
328
+ <button type="button" class="seg-btn" data-mode="floating">Floating pet</button>
329
+ <button type="button" class="seg-btn" data-mode="menubar">Menu bar</button>
345
330
  </span>
346
331
  </div>
347
- </div>
348
-
349
- <div class="set-section">
350
- <div class="set-sec-title">
351
- Alerts
332
+ <div class="set-row">
333
+ <span class="set-row-label">Alerts</span>
352
334
  <input id="set-alerts" class="set-toggle" type="checkbox" aria-label="Enable alerts" />
353
335
  </div>
354
- <div class="set-thresholds">
355
- <span class="thr">
356
- <span class="thr-cap">heads-up</span>
357
- <span class="num">
358
- <input id="set-t1" type="number" min="1" max="100" />
359
- <span class="num-spin">
360
- <button type="button" class="num-btn" data-for="set-t1" data-step="1">▲</button>
361
- <button type="button" class="num-btn" data-for="set-t1" data-step="-1">▼</button>
362
- </span>
363
- </span>
364
- </span>
365
- <span class="thr">
366
- <span class="thr-cap">near limit</span>
367
- <span class="num">
368
- <input id="set-t2" type="number" min="1" max="100" />
369
- <span class="num-spin">
370
- <button type="button" class="num-btn" data-for="set-t2" data-step="1">▲</button>
371
- <button type="button" class="num-btn" data-for="set-t2" data-step="-1">▼</button>
372
- </span>
373
- </span>
374
- </span>
336
+ <div class="set-subgroup">
337
+ <div class="set-row set-sub">
338
+ <span class="set-row-label sub-label">heads-up</span>
339
+ <span class="num"><button type="button" class="num-btn" data-for="set-t1" data-step="-1">−</button><span class="num-val"><input id="set-t1" type="number" min="1" max="100" step="1" /><i>%</i></span><button type="button" class="num-btn" data-for="set-t1" data-step="1">+</button></span>
340
+ </div>
341
+ <div class="set-row set-sub">
342
+ <span class="set-row-label sub-label">near limit</span>
343
+ <span class="num"><button type="button" class="num-btn" data-for="set-t2" data-step="-1">−</button><span class="num-val"><input id="set-t2" type="number" min="1" max="100" step="1" /><i>%</i></span><button type="button" class="num-btn" data-for="set-t2" data-step="1">+</button></span>
344
+ </div>
375
345
  </div>
376
- </div>
377
-
378
- <div class="set-section">
379
- <div class="set-sec-title">Catch fire</div>
380
- <div class="set-thresholds">
381
- <span class="thr">
382
- <span class="thr-cap">session %</span>
383
- <span class="num">
384
- <input id="set-fire" type="number" min="1" max="99" />
385
- <span class="num-spin">
386
- <button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
387
- <button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
388
- </span>
389
- </span>
390
- </span>
346
+ <div class="set-row">
347
+ <span class="set-row-label">Catch fire<i class="set-row-hint">at this session %</i></span>
348
+ <span class="num"><button type="button" class="num-btn" data-for="set-fire" data-step="-1">−</button><span class="num-val"><input id="set-fire" type="number" min="1" max="99" step="1" /><i>%</i></span><button type="button" class="num-btn" data-for="set-fire" data-step="1">+</button></span>
349
+ </div>
350
+ <div class="set-row">
351
+ <span class="set-row-label">Zoom</span>
352
+ <span class="num"><button type="button" class="num-btn" data-for="set-zoom" data-step="-25">−</button><span class="num-val"><input id="set-zoom" type="number" min="100" max="200" step="25" /><i>%</i></span><button type="button" class="num-btn" data-for="set-zoom" data-step="25">+</button></span>
391
353
  </div>
392
354
  </div>
393
355
 
package/renderer/pet.js CHANGED
@@ -89,9 +89,9 @@ const SPRITE = [
89
89
  }
90
90
  // clouds (blocky, dark gray)
91
91
  const cloud = (x, y, b, m, t) => {
92
- add('rect', { x: x + 12, y: y, width: t, height: 9, fill: '#3a3a3a' })
93
- add('rect', { x: x + 5, y: y + 7, width: m, height: 9, fill: '#3a3a3a' })
94
- add('rect', { x: x, y: y + 14, width: b, height: 10, fill: '#3a3a3a' })
92
+ add('rect', { x: x + 12, y: y, width: t, height: 9, fill: '#3a3a3a', class: 'cloud' })
93
+ add('rect', { x: x + 5, y: y + 7, width: m, height: 9, fill: '#3a3a3a', class: 'cloud' })
94
+ add('rect', { x: x, y: y + 14, width: b, height: 10, fill: '#3a3a3a', class: 'cloud' })
95
95
  }
96
96
  cloud(16, 10, 58, 42, 22)
97
97
  cloud(120, 50, 40, 28, 15)
@@ -162,6 +162,19 @@ function fmtReset(ms) {
162
162
  const m = Math.floor((ms % 3600000) / 60000)
163
163
  return h > 0 ? `${h}h ${m}m` : `${m}m`
164
164
  }
165
+ // wall-clock time of the reset, in the machine's own locale + timezone.
166
+ // Past 24h the day matters too, so prefix the weekday.
167
+ function fmtResetClock(ms) {
168
+ if (!ms || ms <= 0) return null
169
+ const at = new Date(Date.now() + ms)
170
+ const time = at.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
171
+ return ms >= 86400000 ? `${at.toLocaleDateString([], { weekday: 'short' })} ${time}` : time
172
+ }
173
+ // "2h 22m (14:35)" — countdown plus the clock time it lands on
174
+ function fmtResetIn(ms) {
175
+ const at = fmtResetClock(ms)
176
+ return at ? `${fmtReset(ms)} (${at})` : fmtReset(ms)
177
+ }
165
178
  // burn-rate projection lives in burn.js (shared with the tests)
166
179
  const burn = Burn.createBurnTracker()
167
180
 
@@ -420,7 +433,7 @@ function renderScoped(list, byModel) {
420
433
  m.innerHTML =
421
434
  `<div class="meter-top"><span>weekly · ${esc(key)}</span><span>${Math.round(s.pct)}%</span></div>` +
422
435
  `<div class="track"><div class="fill week${s.pct >= 80 ? ' high' : ''}" style="width:${s.pct}%"></div></div>` +
423
- `<div class="sub">${s.resetMs != null ? `resets in ${fmtReset(s.resetMs)} · ` : ''}${fmtTokens(tokens)} tokens</div>`
436
+ `<div class="sub">${s.resetMs != null ? `resets in ${fmtResetIn(s.resetMs)} · ` : ''}${fmtTokens(tokens)} tokens</div>`
424
437
  box.appendChild(m)
425
438
  }
426
439
  }
@@ -568,7 +581,7 @@ function render(d) {
568
581
  sf.style.width = `${sessPct}%`
569
582
  sf.classList.toggle('high', sessPct >= 80)
570
583
  el('session-sub').textContent = sessActive
571
- ? `resets in ${fmtReset(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
584
+ ? `resets in ${fmtResetIn(sessReset)} · ${fmtTokens(d.session.tokens)} tokens`
572
585
  : 'no active session'
573
586
 
574
587
  // where this pace is taking you — hidden until there's enough trail to tell
@@ -589,7 +602,7 @@ function render(d) {
589
602
  wf.classList.toggle('high', wkPct >= 80)
590
603
  el('week-sub').textContent =
591
604
  wkReset != null
592
- ? `resets in ${fmtReset(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
605
+ ? `resets in ${fmtResetIn(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
593
606
  : `${fmtTokens(d.week.tokens)} tokens · last 7 days`
594
607
 
595
608
  renderScoped(liveOn ? realUsage.scoped || [] : [], d.byModel || [])
@@ -677,6 +690,10 @@ window.api.onAuthState((s) => {
677
690
 
678
691
  // logged-in account chip (email + plan) shown top-left when connected
679
692
  function showProfile(p) {
693
+ // the settings line says who is connected, not just that someone is
694
+ el('acc-ok').textContent = p?.email
695
+ ? `● ${p.email}${p.plan ? ` · ${p.plan}` : ''}`
696
+ : '● Connected'
680
697
  const chip = el('account-chip')
681
698
  const mini = el('mini-acct')
682
699
  if (!p?.email) {
@@ -703,11 +720,23 @@ function setPlan(node, plan) {
703
720
  }
704
721
  }
705
722
  window.api.onProfile(showProfile)
723
+ let successTimer = null
706
724
  window.api.onAuthResult((r) => {
707
725
  if (r?.ok) {
708
726
  el('acc-msg').textContent = ''
709
727
  el('acc-code').value = ''
710
728
  el('acc-paste').classList.remove('show')
729
+ // logging in is done: land back on the home panel, where the fresh live
730
+ // meters show up under a short-lived cheer — and the pet throws confetti
731
+ document.body.classList.remove('settings-open')
732
+ clearSaveDirty()
733
+ el('home-success').hidden = false
734
+ clearTimeout(successTimer)
735
+ successTimer = setTimeout(() => {
736
+ el('home-success').hidden = true
737
+ fitSize()
738
+ }, 6000)
739
+ celebrate()
711
740
  } else {
712
741
  const e = r?.error || ''
713
742
  el('acc-msg').textContent = /429|rate_limit/i.test(e)
@@ -770,6 +799,10 @@ el('acc-connect').addEventListener('click', () => {
770
799
  el('acc-paste').classList.add('show') // reveal the code field
771
800
  fitSize()
772
801
  })
802
+ // the Connect button only lights up once there's a code to submit
803
+ el('acc-code').addEventListener('input', () => {
804
+ el('acc-confirm').classList.toggle('ready', !!el('acc-code').value.trim())
805
+ })
773
806
  el('acc-confirm').addEventListener('click', () => {
774
807
  const code = el('acc-code').value.trim()
775
808
  if (!code) return
@@ -848,16 +881,34 @@ function snapshotSettings() {
848
881
  }
849
882
  function refreshSaveDirty() {
850
883
  if (settingsBaseline == null) return
851
- el('set-save').classList.toggle('dirty', snapshotSettings() !== settingsBaseline)
884
+ const dirty = snapshotSettings() !== settingsBaseline
885
+ const b = el('set-save')
886
+ b.classList.toggle('dirty', dirty)
887
+ b.disabled = !dirty // nothing to save, nothing to click
852
888
  }
853
889
  function clearSaveDirty() {
854
890
  settingsBaseline = snapshotSettings()
855
- el('set-save').classList.remove('dirty')
891
+ const b = el('set-save')
892
+ b.classList.remove('dirty')
893
+ b.disabled = true
894
+ }
895
+ function reflectAlertsOn() {
896
+ el('set-rows').classList.toggle('alerts-off', !el('set-alerts').checked)
897
+ }
898
+ // zoom applies as you step it, so the widget shows the size right away;
899
+ // Cancel puts the saved value back
900
+ function previewZoom() {
901
+ const v = Number.parseFloat(el('set-zoom').value)
902
+ if (v >= 100 && v <= 200) {
903
+ applyZoom(v)
904
+ fitSize()
905
+ }
856
906
  }
857
907
  function populateSettings() {
858
908
  const c = currentConfig || {}
859
909
  setModeUI(c.mode)
860
910
  el('set-alerts').checked = c.alerts !== false
911
+ reflectAlertsOn()
861
912
  const th = c.alertThresholds || [80, 95]
862
913
  el('set-t1').value = th[0] != null ? th[0] : 80
863
914
  el('set-t2').value = th[1] != null ? th[1] : 95
@@ -883,6 +934,7 @@ for (const b of document.querySelectorAll('.num-btn')) {
883
934
  const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
884
935
  input.value = Math.min(max, Math.max(min, next))
885
936
  refreshSaveDirty() // steppers change the value without an 'input' event
937
+ if (input.id === 'set-zoom') previewZoom()
886
938
  })
887
939
  }
888
940
  // light up Save whenever an editable field changes
@@ -890,6 +942,8 @@ for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire', 'set-zoom']) {
890
942
  el(id).addEventListener('input', refreshSaveDirty)
891
943
  el(id).addEventListener('change', refreshSaveDirty)
892
944
  }
945
+ el('set-alerts').addEventListener('change', reflectAlertsOn)
946
+ el('set-zoom').addEventListener('input', previewZoom)
893
947
  // display-mode segmented control (floating pet | menu bar)
894
948
  for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
895
949
  b.addEventListener('click', () => {
@@ -900,6 +954,7 @@ for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
900
954
  el('set-cancel').addEventListener('click', () => {
901
955
  document.body.classList.remove('settings-open')
902
956
  clearSaveDirty()
957
+ applyZoom(currentConfig?.zoom != null ? currentConfig.zoom : 100) // undo the preview
903
958
  fitSize()
904
959
  })
905
960
  el('set-save').addEventListener('click', () => {
@@ -964,6 +1019,7 @@ if (typeof module === 'object' && module.exports) {
964
1019
  render,
965
1020
  fmtTokens,
966
1021
  fmtReset,
1022
+ fmtResetIn,
967
1023
  setState,
968
1024
  renderModels,
969
1025
  renderProjects,
@@ -221,13 +221,17 @@ body.settings-open #mini {
221
221
  }
222
222
  body.settings-open #settings {
223
223
  display: block;
224
+ animation: set-in 0.18s ease;
224
225
  }
225
- .set-title {
226
- font-size: 12px;
227
- font-weight: 700;
228
- color: var(--text);
229
- text-align: center;
230
- margin-bottom: 6px;
226
+ @keyframes set-in {
227
+ from {
228
+ opacity: 0;
229
+ transform: translateY(4px);
230
+ }
231
+ to {
232
+ opacity: 1;
233
+ transform: none;
234
+ }
231
235
  }
232
236
  .set-hint {
233
237
  font-size: 9px;
@@ -302,94 +306,168 @@ input[type="number"]::-webkit-outer-spin-button {
302
306
  body.is-menubar #min {
303
307
  display: none;
304
308
  }
305
- /* settings sections: Display / Alerts / Catch fireeach with a standardized
306
- title, separated by a hairline divider */
307
- .set-section {
308
- margin-bottom: 10px;
309
- }
310
- .set-section + .set-section {
311
- border-top: 0.5px solid var(--hair);
312
- padding-top: 10px;
309
+ /* settings rows: label on the left, control on the right one setting per
310
+ line so the screen stays short. Alert thresholds indent under their toggle
311
+ and dim while alerts are off. */
312
+ #set-rows {
313
+ margin-bottom: 10px; /* #account above already draws the divider */
313
314
  }
314
- .set-sec-title {
315
+ .set-row {
315
316
  display: flex;
316
317
  align-items: center;
317
318
  justify-content: space-between;
319
+ gap: 8px;
320
+ min-height: 34px;
321
+ border-bottom: 0.5px solid var(--hair);
322
+ }
323
+ /* the threshold pair lives in an inset card under its toggle row */
324
+ .set-row:has(+ .set-subgroup) {
325
+ border-bottom: none;
326
+ }
327
+ /* flat, but visibly nested: a soft coral guide bar ties the pair to the
328
+ toggle above, and each label hugs its own stepper */
329
+ .set-subgroup {
330
+ position: relative;
331
+ padding-left: 14px;
332
+ border-bottom: 0.5px solid var(--hair);
333
+ padding-bottom: 3px;
334
+ transition: opacity 0.15s;
335
+ }
336
+ .set-subgroup::before {
337
+ content: "";
338
+ position: absolute;
339
+ left: 3px;
340
+ top: 5px;
341
+ bottom: 10px;
342
+ width: 2px;
343
+ border-radius: 1px;
344
+ background: var(--coral);
345
+ opacity: 0.35;
346
+ }
347
+ .set-subgroup .set-row {
348
+ border-bottom: none;
349
+ justify-content: flex-end;
350
+ gap: 10px;
351
+ }
352
+ .set-row-label {
318
353
  font-size: 11px;
319
354
  font-weight: 600;
320
355
  color: var(--text);
321
- margin-bottom: 8px;
322
356
  }
323
- /* alerts on/off toggle, sitting to the right of the section title */
357
+ .set-row-hint {
358
+ display: block;
359
+ white-space: nowrap;
360
+ font-style: normal;
361
+ font-weight: 400;
362
+ font-size: 9px;
363
+ color: var(--muted);
364
+ margin-top: 2px;
365
+ }
366
+ .set-row.set-sub {
367
+ min-height: 31px;
368
+ }
369
+ .set-row.set-sub .set-row-label {
370
+ font-weight: 400;
371
+ font-size: 10.5px;
372
+ color: var(--muted);
373
+ }
374
+ #set-rows.alerts-off .set-subgroup {
375
+ opacity: 0.35;
376
+ pointer-events: none;
377
+ }
378
+ .set-row .seg {
379
+ margin-top: 0;
380
+ flex: none;
381
+ }
382
+ .set-row .seg-btn {
383
+ flex: none;
384
+ padding: 4px 9px;
385
+ }
386
+ /* alerts on/off pill switch (still an <input type=checkbox> underneath) */
324
387
  .set-toggle {
388
+ appearance: none;
389
+ -webkit-appearance: none;
325
390
  margin: 0;
326
- width: 13px;
327
- height: 13px;
328
- accent-color: var(--coral);
391
+ width: 30px;
392
+ height: 18px;
393
+ border-radius: 9px;
394
+ background: rgba(255, 255, 255, 0.14);
395
+ position: relative;
329
396
  cursor: pointer;
397
+ -webkit-app-region: no-drag;
398
+ transition: background 0.18s;
330
399
  }
331
- /* threshold fields: caption on top, number input below — same shape for all
332
- three (heads-up, near limit, catch fire) so they read as one set */
333
- .set-thresholds {
334
- display: flex;
335
- gap: 12px;
400
+ .set-toggle::after {
401
+ content: "";
402
+ position: absolute;
403
+ top: 2px;
404
+ left: 2px;
405
+ width: 14px;
406
+ height: 14px;
407
+ border-radius: 50%;
408
+ background: #fff;
409
+ opacity: 0.85;
410
+ transition:
411
+ left 0.18s,
412
+ opacity 0.18s;
336
413
  }
337
- .thr {
338
- display: flex;
339
- flex-direction: column;
340
- gap: 4px;
414
+ .set-toggle:checked {
415
+ background: var(--coral);
341
416
  }
342
- .thr-cap {
343
- font-size: 9px;
344
- color: var(--muted);
345
- height: 12px;
346
- line-height: 12px;
417
+ .set-toggle:checked::after {
418
+ left: 14px;
419
+ opacity: 1;
347
420
  }
348
- /* number field + custom stepper */
421
+ /* number field: minus / value% / plus */
349
422
  .num {
350
- position: relative;
423
+ display: flex;
424
+ align-items: center;
425
+ gap: 4px;
351
426
  flex: none;
352
- width: 66px;
353
427
  }
354
- .num input {
428
+ .num-val {
429
+ position: relative;
430
+ width: 54px;
431
+ }
432
+ .num-val input {
355
433
  width: 100%;
356
434
  background: rgba(255, 255, 255, 0.06);
357
435
  border: 0.5px solid var(--hair);
358
- border-radius: 8px;
436
+ border-radius: 7px;
359
437
  color: var(--text);
360
- font-size: 13px;
438
+ font-size: 12px;
361
439
  font-weight: 600;
362
440
  font-variant-numeric: tabular-nums;
363
- padding: 6px 24px 6px 10px;
441
+ text-align: right;
442
+ padding: 4px 20px 4px 6px;
364
443
  outline: none;
365
444
  transition:
366
445
  border-color 0.15s,
367
446
  background 0.15s;
368
447
  }
369
- .num input:focus {
448
+ .num-val input:focus {
370
449
  border-color: var(--coral);
371
450
  background: rgba(255, 255, 255, 0.09);
372
451
  }
373
- .num-spin {
452
+ .num-val i {
374
453
  position: absolute;
375
- top: 3px;
376
- right: 4px;
377
- bottom: 0;
378
- display: flex;
379
- flex-direction: column;
380
- align-items: center;
381
- justify-content: center;
382
- gap: 2px;
454
+ right: 7px;
455
+ top: 50%;
456
+ transform: translateY(-50%);
457
+ font-style: normal;
458
+ font-size: 10px;
459
+ color: var(--muted);
460
+ pointer-events: none;
383
461
  }
384
462
  .num-btn {
385
463
  -webkit-app-region: no-drag;
386
464
  border: none;
387
465
  background: rgba(255, 255, 255, 0.1);
388
466
  color: var(--muted);
389
- width: 16px;
390
- height: 9px;
391
- border-radius: 3px;
392
- font-size: 6px;
467
+ width: 20px;
468
+ height: 20px;
469
+ border-radius: 6px;
470
+ font-size: 12px;
393
471
  line-height: 1;
394
472
  display: flex;
395
473
  align-items: center;
@@ -432,7 +510,7 @@ body.is-menubar #min {
432
510
  background 0.18s,
433
511
  transform 0.1s;
434
512
  }
435
- .set-actions button:active {
513
+ .set-actions button:active:not(:disabled) {
436
514
  transform: scale(0.97);
437
515
  }
438
516
  #set-cancel {
@@ -443,10 +521,18 @@ body.is-menubar #min {
443
521
  background: rgba(255, 255, 255, 0.2);
444
522
  }
445
523
  #set-save {
446
- background: rgba(217, 119, 87, 0.4); /* calm until there are unsaved changes */
524
+ background: rgba(255, 255, 255, 0.12); /* rests like Cancel; dirty lights it */
525
+ color: var(--text);
526
+ }
527
+ #set-save:disabled {
528
+ opacity: 0.45;
529
+ cursor: default;
530
+ }
531
+ #set-save.dirty,
532
+ #set-save.dirty:hover {
447
533
  color: #fff;
448
534
  }
449
- #set-save:hover {
535
+ #set-save.dirty:hover {
450
536
  background: #e2876a;
451
537
  }
452
538
  /* unsaved changes: light up and pulse so you don't forget to save */
@@ -562,6 +648,10 @@ body.live .live-only {
562
648
  opacity: 0.5;
563
649
  cursor: default;
564
650
  }
651
+ #acc-confirm:not(.ready) {
652
+ background: rgba(255, 255, 255, 0.12);
653
+ color: var(--muted);
654
+ }
565
655
  .acc-link {
566
656
  border: none;
567
657
  background: none;
@@ -580,11 +670,18 @@ body.live .live-only {
580
670
  color: var(--green);
581
671
  font-size: 11px;
582
672
  font-weight: 600;
673
+ flex: 1;
674
+ min-width: 0;
675
+ overflow: hidden;
676
+ text-overflow: ellipsis;
677
+ white-space: nowrap;
583
678
  }
584
679
  #acc-connected {
585
680
  display: none;
586
681
  align-items: center;
587
682
  justify-content: space-between;
683
+ gap: 10px; /* the email ellipsizes instead of touching Disconnect */
684
+ flex-wrap: wrap; /* the success line drops to its own row */
588
685
  }
589
686
  #acc-paste {
590
687
  display: none;
@@ -616,6 +713,16 @@ body.auth-on #acc-connected {
616
713
  display: flex;
617
714
  }
618
715
 
716
+ /* just logged in: a short-lived green cheer at the top of the home panel */
717
+ #home-success {
718
+ font-size: 10px;
719
+ font-weight: 600;
720
+ color: var(--green);
721
+ text-align: center;
722
+ margin-bottom: 8px;
723
+ animation: set-in 0.18s ease;
724
+ }
725
+
619
726
  /* limits: real meters when connected, a "connect" prompt otherwise */
620
727
  #limits-connect {
621
728
  display: flex;
@@ -775,6 +882,24 @@ body.settings-open #min {
775
882
 
776
883
  /* pet */
777
884
  /* expanded: a little night scene */
885
+ body.settings-open #stage {
886
+ height: 100px;
887
+ }
888
+ /* the ground layers stretch to fill the stage; while it's shortened, pin them
889
+ at their natural height so the gears stay round — the top just clips away.
890
+ #scene keeps stretching (dots and stars tolerate it) so its frame rows stay
891
+ at the edges; the blocky clouds don't, so they sit this screen out. */
892
+ body.settings-open #gears {
893
+ top: auto;
894
+ bottom: 0;
895
+ height: 132px;
896
+ }
897
+ body.settings-open .cloud {
898
+ display: none;
899
+ }
900
+ body.settings-open #pet {
901
+ zoom: 0.65;
902
+ }
778
903
  #stage {
779
904
  position: relative;
780
905
  height: 132px;
@@ -908,10 +1033,17 @@ body.settings-open #robot,
908
1033
  body.settings-open #gears {
909
1034
  opacity: 1;
910
1035
  }
911
- /* declutter the scene while configuring */
1036
+ /* declutter the scene while configuring: mood marks AND activity props —
1037
+ the shortened stage only fits the pet and its gears */
912
1038
  body.settings-open #zzz,
913
1039
  body.settings-open #flames,
914
- body.settings-open #sweat {
1040
+ body.settings-open #sweat,
1041
+ body.settings-open #terminal,
1042
+ body.settings-open #reading,
1043
+ body.settings-open #mug,
1044
+ body.settings-open #desk,
1045
+ body.settings-open #plant,
1046
+ body.settings-open #glasses {
915
1047
  display: none;
916
1048
  }
917
1049
  /* reading mode: an open book with a magnifying glass scanning the lines */
@@ -1612,10 +1744,14 @@ body.state-tired #status-text {
1612
1744
 
1613
1745
  /* meter subtitle */
1614
1746
  .sub {
1615
- font-size: 9.5px;
1747
+ /* small enough that "resets in 69h 15m (Fri 7:01 AM) · 1.02B tokens" stays on one line */
1748
+ font-size: 9px;
1616
1749
  color: var(--muted);
1617
1750
  margin-top: 4px;
1618
- letter-spacing: 0.2px;
1751
+ letter-spacing: 0px;
1752
+ white-space: nowrap;
1753
+ overflow: hidden;
1754
+ text-overflow: ellipsis;
1619
1755
  }
1620
1756
 
1621
1757
  /* burn-rate projection — calm by default, warm when you'd run out first */