Re-order css file
Deploy HEOS panel / deploy (push) Successful in 27s

This commit is contained in:
franzz committed 2026-09-19 23:20:22 +02:00
1 parent 14d7b91bd4
commit e668fdec11
1 file changed
+57 -90
+57 -90
View File
@@ -1,5 +1,4 @@
/* A remote lives in the dark next to a TV, so the panel is dark too -- /* Base Theme */
and every control is sized for a thumb, not a cursor. */
:root { :root {
color-scheme: dark; color-scheme: dark;
@@ -19,11 +18,9 @@
--scrim: rgba(4, 6, 11, .6); --scrim: rgba(4, 6, 11, .6);
--shadow: rgba(0, 0, 0, .45); --shadow: rgba(0, 0, 0, .45);
--shade: rgba(255, 255, 255, .05); --shade: rgba(255, 255, 255, .05);
/* spacing scale for every gap/padding/margin in the sheet, smallest to
largest: 2xs 4px, sm 8px, md 12px, gutter 16px, lg 24px. */
--space-xs: 4px; --space-xs: 4px;
--space-sm: 8px; --space-sm: 8px;
--space-md: 12px; /* from the screen's side to a card, and from one card to the next */ --space-md: 12px;
--space-lg: 24px; --space-lg: 24px;
--toast-bad-bg: #2b1f24; --toast-bad-bg: #2b1f24;
--toast-bad-edge: #52303a; --toast-bad-edge: #52303a;
@@ -36,9 +33,10 @@
* { box-sizing: border-box; } * { box-sizing: border-box; }
/* Must beat every display rule below it. `hidden` is a plain UA style, so @media (prefers-reduced-motion: reduce) {
any author rule -- .sheet-wrap's display:flex, say -- silently wins, and * { animation: none !important; transition: none !important; }
an overlay that never hides sits over the whole panel eating taps. */ }
[hidden] { display: none !important; } [hidden] { display: none !important; }
html { background: var(--bg); } html { background: var(--bg); }
@@ -66,7 +64,6 @@ button {
} }
button:active { transform: scale(.96); } button:active { transform: scale(.96); }
/* Every icon is a Font Awesome glyph: a solid shape, filled, never stroked. */
svg { width: 22px; height: 22px; fill: currentColor; } svg { width: 22px; height: 22px; fill: currentColor; }
.app { .app {
@@ -78,17 +75,22 @@ svg { width: 22px; height: 22px; fill: currentColor; }
flex-direction: column; flex-direction: column;
gap: var(--space-md); gap: var(--space-md);
} }
/* A playing card goes to the top. The rest stay in markup order (rooms as
room_keys lists them, then the AVR), because items with the same order
keep their place, so playing cards keep that order among themselves too.
Only the order changes, not the DOM, so a knob held mid-drag stays put. */
.app > .on-top { order: -1; }
/* --- header ---------------------------------------------------------- */ .wide { width: 100%; height: 50px; border-radius: var(--radius-lg); font-size: 15px; }
.ghost { background: transparent; border: 1px solid var(--edge); color: var(--muted); }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes rise { from { transform: translateY(14%); opacity: .4; } }
/* Header */
.top { order: -2; display: flex; align-items: center; justify-content: center; } .top { order: -2; display: flex; align-items: center; justify-content: center; }
.top .logo { height: 44px; width: auto; display: block; margin: var(--space-xs); } .top .logo { height: 44px; width: auto; display: block; margin: var(--space-xs); }
/* --- cards ----------------------------------------------------------- */ /* Card */
.app > .on-top { order: -1; } /* A playing card goes to the top */
.card { .card {
background: var(--card); background: var(--card);
border: 1px solid var(--edge); border: 1px solid var(--edge);
@@ -99,11 +101,7 @@ svg { width: 22px; height: 22px; fill: currentColor; }
gap: var(--space-md); gap: var(--space-md);
} }
.card.offline { opacity: .5; } .card.offline { opacity: .5; }
/* A tint, not a repaint -- it should read at a glance without competing .card.room.playing, .card.avr.playing {
with the song title right above it. A group plays one thing, so it is the
AVR's card that lights up, not the rooms merged into it. */
.card.room.playing,
.card.avr.playing {
background: linear-gradient(135deg, background: linear-gradient(135deg,
color-mix(in srgb, var(--live) 25%, var(--card)) 0%, color-mix(in srgb, var(--live) 25%, var(--card)) 0%,
var(--card) 70%); var(--card) 70%);
@@ -118,7 +116,8 @@ svg { width: 22px; height: 22px; fill: currentColor; }
.card-head h2 .room-icon { flex: 0 0 auto; height: 20px; width: auto; fill: currentColor; } .card-head h2 .room-icon { flex: 0 0 auto; height: 20px; width: auto; fill: currentColor; }
.card-head h2 .room-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .card-head h2 .room-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* --- now playing, between the room's name and its volume --------------- */ /* Card - Media being played */
.media { .media {
background: var(--shade); background: var(--shade);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
@@ -138,20 +137,13 @@ svg { width: 22px; height: 22px; fill: currentColor; }
object-fit: cover; object-fit: cover;
background: var(--raised); background: var(--raised);
} }
/* A film's jacket keeps its own portrait shape rather than being cropped
square like an album's. */
.cover.poster { width: 50px; height: 75px; border-radius: 9px; } .cover.poster { width: 50px; height: 75px; border-radius: 9px; }
.track { display: flex; flex-direction: column; min-width: 0; } .track { display: flex; flex-direction: column; min-width: 0; }
.track span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .track span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.song { font-size: 17px; font-weight: 550; } .song { font-size: 17px; font-weight: 550; }
.artist { font-size: 15px; color: var(--muted); } .artist { font-size: 15px; color: var(--muted); }
/* Doubles as the line between the song and the volume -- it goes with the
block, when nothing is playing. Muted throughout, so it never competes
with the volume meter below it; the cursor only shows once a position is
actually known (an AVR input or a stream with no duration never gets one). */
.progress { display: flex; align-items: center; gap: var(--space-md); } .progress { display: flex; align-items: center; gap: var(--space-md); }
.progress-line { position: relative; flex: 1; height: 3px; border-radius: 2px; background: var(--bg); } .progress-line { position: relative; flex: 1; height: 3px; border-radius: 2px; background: var(--muted); }
.progress-fill { .progress-fill {
display: block; height: 100%; width: calc(var(--progress, 0) * 1%); display: block; height: 100%; width: calc(var(--progress, 0) * 1%);
border-radius: 999px; background: var(--muted); border-radius: 999px; background: var(--muted);
@@ -166,10 +158,6 @@ svg { width: 22px; height: 22px; fill: currentColor; }
transform: translateY(-50%); transform: translateY(-50%);
transition: left .12s ease-out; transition: left .12s ease-out;
} }
/* Elapsed rides above the cursor and remaining below it, all three centred
on one axis, and the total waits at the end of the line. Near either end a
label stops half its own width (--half, measured in app.js) short of it,
so it never hangs off the card. */
.progress-time, .progress-time,
.progress-total { .progress-total {
display: none; display: none;
@@ -188,11 +176,6 @@ svg { width: 22px; height: 22px; fill: currentColor; }
.progress.known .progress-cursor, .progress.known .progress-cursor,
.progress.known .progress-time, .progress.known .progress-time,
.progress.known .progress-total { display: block; } .progress.known .progress-total { display: block; }
/* Where the player can seek, the cursor and both times are one handle, with
a thumb-sized grip around a 7px dot. pan-y leaves a vertical swipe to the
page, so scrolling past never seeks; only a sideways drag is ours. Held,
it lights up and follows the finger without easing after it. */
.progress.seekable .progress-cursor, .progress.seekable .progress-cursor,
.progress.seekable .progress-time { cursor: grab; touch-action: pan-y; } .progress.seekable .progress-time { cursor: grab; touch-action: pan-y; }
.progress.seekable .progress-cursor::before { content: ''; position: absolute; inset: calc(var(--space-md) * -1); } .progress.seekable .progress-cursor::before { content: ''; position: absolute; inset: calc(var(--space-md) * -1); }
@@ -201,7 +184,8 @@ svg { width: 22px; height: 22px; fill: currentColor; }
.progress.dragging .progress-cursor { background: var(--ink); transform: translateY(-50%) scale(1.6); } .progress.dragging .progress-cursor { background: var(--ink); transform: translateY(-50%) scale(1.6); }
.progress.dragging .progress-time { color: var(--ink); } .progress.dragging .progress-time { color: var(--ink); }
/* --- volume ---------------------------------------------------------- */ /* Card - Volume */
.volume { display: flex; align-items: center; gap: var(--space-md); } .volume { display: flex; align-items: center; gap: var(--space-md); }
.control-btn { .control-btn {
@@ -221,9 +205,6 @@ svg { width: 22px; height: 22px; fill: currentColor; }
transition: width .12s ease-out; transition: width .12s ease-out;
} }
/* The level rides the bar. It travels the track less its own width, so it
never hangs off either end over the buttons -- and the fill's end is
always somewhere underneath it. */
.knob { .knob {
position: absolute; top: 50%; position: absolute; top: 50%;
left: calc((100% - 46px) * var(--level, 0) / 100); left: calc((100% - 46px) * var(--level, 0) / 100);
@@ -238,23 +219,25 @@ svg { width: 22px; height: 22px; fill: currentColor; }
cursor: grab; cursor: grab;
touch-action: none; /* the drag is ours, not a page scroll */ touch-action: none; /* the drag is ours, not a page scroll */
} }
/* A thumb-sized grip around a knob only 30px tall. A press on the .knob::before { /* A thumb-sized grip around a knob only 30px tall. A press on the pseudo-element lands on the knob itself. */
pseudo-element lands on the knob itself. */ content: '';
.knob::before { content: ''; position: absolute; inset: calc(var(--space-md) * -1) calc(var(--space-sm) * -1); } position: absolute;
inset: calc(var(--space-md) * -1) calc(var(--space-sm) * -1);
/* Under a finger the knob has to keep up, not ease after it. */ }
.meter.dragging i, .meter.dragging i,
.meter.dragging .knob { transition: none; } .meter.dragging .knob { transition: none; }
.meter.dragging .knob { cursor: grabbing; transform: translateY(-50%) scale(1.12); } .meter.dragging .knob { cursor: grabbing; transform: translateY(-50%) scale(1.12); }
/* --- previous / play-pause / next -------------------------------------- */ /* Card - Navigation */
.actions { display: flex; align-items: stretch; justify-content: center; gap: var(--space-md); } .actions { display: flex; align-items: stretch; justify-content: center; gap: var(--space-md); }
.control-btn .icon-pause { display: none; } .control-btn .icon-pause { display: none; }
.control-btn.playing .icon-play { display: none; } .control-btn.playing .icon-play { display: none; }
.control-btn.playing .icon-pause { display: block; } .control-btn.playing .icon-pause { display: block; }
/* --- Sources : Spotify / Home cinema ----------------------------------- */ /* Card - Sources */
.sources { display: flex; gap: var(--space-md); } .sources { display: flex; gap: var(--space-md); }
.sources-btn { .sources-btn {
flex: 1; min-width: 0; flex: 1; min-width: 0;
@@ -267,20 +250,18 @@ svg { width: 22px; height: 22px; fill: currentColor; }
.sources-btn svg { flex: 0 0 auto; color: var(--ink-bright); } .sources-btn svg { flex: 0 0 auto; color: var(--ink-bright); }
.sources-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .sources-btn span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sources-btn .spinner { display: none; } .sources-btn .spinner { display: none; }
/* Bumped with the `button` type selector so this reliably beats
.joined .toggle .leave's display:block below, same specificity otherwise. */
button.sources-btn.loading .icon { display: none; } button.sources-btn.loading .icon { display: none; }
.sources-btn.loading .spinner { display: block; animation: spin 1s steps(8) infinite; } .sources-btn.loading .spinner { display: block; animation: spin 1s steps(8) infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
/* The account playing on this room right now. */
.spotify[aria-pressed="true"] { border: 2px solid var(--ink); } .spotify[aria-pressed="true"] { border: 2px solid var(--ink); }
/* --- join / leave the AVR, between the Spotify buttons ----------------- */
.toggle .leave { display: none; } .toggle .leave { display: none; }
.toggle[aria-pressed="true"] { background: var(--accent); color: var(--ink-bright); } .toggle[aria-pressed="true"] { background: var(--accent); color: var(--ink-bright); }
/* --- AVR card ------------------------------------------------------ */ /* AVR */
/* AVR - Input */
.input-select { .input-select {
display: flex; align-items: center; gap: var(--space-md); display: flex; align-items: center; gap: var(--space-md);
width: 100%; min-height: 50px; width: 100%; min-height: 50px;
@@ -294,33 +275,6 @@ button.sources-btn.loading .icon { display: none; }
.input-select .value { flex: 1; font-size: 19px; font-weight: 600; } .input-select .value { flex: 1; font-size: 19px; font-weight: 600; }
.input-select .chevron { color: var(--muted); } .input-select .chevron { color: var(--muted); }
/* --- rooms merged into the AVR's card --------------------------------- */
.joined { display: flex; flex-direction: column; gap: var(--space-md); }
/* In here a room is an outline rather than a card: its border is what says
it is playing with the AVR, and the fill -- a tint too, when it plays --
belongs to the group's card instead. Same specificity as
.card.room.playing, so being later is what wins. */
.joined .card.room { background: none; border-color: var(--ink); border-radius: 18px; padding: var(--space-md); gap: var(--space-md); }
.joined .card-head h2 { font-size: 15px; font-weight: 550; color: var(--muted); }
.joined .cover { width: 52px; height: 52px; border-radius: 10px; }
.joined .song { font-size: 18px; }
.joined .step { height: 54px; }
/* In here the Spotify buttons hide and the toggle says "Detach", so it takes
their place and their look: the whole row, a plain background rather than
the pressed accent, and an exit icon where the AVR's would only muddle. */
.joined .toggle[aria-pressed="true"] { background: var(--raised); color: inherit; }
.joined .toggle .cinema { display: none; }
.joined .toggle .leave { display: block; }
/* --- misc ------------------------------------------------------------- */
.wide { width: 100%; height: 50px; border-radius: var(--radius-lg); font-size: 15px; }
.ghost { background: transparent; border: 1px solid var(--edge); color: var(--muted); }
.foot { margin: var(--space-xs) var(--space-xs) 0; min-height: 18px; font-size: 12px; color: var(--muted); text-align: center; }
.foot.bad { color: var(--warn); }
/* --- input sheet ------------------------------------------------------ */
.sheet-wrap { position: fixed; inset: 0; z-index: 10; display: flex; flex-direction: column; justify-content: flex-end; } .sheet-wrap { position: fixed; inset: 0; z-index: 10; display: flex; flex-direction: column; justify-content: flex-end; }
.scrim { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(3px); } .scrim { position: absolute; inset: 0; background: var(--scrim); backdrop-filter: blur(3px); }
@@ -334,7 +288,6 @@ button.sources-btn.loading .icon { display: none; }
display: flex; flex-direction: column; gap: var(--space-md); display: flex; flex-direction: column; gap: var(--space-md);
animation: rise .22s cubic-bezier(.22, .68, .3, 1); animation: rise .22s cubic-bezier(.22, .68, .3, 1);
} }
@keyframes rise { from { transform: translateY(14%); opacity: .4; } }
.grabber { width: 38px; height: 4px; border-radius: 999px; background: var(--edge); margin: var(--space-xs) auto var(--space-xs); } .grabber { width: 38px; height: 4px; border-radius: 999px; background: var(--edge); margin: var(--space-xs) auto var(--space-xs); }
.sheet h3 { margin: 0 var(--space-xs); font-size: 14px; font-weight: 600; color: var(--muted); } .sheet h3 { margin: 0 var(--space-xs); font-size: 14px; font-weight: 600; color: var(--muted); }
@@ -354,7 +307,25 @@ button.sources-btn.loading .icon { display: none; }
.option .code { font-size: 12px; color: var(--muted); flex: 0 0 auto; } .option .code { font-size: 12px; color: var(--muted); flex: 0 0 auto; }
.option[aria-current="true"] .code { color: var(--ink-bright-muted); } .option[aria-current="true"] .code { color: var(--ink-bright-muted); }
/* --- toast ------------------------------------------------------------ */ /* AVR - Grouped Room */
.joined { display: flex; flex-direction: column; gap: var(--space-md); }
.joined .card.room { background: none; border-color: var(--ink); border-radius: 18px; padding: var(--space-md); gap: var(--space-md); }
.joined .card-head h2 { font-size: 15px; font-weight: 550; color: var(--muted); }
.joined .cover { width: 52px; height: 52px; border-radius: 10px; }
.joined .song { font-size: 18px; }
.joined .step { height: 54px; }
.joined .toggle[aria-pressed="true"] { background: var(--raised); color: inherit; }
.joined .toggle .cinema { display: none; }
.joined .toggle .leave { display: block; }
/* Footer */
.foot { margin: var(--space-xs) var(--space-xs) 0; min-height: 18px; font-size: 12px; color: var(--muted); text-align: center; }
.foot.bad { color: var(--warn); }
/* Toast */
.toast { .toast {
position: fixed; left: 50%; transform: translateX(-50%); position: fixed; left: 50%; transform: translateX(-50%);
bottom: calc(22px + env(safe-area-inset-bottom)); bottom: calc(22px + env(safe-area-inset-bottom));
@@ -363,8 +334,4 @@ button.sources-btn.loading .icon { display: none; }
background: var(--toast-bad-bg); border: 1px solid var(--toast-bad-edge); color: var(--toast-bad-ink); background: var(--toast-bad-bg); border: 1px solid var(--toast-bad-edge); color: var(--toast-bad-ink);
font-size: 14px; box-shadow: 0 10px 30px var(--shadow); font-size: 14px; box-shadow: 0 10px 30px var(--shadow);
} }
.toast.ok { background: var(--toast-ok-bg); border-color: var(--toast-ok-edge); color: var(--toast-ok-ink); } .toast.ok { background: var(--toast-ok-bg); border-color: var(--toast-ok-edge); color: var(--toast-ok-ink); }
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}