Files
Bose-SoundTouch/pkg/service/soundtouchweb/static/css/app.css
T
Tobias GesellchenandClaude Opus 4.8 23949189f7 fix(player): stop navbar title/icons overlapping on small screens; show device names in grouping
#500: the absolutely-centered page title and the right-aligned icon bar
shared the same space in the fixed-height navbar and overlapped on phones
(portrait). On <=600px the navbar now wraps into two rows: row 1 keeps the
logo with the title beside it (the title fills the remaining width and
ellipsizes), and the icon bar drops onto its own centered, full-width row
below. CSS-only.

#498: the zone/grouping UI showed raw IP addresses instead of device names.
Root cause was a field-name casing bug: Zone.js read info.Name (uppercase),
but the device info field is info.name (lowercase) everywhere else in the UI
(app.js, DeviceList, Library, TTS, ...). So the lookup always missed and fell
back to the IP. Fixed the casing in the deviceName() helper, and made the
"Add to zone" picker show the device name with the IP as a smaller secondary
line (reusing the .picker-device-info/name/ip pattern the other pickers
already use). Member/master rows resolve names via deviceName().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 21:48:02 +02:00

890 lines
30 KiB
CSS

/* ── Reset & Base ─────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f5f5f5;
--surface: #ffffff;
--border: #e0e0e0;
--text: #1a1a1a;
--text-dim: #666;
--accent: #000000;
--accent-fg: #ffffff;
--online: #22c55e;
--offline: #9ca3af;
--radius: 8px;
--shadow: 0 1px 3px rgba(0,0,0,.10), 0 1px 2px rgba(0,0,0,.06);
--nav-icon-filter: brightness(0) invert(1);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--surface: #1e1e1e;
--border: #333;
--text: #f0f0f0;
--text-dim: #ccc;
--accent: #e0e0e0;
--accent-fg:#111;
--nav-icon-filter: none;
}
}
html, body { height: 100%; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 15px;
line-height: 1.5;
background: var(--bg);
color: var(--text);
}
a { color: inherit; text-decoration: none; cursor: pointer; }
button { cursor: pointer; font: inherit; border: none; background: none; }
ul { list-style: none; }
img { display: block; max-width: 100%; }
/* ── Layout ──────────────────────────────────────────────────────────────── */
.app { display: flex; flex-direction: column; min-height: 100vh; }
#footer {
padding: 1.5rem 1rem;
text-align: center;
font-size: 0.8rem;
color: var(--text-dim);
margin-top: auto;
}
#footer a:hover {
text-decoration: underline;
}
/* ── Navbar ──────────────────────────────────────────────────────────────── */
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 1.25rem;
height: 52px;
background: var(--accent);
color: var(--accent-fg);
position: relative; /* For absolute centering of page-title */
}
.brand {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
letter-spacing: .02em;
flex: 0 0 auto;
z-index: 1;
}
.brand-text {
display: none;
flex-direction: column;
justify-content: center;
line-height: 1.1;
}
.brand-name {
display: inline;
}
.brand-subtitle {
font-size: 0.7rem;
font-weight: 400;
opacity: 0.7;
}
@media (min-width: 600px) {
.brand-text {
display: flex;
}
}
.page-title {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 60%; /* Allow more width for title + IP */
text-align: center;
font-weight: 600;
font-size: 1.1rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0;
pointer-events: none; /* Let clicks pass through to navbar if needed */
display: flex;
flex-direction: column;
align-items: center;
line-height: 1.2;
}
.title-with-subtitle {
display: flex;
flex-direction: column;
align-items: center;
}
.main-title {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.sub-title {
display: block;
font-size: 0.75rem;
font-weight: 400;
opacity: 0.8;
font-family: monospace;
}
/* Small screens (phones, portrait): the absolutely-centered page title and the
right-aligned icon bar share the same space in the fixed-height navbar and
collide (#500). Let the navbar wrap into two rows: row 1 keeps the logo with
the title beside it (the title fills the remaining width and ellipsizes), and
the icon bar drops onto its own full-width row below. */
@media (max-width: 600px) {
.navbar {
flex-wrap: wrap;
height: auto;
min-height: 52px;
row-gap: 0.25rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
.page-title {
position: static;
transform: none;
flex: 1 1 auto; /* sit next to the logo and fill the rest of row 1 */
width: auto;
min-width: 0; /* allow the title to shrink + ellipsize */
}
.nav-links {
order: 1; /* force the icon bar onto its own row below */
width: 100%;
justify-content: center;
flex-wrap: wrap;
}
}
.nav-logo {
width: 24px;
height: 24px;
/* Keep the braille mark in its brand colours; the mono nav icons below
still recolour via --nav-icon-filter, the logo stays the one accent. */
}
.nav-links {
display: flex;
align-items: center;
gap: .25rem;
z-index: 1;
}
.nav-links a, .nav-links .btn-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: var(--accent-fg);
opacity: .75;
border-radius: 4px;
transition: all .15s ease;
}
.nav-links .nav-separator {
color: var(--accent-fg);
opacity: .3;
padding: 0 .25rem;
user-select: none;
}
.nav-links a:hover, .nav-links .btn-icon:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
.nav-links a.active {
opacity: 1;
background: var(--accent-fg);
color: var(--accent);
}
@media (prefers-color-scheme: dark) {
.nav-links a:hover, .nav-links .btn-icon:hover {
background: rgba(255, 255, 255, 0.05);
}
}
.nav-links a.active .nav-tunein-icon,
.nav-links a.active .nav-rb-icon,
.nav-links a.active .nav-device-icon,
.nav-links a.active .nav-url-icon {
filter: none;
opacity: 1;
}
@media (prefers-color-scheme: dark) {
.nav-links a.active {
background: var(--surface);
color: var(--text);
box-shadow: inset 0 0 0 1px var(--border);
}
.nav-links a.active .nav-tunein-icon,
.nav-links a.active .nav-rb-icon,
.nav-links a.active .nav-device-icon,
.nav-links a.active .nav-url-icon {
filter: invert(1);
}
}
.nav-tunein-icon, .nav-rb-icon, .nav-device-icon, .nav-url-icon { height: 20px; display: block; filter: var(--nav-icon-filter); opacity: .75; transition: filter .15s; }
.nav-discover-icon { height: 24px; display: block; filter: var(--nav-icon-filter); opacity: .75; transition: filter .15s; }
.nav-discover-icon.buzzing { animation: buzzing 0.3s linear infinite; opacity: 1; }
@keyframes buzzing {
0% { transform: rotate(0deg); }
25% { transform: rotate(15deg); }
50% { transform: rotate(0deg); }
75% { transform: rotate(-15deg); }
100% { transform: rotate(0deg); }
}
.nav-links a:hover .nav-tunein-icon, .nav-links a.active .nav-tunein-icon,
.nav-links a:hover .nav-rb-icon, .nav-links a.active .nav-rb-icon,
.nav-links a:hover .nav-device-icon, .nav-links a.active .nav-device-icon,
.nav-links a:hover .nav-url-icon, .nav-links a.active .nav-url-icon,
.nav-links .btn-icon:hover .nav-discover-icon { opacity: 1; }
/* ── Main content ─────────────────────────────────────────────────────────── */
.main-content { flex: 1; padding: 1.5rem 1.25rem; max-width: 960px; width: 100%; margin: 0 auto; }
/* ── Page header ──────────────────────────────────────────────────────────── */
.page-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.page-header h2 { font-size: 1.4rem; font-weight: 600; flex: 1; }
/* ── Buttons ─────────────────────────────────────────────────────────────── */
.btn-primary {
background: var(--accent);
color: var(--accent-fg);
padding: .45rem 1rem;
border-radius: var(--radius);
font-size: .875rem;
font-weight: 500;
transition: opacity .15s;
}
.btn-primary:hover { opacity: .85; }
.btn-secondary {
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
padding: .45rem 1rem;
border-radius: var(--radius);
font-size: .875rem;
transition: background .15s;
}
.btn-secondary:hover { background: var(--bg); }
.btn-icon {
color: inherit;
font-size: 1.1rem;
padding: .25rem .4rem;
border-radius: 4px;
line-height: 0;
display: inline-flex;
align-items: center;
justify-content: center;
transition: opacity .15s;
}
.btn-icon:hover { opacity: .7; }
.back-btn {
color: var(--text-dim);
font-size: .875rem;
padding: .3rem .6rem;
border-radius: 4px;
border: 1px solid var(--border);
background: var(--surface);
}
.back-btn:hover { background: var(--bg); }
/* ── Device grid ─────────────────────────────────────────────────────────── */
.device-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.device-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
cursor: pointer;
transition: box-shadow .15s, transform .1s;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
}
.device-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,.12); transform: translateY(-1px); }
.device-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: .25rem; gap: .5rem; }
.device-name { font-weight: 600; font-size: .95rem; }
.device-header-right { display: flex; align-items: center; gap: .5rem; flex-shrink: 0; }
/* Quiet remove affordance: invisible until the card is hovered, then dim,
warming to red only on its own hover. Keeps the grid relaxed. */
.device-remove {
border: 0;
background: none;
padding: 0;
width: 1.1rem;
height: 1.1rem;
line-height: 1;
font-size: .8rem;
color: var(--text-dim);
cursor: pointer;
opacity: 0;
transition: opacity .15s, color .15s;
}
.device-card:hover .device-remove { opacity: .55; }
.device-remove:hover { opacity: 1; color: var(--offline); }
.device-remove:focus-visible { opacity: 1; outline: 2px solid var(--offline); outline-offset: 2px; }
.device-list-note {
margin: .9rem .15rem 0;
font-size: .78rem;
color: var(--text-dim);
line-height: 1.4;
}
.device-type { font-size: .8rem; color: var(--text-dim); margin-bottom: .5rem; display: flex; gap: .4rem; flex-wrap: wrap; }
.device-ip { color: var(--text); font-family: monospace; font-weight: 500; }
.device-indicator {
width: 8px; height: 8px; border-radius: 50%;
flex-shrink: 0;
}
.device-indicator.online { background: var(--online); }
.device-indicator.offline { background: var(--offline); }
.now-playing-mini { font-size: .8rem; color: var(--text-dim); margin-top: .25rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.play-status { margin-right: .3rem; }
.standby-label { font-size: .8rem; color: var(--text-dim); margin-top: .25rem; }
/* ── Device detail ───────────────────────────────────────────────────────── */
.device-detail { max-width: 560px; }
/* ── Now playing ─────────────────────────────────────────────────────────── */
.now-playing {
display: flex;
gap: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
margin: 1rem 0;
box-shadow: var(--shadow);
min-height: 98px; /* Fixed height to avoid jumps between tracks/standby */
align-items: center;
position: relative; /* anchor for the ★ fav button */
}
.now-playing.standby { color: var(--text-dim); font-size: .9rem; }
/* ── Now-playing ★ fav / preset button ──────────────────────────────────── */
.now-playing-fav-wrap {
position: absolute;
top: .45rem;
right: .5rem;
}
.now-playing-fav-btn {
background: none;
border: none;
font-size: 1.1rem;
line-height: 1;
padding: .15rem .2rem;
cursor: pointer;
color: var(--text-dim);
opacity: .3;
transition: opacity .15s, color .15s;
border-radius: 4px;
}
.now-playing-fav-btn:hover,
.now-playing-fav-btn.open {
opacity: .75;
color: var(--text);
}
.now-playing-fav-btn.mapped {
color: #c9a000; /* warm gold, not too bright */
opacity: .85;
}
.now-playing-fav-btn.mapped:hover,
.now-playing-fav-btn.mapped.open {
opacity: 1;
color: #e0b800;
}
.now-playing-fav-overlay {
position: absolute;
top: calc(100% + 4px);
right: 0;
z-index: 50;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: .5rem .6rem;
display: flex;
flex-direction: column;
gap: .4rem;
min-width: 170px;
}
.album-art { width: 64px; height: 64px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
.track-info { flex: 1; overflow: hidden; }
.track-title { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-artist { font-size: .875rem; color: var(--text-dim); margin-top: .15rem; }
.track-album { font-size: .8rem; color: var(--text-dim); }
.track-meta { display: flex; align-items: center; gap: .5rem; margin-top: .25rem; }
.track-source { font-size: .75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; }
.buffering-badge { font-size: .7rem; color: var(--text-dim); background: var(--bg); border-radius: 4px; padding: .1rem .35rem; }
/* ── Transport controls ──────────────────────────────────────────────────── */
.controls {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 1rem;
box-shadow: var(--shadow);
}
.transport { display: flex; align-items: center; gap: .5rem; margin-bottom: .75rem; }
.ctrl-btn {
font-size: 1.25rem;
padding: .4rem .7rem;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface);
color: var(--text);
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
transition: background .12s;
}
.ctrl-btn:hover { background: var(--bg); }
.ctrl-btn.play-btn { font-size: 1.5rem; padding: .4rem .9rem; }
.ctrl-btn.active { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }
.volume-row { display: flex; align-items: center; gap: .75rem; }
.volume-icon { display: flex; align-items: center; }
.volume-slider { flex: 1; accent-color: var(--accent); }
.volume-value { font-size: .8rem; color: var(--text-dim); min-width: 2.5ch; text-align: right; }
.bass-row { display: flex; align-items: center; gap: .75rem; margin-top: .5rem; }
.bass-label { font-size: .8rem; color: var(--text-dim); width: 2.5ch; }
/* ── Progress bar ────────────────────────────────────────────────────────── */
.progress-row { margin-top: .35rem; display: flex; align-items: center; gap: .5rem; }
.progress-bar {
flex: 1;
height: 3px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--accent);
border-radius: 2px;
transition: width .9s linear;
}
.progress-time { font-size: .7rem; color: var(--text-dim); white-space: nowrap; flex-shrink: 0; }
/* ── Save-as-preset picker (NowPlaying) ─────────────────────────────────── */
.preset-picker-wrap { position: relative; margin-top: .6rem; display: inline-block; }
.save-preset-btn {
font-size: .72rem; font-weight: 600;
padding: .3rem .65rem; border-radius: var(--radius);
border: 1px solid var(--border); background: var(--surface);
color: var(--text-dim); cursor: pointer;
transition: border-color .15s, color .15s;
}
.save-preset-btn:hover, .save-preset-btn.open {
border-color: var(--accent); color: var(--accent);
}
.preset-picker-overlay {
position: absolute; top: calc(100% + 6px); left: 0; z-index: 50;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); box-shadow: var(--shadow);
padding: .5rem .6rem; display: flex; flex-direction: column; gap: .4rem;
min-width: 180px;
}
.preset-picker-label {
font-size: .65rem; font-weight: 600; text-transform: uppercase;
letter-spacing: .06em; color: var(--text-dim);
}
.preset-picker-slots { display: flex; gap: .35rem; }
.preset-picker-slot {
width: 2rem; height: 2rem; border-radius: var(--radius);
border: 1px solid var(--border); background: var(--bg);
font-size: .8rem; font-weight: 700; cursor: pointer;
transition: background .1s, border-color .1s, color .1s;
display: flex; align-items: center; justify-content: center;
}
.preset-picker-slot:hover:not(:disabled) {
border-color: var(--accent); color: var(--accent);
}
.preset-picker-slot.saving { opacity: .5; cursor: wait; }
.preset-picker-slot.saved { border-color: var(--accent); background: var(--accent); color: var(--accent-fg); }
/* ── Presets ─────────────────────────────────────────────────────────────── */
.presets-section, .sources-section { margin-top: 1.25rem; }
.section-title { font-size: .8rem; font-weight: 600; text-transform: uppercase; letter-spacing: .06em; color: var(--text-dim); margin-bottom: .6rem; }
.preset-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: .4rem;
}
.preset-slot {
display: flex;
flex-direction: column;
align-items: center;
gap: .25rem;
padding: .4rem .2rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
cursor: pointer;
position: relative;
transition: background .1s, box-shadow .1s;
min-height: 72px;
}
.preset-slot:hover:not(:disabled) { background: var(--bg); box-shadow: var(--shadow); }
.preset-slot:disabled { opacity: .4; cursor: default; }
.preset-slot.active { border-color: var(--accent); background: var(--accent); color: var(--accent-fg); }
.preset-slot.active .preset-name { color: var(--accent-fg); }
.preset-slot-wrap { position: relative; }
.preset-slot-wrap .preset-slot { width: 100%; }
/* Source-specific accent colour applied as a top border on text-only slots
(slots that have no artwork image). Has no visible effect when art is
shown because the image fills the top of the slot. */
.preset-slot[data-source="SPOTIFY"] { --slot-color: #1DB954; }
.preset-slot[data-source="TUNEIN"] { --slot-color: #0082C8; }
.preset-slot[data-source="LOCAL_INTERNET_RADIO"] { --slot-color: #FF6B35; }
.preset-slot[data-source="RADIO_BROWSER"] { --slot-color: #FF6B35; }
.preset-slot[data-source="AMAZON"] { --slot-color: #FF9900; }
.preset-slot[data-source="PANDORA"] { --slot-color: #005483; }
.preset-slot:not(.active):not(.empty):not(:has(.preset-art)) {
border-top: 2px solid var(--slot-color, var(--border));
}
/* Save-to-preset overlay button — fades in on hover, lives outside the play
button to avoid invalid nested <button> elements. */
.preset-save-btn {
position: absolute; top: 2px; left: 3px; z-index: 1;
font-size: .65rem; line-height: 1; font-weight: 700;
background: none; border: none; cursor: pointer; padding: 2px 3px;
border-radius: 2px; color: var(--text-dim);
opacity: 0; transition: opacity .15s, color .15s;
}
.preset-slot-wrap:hover .preset-save-btn,
.preset-save-btn:focus { opacity: .65; }
.preset-save-btn:hover { opacity: 1; color: var(--accent); }
.preset-save-btn.saved { opacity: 1; color: var(--accent); }
.preset-save-btn.error { opacity: 1; color: #e55; }
.preset-art { width: 36px; height: 36px; border-radius: 4px; object-fit: cover; }
.preset-source-label { font-size: .6rem; font-weight: 600; text-transform: uppercase; opacity: .6; }
.preset-name { font-size: .65rem; text-align: center; line-height: 1.2; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word; color: var(--text-dim); }
.preset-num {
position: absolute; top: 2px; right: 4px;
font-size: .6rem; font-weight: 700; color: var(--text-dim); opacity: .5;
}
/* ── Sources ─────────────────────────────────────────────────────────────── */
.source-list { display: flex; flex-wrap: wrap; gap: .4rem; }
.source-btn {
display: flex;
align-items: center;
gap: .35rem;
padding: .35rem .7rem;
border: 1px solid var(--border);
border-radius: 999px;
background: var(--surface);
font-size: .8rem;
transition: background .1s, border-color .1s;
}
.source-btn:hover { background: var(--bg); }
.source-btn.active { border-color: var(--accent); background: var(--accent); color: var(--accent-fg); }
.source-btn.local { border-style: dashed; }
.source-icon { font-size: .9rem; line-height: 1; }
.source-name { font-weight: 500; }
/* ── Zone ────────────────────────────────────────────────────────────────── */
.zone-section { margin-top: 1.25rem; }
.zone-row { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.zone-status-label { font-size: .875rem; color: var(--text-dim); }
.zone-members { display: flex; flex-direction: column; gap: .3rem; }
.zone-member {
display: flex; align-items: center; gap: .6rem;
padding: .4rem .6rem;
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius);
}
.zone-master-row { background: var(--bg); }
.zone-badge {
font-size: .65rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .05em; padding: .15rem .4rem; border-radius: 3px; flex-shrink: 0;
}
.zone-badge.master { background: var(--accent); color: var(--accent-fg); }
.zone-badge.slave { background: var(--border); color: var(--text-dim); }
.zone-member-name { flex: 1; font-size: .875rem; }
.zone-remove { font-size: .75rem; color: var(--text-dim); padding: .15rem .35rem; }
.zone-remove:hover { color: var(--text); }
.zone-actions { display: flex; gap: .5rem; margin-top: .25rem; flex-wrap: wrap; }
.zone-btn { font-size: .8rem; padding: .3rem .7rem; }
/* ── Recents ─────────────────────────────────────────────────────────────── */
.recents-section { margin-top: 1.25rem; }
.recents-list { display: flex; flex-direction: column; gap: 1px; }
.recent-item {
display: flex;
align-items: center;
gap: .75rem;
width: 100%;
padding: .5rem .6rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
text-align: left;
color: var(--text);
transition: background .1s;
}
.recent-item:hover { background: var(--bg); }
.recent-art {
width: 40px; height: 40px; border-radius: 4px;
object-fit: cover; flex-shrink: 0;
}
.recent-art-empty {
display: flex; align-items: center; justify-content: center;
background: var(--bg); font-size: 1.1rem;
}
.recent-info { flex: 1; overflow: hidden; }
.recent-name { display: block; font-size: .875rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.recent-source { display: block; font-size: .75rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .05em; margin-top: .1rem; }
.recent-play { color: var(--text-dim); font-size: .75rem; flex-shrink: 0; opacity: .5; }
.recent-item:hover .recent-play { opacity: 1; }
/* ── TuneIn ──────────────────────────────────────────────────────────────── */
.tunein-toolbar { display: flex; gap: .5rem; margin-bottom: 1rem; }
.tunein-search-input {
flex: 1;
padding: .45rem .75rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
color: var(--text);
font: inherit;
font-size: .875rem;
}
.tunein-search-input:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.breadcrumb { display: flex; align-items: center; gap: .4rem; margin-bottom: .75rem; font-size: .85rem; flex-wrap: wrap; }
.breadcrumb-sep { color: var(--text-dim); }
.breadcrumb-link { color: var(--text-dim); cursor: pointer; }
.breadcrumb-link:hover { text-decoration: underline; }
.breadcrumb-current { font-weight: 500; }
.loading-bar {
height: 2px;
background: linear-gradient(90deg, var(--accent) 0%, transparent 100%);
border-radius: 1px;
margin-bottom: 1rem;
animation: loading 1s ease-in-out infinite;
}
@keyframes loading { 0%,100% { opacity: .4; } 50% { opacity: 1; } }
.tunein-list { display: flex; flex-direction: column; gap: 1px; }
.tunein-item {
display: flex;
align-items: center;
gap: .75rem;
padding: .6rem .75rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
cursor: pointer;
transition: background .1s;
}
.tunein-item:hover { background: var(--bg); }
.tunein-thumb { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
.tunein-item-info { flex: 1; overflow: hidden; }
.tunein-item-name { display: block; font-size: .9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tunein-item-desc { display: block; font-size: .75rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tunein-item-arrow { color: var(--text-dim); font-size: .9rem; flex-shrink: 0; }
.tunein-play-btn {
background: transparent;
border: 1px solid var(--text-dim);
color: var(--text);
border-radius: 999px;
width: 32px; height: 32px;
display: inline-flex; align-items: center; justify-content: center;
cursor: pointer; flex-shrink: 0;
font-size: .85rem; line-height: 1;
padding: 0;
transition: background .15s, border-color .15s;
}
.tunein-play-btn:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.tunein-section-name { font-size: .85rem; font-weight: 600; color: var(--text-dim); padding: 8px 0 2px; margin: 0; }
.tunein-load-more { margin: 4px 0 12px; }
/* ── Device picker overlay ───────────────────────────────────────────────── */
.overlay {
position: fixed; inset: 0;
background: rgba(0,0,0,.45);
display: flex; align-items: center; justify-content: center;
z-index: 100;
}
.device-picker {
background: var(--surface);
border-radius: var(--radius);
padding: 1.5rem;
min-width: 240px;
max-width: 320px;
box-shadow: 0 8px 32px rgba(0,0,0,.2);
}
.picker-title { font-weight: 600; margin-bottom: .25rem; }
.picker-item-name { font-size: .875rem; color: var(--text-dim); margin-bottom: 1rem; }
.picker-devices { display: flex; flex-direction: column; gap: .5rem; margin-bottom: 1rem; }
.picker-device-btn {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: .6rem 1rem;
text-align: left;
font-size: .9rem;
color: var(--text);
transition: background .1s, border-color .1s;
}
.picker-device-btn:hover {
background: var(--bg);
border-color: var(--text-dim);
}
.picker-device-info {
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.picker-device-name {
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
display: block;
}
.picker-device-btn:hover .picker-device-name {
white-space: normal;
word-break: break-all;
}
.picker-device-ip {
font-size: 0.75rem;
color: var(--text-dim);
font-family: monospace;
}
.picker-cancel { width: 100%; }
.picker-no-devices { font-size: .875rem; color: var(--text); text-align: center; padding: .5rem 0; }
/* ── Empty state ─────────────────────────────────────────────────────────── */
.empty-state { text-align: center; padding: 4rem 1rem; color: var(--text); }
.empty-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: .8;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 64px;
height: 64px;
line-height: 1;
}
.empty-icon.radiating { animation: pulse 1.5s ease-in-out infinite; opacity: 1; color: var(--accent); font-weight: bold; }
.empty-icon.radiating::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
border: 3px solid var(--accent);
border-radius: 50%;
transform: scale(1);
animation: radiate 1.5s ease-out infinite;
pointer-events: none;
}
.empty-state p { margin-bottom: 1.5rem; }
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.15); }
100% { transform: scale(1); }
}
@keyframes radiate {
0% { transform: scale(0.8); opacity: 1; }
100% { transform: scale(2.5); opacity: 0; }
}
/* ── Toast ───────────────────────────────────────────────────────────────── */
.toast {
position: fixed;
bottom: 1.5rem;
left: 50%;
transform: translateX(-50%);
background: var(--accent);
color: var(--accent-fg);
padding: .6rem 1.25rem;
border-radius: 999px;
font-size: .875rem;
box-shadow: 0 4px 12px rgba(0,0,0,.2);
z-index: 200;
pointer-events: none;
animation: fade-in .2s ease;
}
@keyframes fade-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } }