clauddy 1.14.0 → 1.16.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/auth.js CHANGED
@@ -152,6 +152,32 @@ function win(o) {
152
152
  : null
153
153
  }
154
154
 
155
+ // Weekly limits scoped to one model (e.g. "Fable" on Max) live in the `limits`
156
+ // array. The older seven_day_<model> windows are kept as a fallback for plans
157
+ // that still report them that way.
158
+ function scopedWeeks(j) {
159
+ const out = []
160
+ for (const l of Array.isArray(j.limits) ? j.limits : []) {
161
+ if (l?.kind !== 'weekly_scoped' || typeof l.percent !== 'number') continue
162
+ const label = l.scope?.model?.display_name
163
+ if (!label) continue
164
+ out.push({
165
+ label,
166
+ pct: l.percent,
167
+ resetMs: l.resets_at ? Date.parse(l.resets_at) - Date.now() : null,
168
+ })
169
+ }
170
+ if (out.length) return out
171
+ for (const [key, label] of [
172
+ ['seven_day_opus', 'Opus'],
173
+ ['seven_day_sonnet', 'Sonnet'],
174
+ ]) {
175
+ const w = win(j[key])
176
+ if (w) out.push({ label, ...w })
177
+ }
178
+ return out
179
+ }
180
+
155
181
  // Step 3: fetch the authoritative usage
