Every speed in config.js is in pixels per second, so the same numbers cross a small screen in less time than a big one: on 1280x800 the frame covers the width in two thirds of the time it takes on 1920x1080, and the player reads as hurried. One factor now sits behind every animation clock — tickSeconds() for the frame loops, `speed` for the corners timer, and a --motion-stretch custom property for the two CSS fades — so the Speed switch on the queue page slows the drift, the glides, the pong rally, the drag beats, the wash lap, the screenshot intervals and the fades together. It is read per frame rather than baked into the constants, so flipping the switch lands on the next frame: no animation restarts, and one part-way through a beat keeps its place. The trail's stills now age on animation time rather than wall-clock time, so a still fades at the rate the frame that dropped it is moving. The audio crossfade is deliberately left out, being tied to the track that is ending. The flag is saved with the queue, unlike `paused`: it describes the screen the player is on, which a restart does not change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011vCaS2whbFjExShoPNnS3A
308 lines
4.9 KiB
CSS
308 lines
4.9 KiB
CSS
:root {
|
|
--accent: #ff4fc3;
|
|
--ink: #14060f;
|
|
--panel: #1f0c1a;
|
|
--panel-2: #2a1124;
|
|
--text: #f7e9f3;
|
|
--muted: #a8859c;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
margin: 0 auto;
|
|
padding: 24px 20px 60px;
|
|
max-width: 720px;
|
|
background: var(--ink);
|
|
color: var(--text);
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
h1 {
|
|
margin: 0;
|
|
font-size: 1.8rem;
|
|
letter-spacing: -0.02em;
|
|
color: var(--accent);
|
|
}
|
|
|
|
#status {
|
|
font-size: 0.7rem;
|
|
letter-spacing: 0.12em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
|
|
#status.offline {
|
|
color: #ff8a5c;
|
|
}
|
|
|
|
#status.online::before {
|
|
content: "";
|
|
display: inline-block;
|
|
width: 7px;
|
|
height: 7px;
|
|
margin-right: 6px;
|
|
border-radius: 50%;
|
|
background: #55e08a;
|
|
}
|
|
|
|
/* --- add form ----------------------------------------------------------- */
|
|
|
|
#add-form {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
#url {
|
|
flex: 1;
|
|
min-width: 0;
|
|
font: inherit;
|
|
padding: 12px 14px;
|
|
border-radius: 10px;
|
|
border: 1px solid #43213a;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
}
|
|
|
|
#url:focus {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 1px;
|
|
}
|
|
|
|
#add-form button {
|
|
font: inherit;
|
|
font-weight: 700;
|
|
padding: 12px 24px;
|
|
border: 0;
|
|
border-radius: 10px;
|
|
background: var(--accent);
|
|
color: var(--ink);
|
|
cursor: pointer;
|
|
}
|
|
|
|
#error {
|
|
margin: 10px 2px 0;
|
|
color: #ff8a5c;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* --- totals ------------------------------------------------------------- */
|
|
|
|
#totals {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin: 22px 0 12px;
|
|
}
|
|
|
|
.total {
|
|
flex: 1;
|
|
padding: 12px 14px;
|
|
border-radius: 10px;
|
|
background: var(--panel);
|
|
border: 1px solid #37172f;
|
|
}
|
|
|
|
.label {
|
|
display: block;
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.14em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.value {
|
|
display: block;
|
|
margin-top: 4px;
|
|
font-size: 1.5rem;
|
|
font-variant-numeric: tabular-nums;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* --- transport ---------------------------------------------------------- */
|
|
|
|
#transport {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
#playpause {
|
|
width: 100%;
|
|
font: inherit;
|
|
font-weight: 700;
|
|
padding: 12px 24px;
|
|
border-radius: 10px;
|
|
border: 1px solid #43213a;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Paused is the state that wants acting on, so that is the one wearing the
|
|
accent. */
|
|
#playpause.paused {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--ink);
|
|
}
|
|
|
|
#playpause:disabled {
|
|
opacity: 0.4;
|
|
cursor: default;
|
|
}
|
|
|
|
/* --- list --------------------------------------------------------------- */
|
|
|
|
#list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
}
|
|
|
|
.item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 12px;
|
|
border-radius: 10px;
|
|
background: var(--panel);
|
|
border: 1px solid transparent;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.item.current {
|
|
background: var(--panel-2);
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 1px rgba(255, 79, 195, 0.35);
|
|
}
|
|
|
|
.item.dragging {
|
|
opacity: 0.35;
|
|
}
|
|
|
|
.item.drop-before {
|
|
border-top-color: var(--accent);
|
|
box-shadow: 0 -2px 0 var(--accent);
|
|
}
|
|
|
|
.item.drop-after {
|
|
border-bottom-color: var(--accent);
|
|
box-shadow: 0 2px 0 var(--accent);
|
|
}
|
|
|
|
.handle {
|
|
flex: none;
|
|
cursor: grab;
|
|
color: var(--muted);
|
|
padding: 0 2px;
|
|
font-size: 1.1rem;
|
|
line-height: 1;
|
|
user-select: none;
|
|
}
|
|
|
|
.index {
|
|
flex: none;
|
|
width: 1.6em;
|
|
text-align: right;
|
|
color: var(--muted);
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.item.current .index {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.title {
|
|
flex: 1;
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.duration {
|
|
flex: none;
|
|
color: var(--muted);
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.remove {
|
|
flex: none;
|
|
border: 0;
|
|
background: transparent;
|
|
color: var(--muted);
|
|
font: inherit;
|
|
font-size: 1.2rem;
|
|
line-height: 1;
|
|
padding: 2px 6px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.remove:hover {
|
|
background: #46203c;
|
|
color: #ff8a5c;
|
|
}
|
|
|
|
#empty {
|
|
color: var(--muted);
|
|
text-align: center;
|
|
padding: 30px 0;
|
|
}
|
|
|
|
/* --- footer ------------------------------------------------------------- */
|
|
|
|
footer {
|
|
margin-top: 32px;
|
|
padding-top: 18px;
|
|
border-top: 1px solid #331630;
|
|
}
|
|
|
|
footer .switcher + .switcher {
|
|
margin-top: 18px;
|
|
}
|
|
|
|
#backgrounds,
|
|
#animations,
|
|
#speeds {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
#backgrounds button,
|
|
#animations button,
|
|
#speeds button {
|
|
font: inherit;
|
|
padding: 8px 18px;
|
|
border-radius: 999px;
|
|
border: 1px solid #43213a;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
cursor: pointer;
|
|
}
|
|
|
|
#backgrounds button.active,
|
|
#animations button.active,
|
|
#speeds button.active {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--ink);
|
|
font-weight: 600;
|
|
}
|