Skip to main content

max / goingson

Replace the spinners with drive-activity lights A rotating ring is the one piece of smooth continuous motion left in the app, and it is the wrong idiom for the period besides. Both spinners become a small recessed lamp that flickers. The flicker is deliberately uneven: eight stops at irregular intervals under steps(1, end), so every change is a hard cut and no value is ever interpolated. Even blinking reads as a timer; uneven blinking reads as work happening, which is what a drive light communicates. The lamp is square at --radius-xs and takes --bevel-inset, because a recess is what it is: a well cut into the panel with the lit face at the bottom. Lit is --action rather than a literal amber or green, so it tracks the theme like everything else. This also retires the last two 2px currentColor borders flagged in wave 4. Renamed .spinner to .activity-light and .oauth-waiting-spinner to .oauth-waiting-light: a class called spinner that does not spin is the kind of drift that misleads the next reader. Fixed in passing: .oauth-waiting-spinner asked for margin 0 auto but the element was inline-block, so it never centred. The lamp is display block where it stands alone in a modal.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-27 00:35 UTC
Signed with PGP, not checked
Commit: ae417e0f3f7357ff6aa0a92345b49ff577ec1427
Parent: fba1088
2 files changed, +38 insertions, -22 deletions
@@ -2371,7 +2371,8 @@
2371 2371 color: var(--content-secondary);
2372 2372 margin-bottom: var(--space-5);
2373 2373 }
2374 - .oauth-waiting-spinner {
2374 + .oauth-waiting-light {
2375 + display: block;
2375 2376 margin: 0 auto var(--space-5);
2376 2377 }
2377 2378
@@ -4041,19 +4042,33 @@
4041 4042 }
4042 4043
4043 4044 /* Spinner animation */
4044 - @keyframes spin {
4045 - 0% { transform: rotate(0deg); }
4046 - 100% { transform: rotate(360deg); }
4045 + /* Activity light. A drive light does not sweep, it flickers: hard cuts at
4046 + uneven intervals, which is what reads as work happening rather than a
4047 + timer running. steps(1, end) holds each stop until the next one, so no
4048 + value is ever interpolated and nothing fades. */
4049 + @keyframes activity-flicker {
4050 + 0%, 9% { background: var(--action); }
4051 + 10%, 21% { background: var(--surface-sunken); }
4052 + 22%, 26% { background: var(--action); }
4053 + 27%, 43% { background: var(--surface-sunken); }
4054 + 44%, 57% { background: var(--action); }
4055 + 58%, 62% { background: var(--surface-sunken); }
4056 + 63%, 69% { background: var(--action); }
4057 + 70%, 100% { background: var(--surface-sunken); }
4047 4058 }
4048 4059
4049 - .spinner {
4060 + /* The lamp sits in a recess, which is why it takes the inset bevel: the
4061 + well is cut into the panel and the lit face sits at the bottom of it. */
4062 + .activity-light {
4050 4063 display: inline-block;
4051 - width: 1em;
4052 - height: 1em;
4053 - border: 2px solid currentColor;
4054 - border-top-color: transparent;
4055 - border-radius: var(--radius-full);
4056 - animation: spin 0.8s linear infinite;
4064 + width: 0.7em;
4065 + height: 0.7em;
4066 + vertical-align: -0.05em;
4067 + background: var(--surface-sunken);
4068 + border: var(--border-width) solid var(--border);
4069 + border-radius: var(--radius-xs);
4070 + box-shadow: var(--bevel-inset);
4071 + animation: activity-flicker 1.2s steps(1, end) infinite;
4057 4072 }
4058 4073
4059 4074 /* Button loading state */
@@ -4072,14 +4087,15 @@
4072 4087 position: absolute;
4073 4088 left: 50%;
4074 4089 top: 50%;
4075 - width: 1em;
4076 - height: 1em;
4077 - margin-left: -0.5em;
4078 - margin-top: -0.5em;
4079 - border: 2px solid currentColor;
4080 - border-top-color: transparent;
4081 - border-radius: var(--radius-full);
4082 - animation: spin 0.8s linear infinite;
4090 + width: 0.7em;
4091 + height: 0.7em;
4092 + margin-left: -0.35em;
4093 + margin-top: -0.35em;
4094 + background: var(--surface-sunken);
4095 + border: var(--border-width) solid var(--border);
4096 + border-radius: var(--radius-xs);
4097 + box-shadow: var(--bevel-inset);
4098 + animation: activity-flicker 1.2s steps(1, end) infinite;
4083 4099 }
4084 4100
4085 4101 .hidden { display: none !important; }
@@ -595,7 +595,7 @@
595 595 }
596 596
597 597 /**
598 - * Persistent spinner modal shown while an email sync runs. Replaced by the
598 + * Persistent activity modal shown while an email sync runs. Replaced by the
599 599 * Sync Results modal on success, or dismissed on error. Mirrors
600 600 * showOAuthWaitingModal.
601 601 * @param {string} message - Status line describing the in-progress sync
@@ -605,7 +605,7 @@
605 605 <div class="oauth-waiting">
606 606 <div class="oauth-waiting-title">Syncing email...</div>
607 607 <div class="oauth-waiting-body">${esc(message)}</div>
608 - <div class="spinner oauth-waiting-spinner"></div>
608 + <div class="activity-light oauth-waiting-light"></div>
609 609 </div>
610 610 `;
611 611 GoingsOn.ui.openModal('Email Sync', content);
@@ -660,7 +660,7 @@
660 660 <div class="oauth-waiting-body">
661 661 A browser window should have opened. Please sign in and authorize the app.
662 662 </div>
663 - <div class="spinner oauth-waiting-spinner"></div>
663 + <div class="activity-light oauth-waiting-light"></div>
664 664 <button type="button" class="btn btn-secondary" data-act="emails.cancelOAuth">Cancel</button>
665 665 </div>
666 666 `;