156
182
  async function fetchUsage() {
157
183
  const token = await validToken()
@@ -173,8 +199,7 @@ async function fetchUsage() {
173
199
  return {
174
200
  session: win(j.five_hour) || { pct: 0, resetMs: null },
175
201
  week: win(j.seven_day) || { pct: 0, resetMs: null },
176
- sonnet: win(j.seven_day_sonnet),
177
- opus: win(j.seven_day_opus),
202
+ scoped: scopedWeeks(j),
178
203
  }
179
204
  }
180
205
 
package/main.js CHANGED
@@ -95,12 +95,22 @@ function loadConfig() {
95
95
  // native notification when usage crosses a threshold
96
96
  const armed = new Set()
97
97
  function checkAlerts(config, d) {
98
- if (!config.alerts || !Notification.isSupported()) return
99
- const ths = config.alertThresholds || [80, 95]
100
- const scopes = [
98
+ alertScopes(config, [
101
99
  ['session', d.session.pct],
102
100
  ['weekly usage', d.week.pct],
103
- ]
101
+ ])
102
+ }
103
+ // per-model weekly limits only exist on the account side, so they're checked
104
+ // off the OAuth poll rather than the local tick
105
+ function checkScopedAlerts(config, u) {
106
+ alertScopes(
107
+ config,
108
+ (u?.scoped || []).map((s) => [`${s.label} weekly usage`, s.pct]),
109
+ )
110
+ }
111
+ function alertScopes(config, scopes) {
112
+ if (!config.alerts || !Notification.isSupported()) return
113
+ const ths = config.alertThresholds || [80, 95]
104
114
  for (const [name, pct] of scopes) {
105
115
  for (const t of ths) {
106
116
  const key = `${name}:${t}`
@@ -361,6 +371,7 @@ function updateTray() {
361
371
  function pushRealUsage(u) {
362
372
  sessionPct = u?.session ? u.session.pct : null
363
373
  updateTray()
374
+ checkScopedAlerts(config, u)
364
375
  if (win && !win.isDestroyed()) win.webContents.send('real-usage', u)
365
376
  }
366
377
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.14.0",
4
+ "version": "1.16.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">
@@ -253,6 +254,8 @@
253
254
  <div class="track"><div class="fill week" id="week-fill"></div></div>
254
255
  <div class="sub" id="week-sub">—</div>
255
256
  </div>
257
+ <!-- per-model weekly limits (e.g. Fable), one meter each -->
258
+ <div id="scoped-meters"></div>
256
259
  </div>
257
260
  <button id="limits-connect">
258
261
  <svg class="lc-ico" viewBox="0 0 24 24" aria-hidden="true">
@@ -301,91 +304,52 @@
301
304
 
302
305
  <!-- settings panel -->
303
306
  <div id="settings">
304
- <div class="set-title">Settings</div>
305
-
306
307
  <div id="account">
307
308
  <div id="acc-disconnected">
308
- <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>
309
310
  <button id="acc-connect" class="acc-btn">Log in with browser</button>
310
311
  <div id="acc-paste">
311
- <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>
312
313
  <input id="acc-code" type="text" placeholder="authentication code" />
313
314
  <button id="acc-confirm" class="acc-btn">Connect</button>
314
315
  <div id="acc-msg"></div>
315
316
  </div>
316
317
  </div>
317
318
  <div id="acc-connected">
318
- <span class="acc-ok">● Connected · live data</span>
319
+ <span class="acc-ok" id="acc-ok">● Connected</span>
319
320
  <button id="acc-logout" class="acc-link">Disconnect</button>
320
321
  </div>
321
322
  </div>
322
323
 
323
- <div class="set-section">
324
- <div class="set-sec-title">Display</div>
325
- <span class="seg" id="set-mode">
326
- <button type="button" class="seg-btn" data-mode="floating">Floating pet</button>
327
- <button type="button" class="seg-btn" data-mode="menubar">Menu bar</button>
328
- </span>
329
- </div>
330
-
331
- <div class="set-section">
332
- <div class="set-sec-title">Zoom</div>
333
- <div class="set-thresholds">
334
- <span class="thr">
335
- <span class="thr-cap">size</span>
336
- <span class="num">
337
- <input id="set-zoom" type="number" min="100" max="200" step="25" />
338
- <span class="num-spin">
339
- <button type="button" class="num-btn" data-for="set-zoom" data-step="25">▲</button>
340
- <button type="button" class="num-btn" data-for="set-zoom" data-step="-25">▼</button>
341
- </span>
342
- </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>
343
330
  </span>
344
331
  </div>
345
- </div>
346
-
347
- <div class="set-section">
348
- <div class="set-sec-title">
349
- Alerts
332
+ <div class="set-row">
333
+ <span class="set-row-label">Alerts</span>
350
334
  <input id="set-alerts" class="set-toggle" type="checkbox" aria-label="Enable alerts" />
351
335
  </div>
352
- <div class="set-thresholds">
353
- <span class="thr">
354
- <span class="thr-cap">heads-up</span>
355
- <span class="num">
356
- <input id="set-t1" type="number" min="1" max="100" />
357
- <span class="num-spin">
358
- <button type="button" class="num-btn" data-for="set-t1" data-step="1">▲</button>
359
- <button type="button" class="num-btn" data-for="set-t1" data-step="-1">▼</button>
360
- </span>
361
- </span>
362
- </span>
363
- <span class="thr">
364
- <span class="thr-cap">near limit</span>
365
- <span class="num">
366
- <input id="set-t2" type="number" min="1" max="100" />
367
- <span class="num-spin">
368
- <button type="button" class="num-btn" data-for="set-t2" data-step="1">▲</button>
369
- <button type="button" class="num-btn" data-for="set-t2" data-step="-1">▼</button>
370
- </span>
371
- </span>
372
- </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>
373
345
  </div>
374
- </div>
375
-
376
- <div class="set-section">
377
- <div class="set-sec-title">Catch fire</div>
378
- <div class="set-thresholds">
379
- <span class="thr">
380
- <span class="thr-cap">session %</span>
381
- <span class="num">
382
- <input id="set-fire" type="number" min="1" max="99" />
383
- <span class="num-spin">
384
- <button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
385
- <button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
386
- </span>
387
- </span>
388
- </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>
389
353
  </div>
390
354
  </div>
391
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)
@@ -404,6 +404,27 @@ function fitNames(box) {
404
404
  box.style.setProperty('--name-w', `${w}px`)
405
405
  }
406
406
 
407
+ // per-model weekly limits (e.g. "Fable" on Max) — one meter each, in the same
408
+ // shape as the all-models one. The token count pairs the limit with the local
409
+ // log totals for that model family (a "Fable" limit covers every Fable row).
410
+ function renderScoped(list, byModel) {
411
+ const box = el('scoped-meters')
412
+ box.innerHTML = ''
413
+ for (const s of list) {
414
+ const key = s.label.toLowerCase()
415
+ const tokens = byModel
416
+ .filter((m) => m.label.toLowerCase().startsWith(key))
417
+ .reduce((n, m) => n + m.tokens, 0)
418
+ const m = document.createElement('div')
419
+ m.className = 'meter'
420
+ m.innerHTML =
421
+ `<div class="meter-top"><span>weekly · ${esc(key)}</span><span>${Math.round(s.pct)}%</span></div>` +
422
+ `<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>`
424
+ box.appendChild(m)
425
+ }
426
+ }
427
+
407
428
  // by model (7 days)
408
429
  function renderModels(list) {
409
430
  renderBars('bymodel-list', list, 4)
@@ -571,6 +592,7 @@ function render(d) {
571
592
  ? `resets in ${fmtReset(wkReset)} · ${fmtTokens(d.week.tokens)} tokens`
572
593
  : `${fmtTokens(d.week.tokens)} tokens · last 7 days`
573
594
 
595
+ renderScoped(liveOn ? realUsage.scoped || [] : [], d.byModel || [])
574
596
  renderModels(d.byModel || [])
575
597
  renderProjects(d.byProject || [])
576
598
  renderHeat(d.days30 || [])
@@ -655,6 +677,10 @@ window.api.onAuthState((s) => {
655
677
 
656
678
  // logged-in account chip (email + plan) shown top-left when connected
657
679
  function showProfile(p) {
680
+ // the settings line says who is connected, not just that someone is
681
+ el('acc-ok').textContent = p?.email
682
+ ? `● ${p.email}${p.plan ? ` · ${p.plan}` : ''}`
683
+ : '● Connected'
658
684
  const chip = el('account-chip')
659
685
  const mini = el('mini-acct')
660
686
  if (!p?.email) {
@@ -681,11 +707,23 @@ function setPlan(node, plan) {
681
707
  }
682
708
  }
683
709
  window.api.onProfile(showProfile)
710
+ let successTimer = null
684
711
  window.api.onAuthResult((r) => {
685
712
  if (r?.ok) {
686
713
  el('acc-msg').textContent = ''
687
714
  el('acc-code').value = ''
688
715
  el('acc-paste').classList.remove('show')
716
+ // logging in is done: land back on the home panel, where the fresh live
717
+ // meters show up under a short-lived cheer — and the pet throws confetti
718
+ document.body.classList.remove('settings-open')
719
+ clearSaveDirty()
720
+ el('home-success').hidden = false
721
+ clearTimeout(successTimer)
722
+ successTimer = setTimeout(() => {
723
+ el('home-success').hidden = true
724
+ fitSize()
725
+ }, 6000)
726
+ celebrate()
689
727
  } else {
690
728
  const e = r?.error || ''
691
729
  el('acc-msg').textContent = /429|rate_limit/i.test(e)
@@ -748,6 +786,10 @@ el('acc-connect').addEventListener('click', () => {
748
786
  el('acc-paste').classList.add('show') // reveal the code field
749
787
  fitSize()
750
788
  })
789
+ // the Connect button only lights up once there's a code to submit
790
+ el('acc-code').addEventListener('input', () => {
791
+ el('acc-confirm').classList.toggle('ready', !!el('acc-code').value.trim())
792
+ })
751
793
  el('acc-confirm').addEventListener('click', () => {
752
794
  const code = el('acc-code').value.trim()
753
795
  if (!code) return
@@ -826,16 +868,34 @@ function snapshotSettings() {
826
868
  }
827
869
  function refreshSaveDirty() {
828
870
  if (settingsBaseline == null) return
829
- el('set-save').classList.toggle('dirty', snapshotSettings() !== settingsBaseline)
871
+ const dirty = snapshotSettings() !== settingsBaseline
872
+ const b = el('set-save')
873
+ b.classList.toggle('dirty', dirty)
874
+ b.disabled = !dirty // nothing to save, nothing to click
830
875
  }
831
876
  function clearSaveDirty() {
832
877
  settingsBaseline = snapshotSettings()
833
- el('set-save').classList.remove('dirty')
878
+ const b = el('set-save')
879
+ b.classList.remove('dirty')
880
+ b.disabled = true
881
+ }
882
+ function reflectAlertsOn() {
883
+ el('set-rows').classList.toggle('alerts-off', !el('set-alerts').checked)
884
+ }
885
+ // zoom applies as you step it, so the widget shows the size right away;
886
+ // Cancel puts the saved value back
887
+ function previewZoom() {
888
+ const v = Number.parseFloat(el('set-zoom').value)
889
+ if (v >= 100 && v <= 200) {
890
+ applyZoom(v)
891
+ fitSize()
892
+ }
834
893
  }
835
894
  function populateSettings() {
836
895
  const c = currentConfig || {}
837
896
  setModeUI(c.mode)
838
897
  el('set-alerts').checked = c.alerts !== false
898
+ reflectAlertsOn()
839
899
  const th = c.alertThresholds || [80, 95]
840
900
  el('set-t1').value = th[0] != null ? th[0] : 80
841
901
  el('set-t2').value = th[1] != null ? th[1] : 95
@@ -861,6 +921,7 @@ for (const b of document.querySelectorAll('.num-btn')) {
861
921
  const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
862
922
  input.value = Math.min(max, Math.max(min, next))
863
923
  refreshSaveDirty() // steppers change the value without an 'input' event
924
+ if (input.id === 'set-zoom') previewZoom()
864
925
  })
865
926
  }
866
927
  // light up Save whenever an editable field changes
@@ -868,6 +929,8 @@ for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire', 'set-zoom']) {
868
929
  el(id).addEventListener('input', refreshSaveDirty)
869
930
  el(id).addEventListener('change', refreshSaveDirty)
870
931
  }
932
+ el('set-alerts').addEventListener('change', reflectAlertsOn)
933
+ el('set-zoom').addEventListener('input', previewZoom)
871
934
  // display-mode segmented control (floating pet | menu bar)
872
935
  for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
873
936
  b.addEventListener('click', () => {
@@ -878,6 +941,7 @@ for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
878
941
  el('set-cancel').addEventListener('click', () => {
879
942
  document.body.classList.remove('settings-open')
880
943
  clearSaveDirty()
944
+ applyZoom(currentConfig?.zoom != null ? currentConfig.zoom : 100) // undo the preview
881
945
  fitSize()
882
946
  })
883
947
  el('set-save').addEventListener('click', () => {
@@ -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
+ /* 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 */
309
314
  }
310
- .set-section + .set-section {
311
- border-top: 0.5px solid var(--hair);
312
- padding-top: 10px;
313
- }
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 */
@@ -1679,7 +1811,8 @@ body.state-tired #status-text {
1679
1811
  }
1680
1812
 
1681
1813
  /* meters */
1682
- .meter + .meter {
1814
+ .meter + .meter,
1815
+ #scoped-meters .meter {
1683
1816
  margin-top: 9px;
1684
1817
  }
1685
1818
  .meter-top {