SatsAmp/design/glass-radio-demo.html
avi 1b7c6febbb feat: rename app to SatsAmp + appearance themes (violet/blue/green/orange accents, System/Dark/Light modes)
- project dir ~/Projects/RadAmp -> ~/Projects/SatsAmp; classes, strings, launcher label, UA strings renamed; applicationId + radamp.db kept so existing installs/data survive
- UiPrefs (SharedPreferences) + UiTokens: 4 accents x dark/light, every text/accent pair AA-verified (accentOnFill picks ink vs white per accent)
- legacy Premium* colors rewritten to LocalTokens.current reads; theme switches live, persists across restart
- Appearance dialog in Library top bar (swatches + System/Dark/Light), verified on-device: orange live-switch, light mode + persistence, folder-tab regression pass
2026-09-07 18:45:29 -05:00

823 lines
38 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>SatsAmp Glass — Theme Demo</title>
<style>
/* ============================================================
SatsAmp Glass — glassmorphism theme for audio/radio streaming
Dark default + light variant. WCAG 2.2 AA verified (see demo).
============================================================ */
/* ---------- 1. TOKENS ---------- */
:root {
color-scheme: dark;
/* Base background: flat dark charcoal, very subtle vertical gradient */
--bg-top: #23232B;
--bg-bottom: #1B1B22;
--bg-gradient: linear-gradient(180deg, var(--bg-top) 0%, var(--bg-bottom) 100%);
/* Glass recipe */
--glass-bg: rgba(255, 255, 255, 0.06);
--glass-bg-strong: rgba(255, 255, 255, 0.09); /* hover */
--glass-border: rgba(255, 255, 255, 0.10); /* decorative only, NOT a 3:1 boundary */
--border-interactive: rgba(255, 255, 255, 0.36); /* 3.0:1 on glass — WCAG 1.4.11 */
--glass-blur: 18px; /* hard cap: 18–20px, never higher */
--glass-saturate: 160%;
--glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.14);
--shadow-glass: 0 10px 30px rgba(0, 0, 0, 0.35);
--shadow-fab: 0 12px 32px rgba(0, 0, 0, 0.5), 0 4px 10px rgba(124, 77, 255, 0.35);
/* Accent — violet */
--accent: #7C4DFF;
--accent-hover: #8E66FF;
--accent-pressed: #6A3DE0;
--accent-soft: rgba(124, 77, 255, 0.16); /* icon chips, active fills */
/* #7C4DFF is only 2.71:1 on glass → text/active-label accent on glass uses this
(4.83:1 on glass, 5.78:1 on base). #7C4DFF itself is reserved for fills
and for text on the SOLID base (3.24:1, large text only). */
--accent-text: #A78BFF;
/* Text (all pairs ≥4.5:1 on glass; do not darken) */
--text-title: #FFFFFF; /* 13.0:1 on glass */
--text-body: rgba(255, 255, 255, 0.88); /* 10.5:1 on glass */
--text-caption: rgba(255, 255, 255, 0.55); /* 5.1:1 on glass — floor */
/* Status */
--live: #34C759; /* 5.9:1 on glass */
--danger: #FF6B6B;
/* Focus */
--focus-ring: #A78BFF; /* 4.8:1 vs glass */
--focus-width: 2px;
--focus-offset: 3px;
/* Solid panels (long lists / body copy — glass is sparing) */
--panel-solid: #202029; /* ≈90% opaque base */
--panel-border: rgba(255, 255, 255, 0.08);
/* Radii */
--r-card: 20px; /* now-playing bar, cards */
--r-input: 14px; /* inputs, chips */
--r-pill: 999px; /* pills, circular buttons */
/* Spacing scale */
--sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px;
--sp-5: 20px; --sp-6: 24px; --sp-8: 32px; --sp-10: 40px;
/* Type */
--font: "Inter", "SF Pro Text", "Geist", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--fs-body: 16px; --lh-body: 1.6;
--fs-title: 22px; --fs-hero: 28px;
--fs-caption: 12px; --ls-caption: 0.4px;
/* Safe areas */
--safe-bottom: env(safe-area-inset-bottom, 0px);
}
/* ---------- Light variant ---------- */
[data-theme="light"] {
color-scheme: light;
--bg-top: #F2F1F7;
--bg-bottom: #E7E5F0;
--glass-bg: rgba(255, 255, 255, 0.62);
--glass-bg-strong: rgba(255, 255, 255, 0.78);
--glass-border: rgba(35, 35, 43, 0.10);
--border-interactive: rgba(35, 35, 43, 0.45); /* 3:1 on light glass */
--glass-highlight: inset 0 1px 0 rgba(255, 255, 255, 0.65);
--shadow-glass: 0 10px 30px rgba(35, 35, 43, 0.14);
--accent-soft: rgba(124, 77, 255, 0.12);
/* On light glass: dark text carries contrast, violet text uses deeper mix */
--text-title: #1B1B22;
--text-body: rgba(27, 27, 34, 0.88);
--text-caption: rgba(27, 27, 34, 0.62); /* ≥4.5:1 on light glass */
--accent-text: #5B2EDB; /* ≥4.5:1 on light glass */
--panel-solid: #FBFAFE;
--panel-border: rgba(35, 35, 43, 0.10);
--focus-ring: #5B2EDB;
--live: #0F7A36; /* #34C759 is only 2.13:1 on light glass — darken per AA rule */
--danger: #C03434;
}
/* ---------- 2. BASE ---------- */
.glass-app {
font-family: var(--font);
font-size: var(--fs-body);
line-height: var(--lh-body);
color: var(--text-body);
background: var(--bg-gradient);
min-height: 100%;
-webkit-font-smoothing: antialiased;
/* Violet glows live UNDER glass, never behind text: painted on the root only */
background-image:
radial-gradient(60% 32% at 78% 12%, rgba(124, 77, 255, 0.14), transparent 70%),
radial-gradient(50% 28% at 18% 88%, rgba(124, 77, 255, 0.08), transparent 70%),
var(--bg-gradient);
}
.glass-app *, *::before, *::after { box-sizing: border-box; }
h1, h2, h3 { color: var(--text-title); font-weight: 700; margin: 0; }
.caption {
font-size: var(--fs-caption);
letter-spacing: var(--ls-caption);
color: var(--text-caption);
font-weight: 500; /* no weight below 400 on glass; 500 keeps captions crisp */
}
.no-italic, .glass-app em { font-style: normal; }
/* Focus ring on every interactive element, visible over blur */
.glass-app :focus-visible {
outline: var(--focus-width) solid var(--focus-ring);
outline-offset: var(--focus-offset);
border-radius: inherit;
}
/* Skip link */
.skip-link {
position: absolute; left: var(--sp-2); top: -48px; z-index: 100;
background: var(--accent); color: #fff; padding: var(--sp-2) var(--sp-4);
border-radius: var(--r-input); text-decoration: none; font-weight: 600;
transition: top 0.15s ease;
}
.skip-link:focus { top: var(--sp-2); }
/* ---------- 3. GLASS PRIMITIVE ---------- */
.glass {
background: var(--glass-bg);
border: 1px solid var(--glass-border);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
box-shadow: var(--glass-highlight), var(--shadow-glass);
}
/* Solid fallback when backdrop-filter is unsupported: opaque surface, same border */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
.glass { background: var(--panel-solid); }
}
/* Text-on-glass safety: 90%-opaque overlay behind text blocks that sit over a
glow or artwork. The blur never carries contrast — this does. */
.glass--text-safe::after {
content: ""; position: absolute; inset: 0; z-index: -1;
background: rgba(27, 27, 34, 0.9);
border-radius: inherit;
}
[data-theme="light"] .glass--text-safe::after { background: rgba(251, 250, 254, 0.92); }
/* ---------- 4. STATUS BAR ---------- */
.status-bar {
display: flex; justify-content: space-between; align-items: center;
padding: var(--sp-2) var(--sp-5);
color: var(--text-title);
font-size: 14px; font-weight: 600;
font-variant-numeric: tabular-nums;
}
.status-bar .sys-icons { display: flex; gap: var(--sp-2); align-items: center; }
.status-bar svg { display: block; }
/* ---------- 5. NOW PLAYING BAR (hero glass) ---------- */
.np-bar {
position: sticky; top: 0; z-index: 20;
display: grid;
grid-template-columns: 48px 1fr;
gap: var(--sp-3);
align-items: center;
padding: var(--sp-3) var(--sp-4);
border-radius: var(--r-card);
margin: 0 var(--sp-4);
}
.np-chip {
width: 48px; height: 48px;
border-radius: 14px;
display: grid; place-items: center;
background: var(--accent);
color: #fff;
box-shadow: 0 4px 14px rgba(124, 77, 255, 0.45);
}
.np-title {
color: var(--text-title); font-weight: 700; font-size: 15px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.np-meta {
display: flex; justify-content: space-between; align-items: center;
gap: var(--sp-2);
}
.np-meta .caption { font-variant-numeric: tabular-nums; }
.signal-bars { display: inline-flex; gap: 2px; align-items: flex-end; height: 10px; }
.signal-bars i { width: 3px; background: var(--live); border-radius: 1px; }
.signal-bars i:nth-child(1) { height: 4px; } .signal-bars i:nth-child(2) { height: 6px; }
.signal-bars i:nth-child(3) { height: 8px; } .signal-bars i:nth-child(4) { height: 10px; opacity: 0.35; }
.live-dot {
display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: var(--live); margin-right: var(--sp-1);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: 0.45; } }
/* ---------- 6. NOW PLAYING DETAIL ---------- */
.np-hero-title { font-size: var(--fs-hero); line-height: 1.2; }
.np-detail { padding: var(--sp-6) var(--sp-5) var(--sp-4); }
.np-artwork-row { display: flex; gap: var(--sp-4); align-items: flex-end; margin-top: var(--sp-5); }
.np-artwork {
width: 132px; height: 132px; border-radius: var(--r-card);
background:
radial-gradient(120% 120% at 20% 10%, rgba(124,77,255,0.55), transparent 60%),
linear-gradient(160deg, #2C2C3A, #1E1E28);
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass);
display: grid; place-items: center; color: var(--accent-text);
}
.station-chip {
display: inline-flex; align-items: center; gap: var(--sp-2);
padding: var(--sp-2) var(--sp-4);
border-radius: var(--r-pill);
font-size: 13px; font-weight: 600;
color: var(--text-body);
}
.station-chip .np-chip { width: 24px; height: 24px; border-radius: 8px; }
.station-chip .np-chip svg { width: 14px; height: 14px; }
/* Volume slider — glass track, violet fill + thumb */
.volume {
--fill: 72%;
margin-top: var(--sp-6);
}
.volume .caption { display: flex; justify-content: space-between; margin-bottom: var(--sp-2); }
input[type="range"].vol {
-webkit-appearance: none; appearance: none;
width: 100%; height: 10px; border-radius: var(--r-pill);
background:
linear-gradient(90deg, var(--accent) var(--fill), transparent var(--fill)),
var(--glass-bg);
border: 1px solid var(--border-interactive);
box-shadow: var(--glass-highlight);
}
input[type="range"].vol::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 24px; height: 24px; border-radius: 50%;
background: var(--accent);
border: 2px solid #fff;
box-shadow: 0 4px 12px rgba(124, 77, 255, 0.5);
cursor: grab;
}
input[type="range"].vol::-moz-range-thumb {
width: 24px; height: 24px; border-radius: 50%;
background: var(--accent); border: 2px solid #fff;
box-shadow: 0 4px 12px rgba(124, 77, 255, 0.5);
}
input[type="range"].vol::-moz-range-progress { background: var(--accent); height: 10px; border-radius: var(--r-pill); }
/* Transport row + FAB */
.transport {
display: flex; align-items: center; justify-content: center;
gap: var(--sp-6); margin-top: var(--sp-8);
}
.icon-btn {
width: 48px; height: 48px; border-radius: var(--r-pill);
display: grid; place-items: center;
border: 1px solid var(--glass-border);
background: var(--glass-bg); color: var(--text-body);
cursor: pointer;
box-shadow: var(--glass-highlight);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
transition: background 0.15s ease, transform 0.1s ease;
}
.icon-btn:hover { background: var(--glass-bg-strong); color: var(--text-title); }
.icon-btn:active { transform: scale(0.94); }
.icon-btn.is-on { background: var(--accent-soft); color: var(--accent-text); border-color: var(--accent); }
.fab {
width: 64px; height: 64px; border-radius: var(--r-pill);
border: none; cursor: pointer;
background: var(--accent); color: #fff; /* #FFF on #7C4DFF = 4.81:1 */
display: grid; place-items: center;
box-shadow: var(--shadow-fab);
transition: background 0.15s ease, transform 0.1s ease;
}
.fab:hover { background: var(--accent-hover); }
.fab:active { background: var(--accent-pressed); transform: scale(0.95); }
.fab--sm { width: 52px; height: 52px; }
/* ---------- 7. SEGMENTED TAB STRIP ---------- */
.seg-tabs {
display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
gap: var(--sp-1);
margin: var(--sp-5) var(--sp-4) 0;
padding: var(--sp-1);
border-radius: var(--r-input);
}
.seg-tab {
display: flex; flex-direction: column; align-items: center; gap: 2px;
padding: var(--sp-2) var(--sp-1);
border: none; background: transparent; cursor: pointer;
border-radius: 10px;
color: var(--text-caption);
font: 600 12px/1.35 var(--font);
letter-spacing: var(--ls-caption);
position: relative;
}
.seg-tab svg { width: 20px; height: 20px; }
.seg-tab:hover { color: var(--text-body); }
.seg-tab[aria-selected="true"] {
color: var(--accent-text); /* AA on glass */
}
.seg-tab[aria-selected="true"]::after {
content: ""; position: absolute; bottom: 2px; left: 25%; right: 25%;
height: 2px; border-radius: 2px; background: var(--accent);
}
/* ---------- 8. CONTENT PANEL (solid — glass is sparing) ---------- */
.panel {
margin: var(--sp-4);
background: var(--panel-solid);
border: 1px solid var(--panel-border);
border-radius: var(--r-card);
overflow: clip;
}
.panel-head {
display: flex; justify-content: space-between; align-items: baseline;
padding: var(--sp-4) var(--sp-4) var(--sp-2);
}
.row-item {
display: grid; grid-template-columns: 40px 1fr auto; gap: var(--sp-3);
align-items: center;
padding: var(--sp-3) var(--sp-4);
border: none; background: transparent; width: 100%; text-align: left;
color: var(--text-body); cursor: pointer;
font: inherit;
}
.row-item + .row-item { border-top: 1px solid var(--panel-border); }
.row-item:hover { background: rgba(255, 255, 255, 0.04); }
.row-item .t { color: var(--text-title); font-weight: 600; font-size: 15px; }
.row-thumb {
width: 40px; height: 40px; border-radius: 10px;
background: var(--accent-soft); color: var(--accent-text);
display: grid; place-items: center;
}
/* ---------- 9. BOTTOM NAV ---------- */
.bottom-nav {
position: sticky; bottom: 0; z-index: 20;
display: grid; grid-auto-flow: column; grid-auto-columns: 1fr;
padding: var(--sp-2) var(--sp-2) calc(var(--sp-2) + var(--safe-bottom));
border-radius: var(--r-card) var(--r-card) 0 0;
margin: 0 var(--sp-2);
}
.nav-item {
display: flex; flex-direction: column; align-items: center; gap: 2px;
padding: var(--sp-2) var(--sp-1);
min-height: 48px; /* hit target ≥44px */
border: none; background: transparent; cursor: pointer;
border-radius: var(--r-input);
color: var(--text-caption);
font: 600 11px/1.3 var(--font); letter-spacing: var(--ls-caption);
}
.nav-item svg { width: 22px; height: 22px; }
.nav-item:hover { color: var(--text-body); }
/* Active = unmistakable: violet fill behind the icon, not a subtle tint */
.nav-item[aria-current="page"] { color: var(--accent-text); }
.nav-item[aria-current="page"] .nav-ico {
background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent);
border-radius: var(--r-pill);
}
.nav-ico { display: grid; place-items: center; width: 44px; height: 26px; }
/* ---------- 10. ACTION ROW ---------- */
.action-row {
display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3);
padding: var(--sp-3) var(--sp-4);
}
.btn-glass {
display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
min-height: 52px; padding: 0 var(--sp-5);
border-radius: var(--r-input);
font: 700 15px var(--font);
cursor: pointer;
color: var(--text-title);
border: 1px solid var(--border-interactive);
background: var(--glass-bg);
box-shadow: var(--glass-highlight), var(--shadow-glass);
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
transition: background 0.15s ease, transform 0.1s ease;
}
.btn-glass:hover { background: var(--glass-bg-strong); }
.btn-glass:active { transform: scale(0.98); }
.btn-glass--primary {
background: var(--accent); color: #fff; border-color: transparent;
}
.btn-glass--primary:hover { background: var(--accent-hover); }
.btn-glass--primary:active { background: var(--accent-pressed); }
/* ---------- 11. TOAST + MODAL ---------- */
.toast {
position: fixed; left: 50%; bottom: calc(96px + var(--safe-bottom));
transform: translateX(-50%);
padding: var(--sp-3) var(--sp-5);
border-radius: var(--r-pill);
color: var(--text-title); font-size: 14px; font-weight: 600;
z-index: 60; max-width: min(92vw, 420px); text-align: center;
}
.modal-backdrop {
position: fixed; inset: 0; z-index: 50;
background: rgba(10, 10, 14, 0.6);
display: grid; place-items: center; padding: var(--sp-5);
}
.modal {
width: min(92vw, 380px);
border-radius: var(--r-card);
padding: var(--sp-6);
background: var(--panel-solid); /* solid: text never relies on blur */
border: 1px solid var(--glass-border);
box-shadow: var(--shadow-glass);
}
.modal h3 { font-size: var(--fs-title); margin-bottom: var(--sp-2); }
.modal p { margin: 0 0 var(--sp-5); }
.modal-actions { display: flex; justify-content: flex-end; gap: var(--sp-3); }
.link-btn {
border: none; background: none; cursor: pointer;
font: 700 15px var(--font); color: var(--accent-text);
padding: var(--sp-2) var(--sp-3); border-radius: 10px;
}
.link-btn--danger { color: var(--danger); }
/* ---------- 12. ACCESSIBILITY FALLBACKS ---------- */
@media (prefers-reduced-transparency: reduce) {
:root { --glass-bg: #262631; --glass-bg-strong: #2C2C38; }
[data-theme="light"] { --glass-bg: #FFFFFF; --glass-bg-strong: #F4F3FA; }
.glass, .icon-btn, .nav-item, .btn-glass, .toast, .seg-tabs {
-webkit-backdrop-filter: none; backdrop-filter: none;
background: var(--glass-bg);
}
.modal-backdrop { background: rgba(10, 10, 14, 0.92); }
}
@media (prefers-reduced-motion: reduce) {
.live-dot { animation: none; }
*, *::before, *::after { transition: none !important; }
.icon-btn:active, .fab:active, .btn-glass:active { transform: none; }
}
@media (forced-colors: active) {
.glass, .icon-btn, .btn-glass, .seg-tabs, .bottom-nav, .toast, .modal, .panel {
background: Canvas; border: 1px solid CanvasText;
backdrop-filter: none; -webkit-backdrop-filter: none;
box-shadow: none; forced-color-adjust: none;
}
.np-chip, .fab { background: Highlight; color: HighlightText; border: 1px solid CanvasText; }
.seg-tab[aria-selected="true"], .nav-item[aria-current="page"] { color: Highlight; }
.seg-tab[aria-selected="true"]::after { background: Highlight; }
.glass-app :focus-visible { outline: 3px solid Highlight; }
}
/* demo-page chrome only (not part of the theme) */
body { margin: 0; }
.demo-wrap {
display: grid; grid-template-columns: auto 1fr; gap: 32px;
justify-content: center; align-items: start;
padding: 32px; min-height: 100vh;
}
@media (max-width: 980px) { .demo-wrap { grid-template-columns: 1fr; } }
.phone {
width: 390px; height: 844px; border-radius: 44px; overflow: hidden;
border: 1px solid rgba(255,255,255,0.14);
box-shadow: 0 30px 80px rgba(0,0,0,0.6);
display: flex; flex-direction: column;
position: relative;
}
.phone-scroll { flex: 1; overflow-y: auto; scrollbar-width: thin; }
.demo-side { max-width: 560px; color: var(--text-body); font: 400 15px/1.6 var(--font); }
.demo-side h1 { font-size: 26px; margin-bottom: 4px; }
.demo-side h2 { font-size: 17px; margin: 28px 0 10px; }
.controls { display: flex; gap: 10px; flex-wrap: wrap; margin: 14px 0 6px; }
.chip-toggle {
border: 1px solid var(--border-interactive); background: var(--glass-bg);
color: var(--text-body); font: 600 13px var(--font);
padding: 8px 16px; border-radius: 999px; cursor: pointer;
}
.chip-toggle[aria-pressed="true"] { background: var(--accent); color: #fff; border-color: transparent; }
table.ratios { width: 100%; border-collapse: collapse; font-size: 13px; }
table.ratios th, table.ratios td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--panel-border); }
table.ratios th { color: var(--text-title); font-weight: 600; }
table.ratios td.pass { color: var(--live); font-weight: 700; }
.sw { display: inline-block; width: 12px; height: 12px; border-radius: 3px; border: 1px solid var(--glass-border); vertical-align: -1px; margin-right: 6px; }
kbd {
font: 600 12px ui-monospace, monospace; background: var(--panel-solid);
border: 1px solid var(--border-interactive); border-radius: 6px; padding: 2px 7px;
color: var(--text-title);
}
</style>
</head>
<body class="glass-app">
<a class="skip-link" href="#main">Skip to player</a>
<div class="demo-wrap">
<!-- ================= PHONE ================= -->
<div class="phone glass-app" role="application" aria-label="SatsAmp demo device">
<div class="status-bar" aria-hidden="true">
<span>16:29</span>
<span class="sys-icons">
<!-- signal / wifi / battery -->
<svg width="18" height="12" viewBox="0 0 18 12" fill="currentColor"><rect y="8" width="3" height="4" rx="1"/><rect x="5" y="5" width="3" height="7" rx="1"/><rect x="10" y="2.5" width="3" height="9.5" rx="1"/><rect x="15" width="3" height="12" rx="1"/></svg>
<svg width="16" height="12" viewBox="0 0 16 12" fill="currentColor"><path d="M8 10.6 6 8.3a3.4 3.4 0 0 1 4 0l-2 2.3Zm-3.8-4.4a6.5 6.5 0 0 1 7.6 0l-1.4 1.6a4.4 4.4 0 0 0-4.8 0L4.2 6.2ZM2.1 3.5a10 10 0 0 1 11.8 0l-1.4 1.6a7.9 7.9 0 0 0-9 0L2.1 3.5Z"/></svg>
<svg width="25" height="12" viewBox="0 0 25 12" fill="none"><rect x=".5" y=".5" width="21" height="11" rx="3" stroke="currentColor" opacity=".5"/><rect x="2" y="2" width="15" height="8" rx="1.6" fill="currentColor"/><path d="M23.5 4v4a2.2 2.2 0 0 0 0-4Z" fill="currentColor" opacity=".5"/></svg>
</span>
</div>
<div class="phone-scroll" id="main">
<!-- 2. NOW PLAYING BAR — hero glass -->
<header class="np-bar glass glass--text-safe">
<div class="np-chip" aria-hidden="true">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="2.2" fill="currentColor" stroke="none"/><path d="M8.5 8.5a5 5 0 0 0 0 7M15.5 8.5a5 5 0 0 1 0 7M5.6 5.6a9 9 0 0 0 0 12.8M18.4 5.6a9 9 0 0 1 0 12.8"/></svg>
</div>
<div>
<div class="np-title">KXLU 88.9 FM — Late Night</div>
<div class="np-meta">
<span class="caption"><span class="live-dot"></span>LIVE · 01:24:07</span>
<span class="caption">
<span class="signal-bars" role="img" aria-label="signal 3 of 4"><i></i><i></i><i></i><i></i></span>
&nbsp;buffering 42%
</span>
</div>
</div>
</header>
<!-- 3. NOW PLAYING DETAIL -->
<section class="np-detail" aria-label="Now playing">
<p class="caption" style="margin:0 0 6px">NOW PLAYING</p>
<h1 class="np-hero-title">Night Signals</h1>
<div class="np-artwork-row">
<div class="np-artwork" role="img" aria-label="Station artwork">
<svg width="44" height="44" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M9 18V6l10-2v12"/><circle cx="6.5" cy="18" r="2.5"/><circle cx="16.5" cy="16" r="2.5"/></svg>
</div>
<span class="station-chip glass glass--text-safe">
<span class="np-chip" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/><path d="M8.8 8.8a4.5 4.5 0 0 0 0 6.4M15.2 8.8a4.5 4.5 0 0 1 0 6.4"/></svg></span>
KXLU 88.9
</span>
</div>
<div class="volume">
<label class="caption" for="vol"><span>Volume</span><span id="volout">72%</span></label>
<input class="vol" id="vol" type="range" min="0" max="100" value="72" style="--fill:72%"
aria-valuetext="72 percent">
</div>
<div class="transport" role="group" aria-label="Playback">
<button class="icon-btn" aria-label="Previous station">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor"><path d="M7 6h2v12H7zM19 6l-8 6 8 6V6Z"/></svg>
</button>
<button class="icon-btn" aria-label="Stop">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
</button>
<button class="fab" id="playfab" aria-label="Pause" aria-pressed="true">
<svg width="26" height="26" viewBox="0 0 24 24" fill="currentColor" id="fabicon"><rect x="6.5" y="5" width="4" height="14" rx="1.5"/><rect x="13.5" y="5" width="4" height="14" rx="1.5"/></svg>
</button>
<button class="icon-btn is-on" id="shuffle" aria-label="Shuffle" aria-pressed="true">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M16 3h5v5M4 20 21 3M21 16v5h-5M15 15l6 6M4 4l5 5"/></svg>
</button>
</div>
</section>
<!-- 4. SEGMENTED TAB STRIP -->
<div class="seg-tabs glass" role="tablist" aria-label="Content sections">
<button class="seg-tab" role="tab" aria-selected="true" tabindex="0">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/><path d="M8.8 8.8a4.5 4.5 0 0 0 0 6.4M15.2 8.8a4.5 4.5 0 0 1 0 6.4M6 6a8.5 8.5 0 0 0 0 12M18 6a8.5 8.5 0 0 1 0 12"/></svg>
Stations
</button>
<button class="seg-tab" role="tab" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 19V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v14M4 19a2 2 0 0 0 2 2h11M9 7h5"/></svg>
Library
</button>
<button class="seg-tab" role="tab" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12h4l2.5 6L14 5l2.5 7H21"/></svg>
Streams
</button>
<button class="seg-tab" role="tab" aria-selected="false" tabindex="-1">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h10M4 12h10M4 18h7M17 14v7M14 17.5 17 21l3-3.5"/></svg>
Playlists
</button>
</div>
<!-- content on a SOLID panel — glass is sparing -->
<section class="panel" aria-label="Stations list">
<div class="panel-head">
<h2 style="font-size:17px">Recent stations</h2>
<span class="caption">4</span>
</div>
<button class="row-item"><span class="row-thumb"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span><span><span class="t">KXLU 88.9 FM</span><br><span class="caption">Late night · 128k</span></span><span class="caption">LIVE</span></button>
<button class="row-item"><span class="row-thumb"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span><span><span class="t">Apollo FM</span><br><span class="caption">Imported folder · 12 songs</span></span><span class="caption">OFFLINE</span></button>
<button class="row-item"><span class="row-thumb"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span><span><span class="t">Zaza All</span><br><span class="caption">Imported folder · 31 songs</span></span><span class="caption">OFFLINE</span></button>
<button class="row-item"><span class="row-thumb"><svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></span><span><span class="t">Deep House Radio</span><br><span class="caption">Stream · 320k</span></span><span class="caption">BUFFERING</span></button>
</section>
<!-- 6. ACTION ROW -->
<div class="action-row">
<button class="btn-glass btn-glass--primary">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
Play
</button>
<button class="btn-glass">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h16M4 12h10M4 18h7"/></svg>
Queue
</button>
</div>
</div>
<!-- 5. BOTTOM NAV -->
<nav class="bottom-nav glass" aria-label="Primary">
<button class="nav-item" aria-current="page"><span class="nav-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/><path d="M8.8 8.8a4.5 4.5 0 0 0 0 6.4M15.2 8.8a4.5 4.5 0 0 1 0 6.4M6 6a8.5 8.5 0 0 0 0 12M18 6a8.5 8.5 0 0 1 0 12"/></svg></span>Stations</button>
<button class="nav-item"><span class="nav-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 19V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v14M4 19a2 2 0 0 0 2 2h11M9 7h5"/></svg></span>Library</button>
<button class="nav-item"><span class="nav-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12h4l2.5 6L14 5l2.5 7H21"/></svg></span>Streams</button>
<button class="nav-item"><span class="nav-ico"><svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 6h10M4 12h10M4 18h7M17 14v7M14 17.5 17 21l3-3.5"/></svg></span>Playlists</button>
</nav>
</div>
<!-- ================= SIDE PANEL ================= -->
<aside class="demo-side">
<h1>SatsAmp Glass</h1>
<p class="caption">GLASSMORPHISM THEME · AUDIO / RADIO · WCAG 2.2 AA</p>
<div class="controls" role="group" aria-label="Demo controls">
<button class="chip-toggle" id="themeBtn" aria-pressed="false">Light mode</button>
<button class="chip-toggle" id="modalBtn">Open modal (Esc closes)</button>
</div>
<p style="margin-top:14px">
<kbd>Tab</kbd> walks skip link → bar → transport → tabs → rows → nav.
Arrow keys move within the tablist. Every stop shows the 2px <span style="color:var(--accent-text)">#A78BFF</span>
focus ring at 3px offset, visible over glass.
</p>
<h2>Contrast (measured, not estimated)</h2>
<table class="ratios" id="ratioTable">
<thead><tr><th>Token</th><th>On</th><th>Ratio</th><th>Needs</th><th></th></tr></thead>
<tbody></tbody>
</table>
<p class="caption" style="margin-top:12px">
The reference accent #7C4DFF fails on glass (2.71:1), so text/active labels on glass
use <b>--accent-text #A78BFF</b> (4.83:1). #7C4DFF stays on fills, where white on it is 4.81:1.
The decorative 10%-white glass border fails 3:1, so interactive outlines use a
36%-white border (3.0:1). Blur never carries contrast: text sits on 90%-opaque overlays or solid panels.
</p>
</aside>
</div>
<!-- toast + modal (dismiss with Esc) -->
<div class="toast glass glass--text-safe" id="toast" role="status" hidden>Added to queue</div>
<div class="modal-backdrop" id="modalWrap" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<h3 id="mTitle">Delete “Zaza All”?</h3>
<p>You can delete just the folder group — the songs stay in All Songs — or delete the songs and their files too.</p>
<div class="modal-actions">
<button class="link-btn" data-close>Cancel</button>
<button class="link-btn" data-close>Group only</button>
<button class="link-btn link-btn--danger" data-close>Group + songs</button>
</div>
</div>
</div>
<script>
// volume slider fill
const vol = document.getElementById('vol');
vol.addEventListener('input', () => {
vol.style.setProperty('--fill', vol.value + '%');
document.getElementById('volout').textContent = vol.value + '%';
vol.setAttribute('aria-valuetext', vol.value + ' percent');
});
// play/pause FAB
const fab = document.getElementById('playfab');
fab.addEventListener('click', () => {
const on = fab.getAttribute('aria-pressed') === 'true';
fab.setAttribute('aria-pressed', String(!on));
fab.setAttribute('aria-label', on ? 'Play' : 'Pause');
document.getElementById('fabicon').innerHTML = on
? '<path d="M8 5v14l11-7z"/>'
: '<rect x="6.5" y="5" width="4" height="14" rx="1.5"/><rect x="13.5" y="5" width="4" height="14" rx="1.5"/>';
});
document.getElementById('shuffle').addEventListener('click', e => {
const b = e.currentTarget, on = b.getAttribute('aria-pressed') === 'true';
b.setAttribute('aria-pressed', String(!on)); b.classList.toggle('is-on', !on);
});
// roving tablist with arrow keys
const tabs = [...document.querySelectorAll('.seg-tab')];
tabs.forEach((t, i) => t.addEventListener('keydown', e => {
let j = null;
if (e.key === 'ArrowRight') j = (i + 1) % tabs.length;
if (e.key === 'ArrowLeft') j = (i - 1 + tabs.length) % tabs.length;
if (j !== null) { e.preventDefault(); selectTab(j); tabs[j].focus(); }
}));
function selectTab(j) { tabs.forEach((t, k) => {
t.setAttribute('aria-selected', String(k === j));
t.tabIndex = k === j ? 0 : -1;
}); }
tabs.forEach((t, i) => t.addEventListener('click', () => selectTab(i)));
// toast on Queue
document.querySelectorAll('.action-row .btn-glass')[1].addEventListener('click', () => {
const t = document.getElementById('toast'); t.hidden = false;
clearTimeout(window.__tt); window.__tt = setTimeout(() => t.hidden = true, 2200);
});
// modal with focus trap + Esc
const wrap = document.getElementById('modalWrap');
const modalBtn = document.getElementById('modalBtn');
let lastFocus = null;
modalBtn.addEventListener('click', () => {
lastFocus = document.activeElement; wrap.hidden = false;
wrap.querySelector('[data-close]').focus();
});
wrap.addEventListener('click', e => { if (e.target.closest('[data-close]') || e.target === wrap) close(); });
function close() { wrap.hidden = true; lastFocus && lastFocus.focus(); }
wrap.addEventListener('keydown', e => {
if (e.key === 'Escape') { close(); return; }
if (e.key !== 'Tab') return;
const f = [...wrap.querySelectorAll('button')];
const first = f[0], last = f[f.length - 1];
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
});
// theme toggle
const themeBtn = document.getElementById('themeBtn');
themeBtn.addEventListener('click', () => {
const light = document.documentElement.dataset.theme !== 'light';
document.documentElement.dataset.theme = light ? 'light' : 'dark';
themeBtn.setAttribute('aria-pressed', String(light));
themeBtn.textContent = light ? 'Dark mode' : 'Light mode';
renderRatios();
});
// live contrast measurement against the actual rendered background
function srgb(c){ c/=255; return c<=0.04045 ? c/12.92 : Math.pow((c+0.055)/1.055,2.4); }
function lum(rgb){ const [r,g,b]=rgb.map(srgb); return 0.2126*r+0.7152*g+0.0722*b; }
function ratio(a,b){ const la=lum(a), lb=lum(b); return (Math.max(la,lb)+0.05)/(Math.min(la,lb)+0.05); }
function parse(c){
c = (c || '').trim();
if (c.startsWith('#')) {
const h = c.replace('#','');
if (h.length === 3) return [parseInt(h[0]+h[0],16), parseInt(h[1]+h[1],16), parseInt(h[2]+h[2],16), 1];
return [parseInt(h.slice(0,2),16), parseInt(h.slice(2,4),16), parseInt(h.slice(4,6),16), 1];
}
const m = c.match(/[\d.]+/g);
if (!m || m.length < 3) return [0,0,0,0];
return [Number(m[0]), Number(m[1]), Number(m[2]), m[3] === undefined ? 1 : Number(m[3])];
}
function over(fg, bg){ return [0,1,2].map(i => fg[3]*fg[i] + (1-fg[3])*bg[i]); }
function measuredBg(sel){ // walk ancestors compositing rgba over the base
const el = document.querySelector(sel);
let bg = parse(getComputedStyle(document.documentElement).getPropertyValue('--bg-top').trim() || '#23232B');
if (document.documentElement.dataset.theme === 'light') bg = [0xF2,0xF1,0xF7,1];
let n = el;
while (n && n !== document.body) {
const c = parse(getComputedStyle(n).backgroundColor);
if (c[3] > 0) bg = over(c, bg);
n = n.parentElement;
}
return bg;
}
const ROWS = [
['--text-title (on glass bar)', '.np-title', '4.5:1'],
['--text-body (station chip)', '.station-chip', '4.5:1'],
['--text-caption (on glass bar)', '.np-bar .caption', '4.5:1'],
['--accent-text (active tab)', '.seg-tab[aria-selected="true"]', '4.5:1'],
['--live #34C759 (on base)', '.live-dot', '4.5:1'],
['white on #7C4DFF (FAB)', '#playfab', '4.5:1'],
['--focus-ring vs base', '.np-detail', '3:1'],
];
function renderRatios(){
const tb = document.querySelector('#ratioTable tbody'); tb.innerHTML = '';
const glass = measuredBg('.np-bar');
for (const [label, sel, need] of ROWS) {
const el = document.querySelector(sel);
const cs = getComputedStyle(el);
let bg = measuredBg(sel);
let fg = parse(cs.color);
// focus-ring row: measure ring vs surrounding surface instead
if (label.includes('focus')) {
fg = parse(getComputedStyle(document.documentElement).getPropertyValue('--focus-ring').trim());
}
// live row: dot color measured against the glass bar it sits on
if (label.includes('live')) {
fg = parse(cs.backgroundColor);
bg = measuredBg('.np-bar .caption');
}
if (label.includes('FAB')) { fg = [255,255,255,1]; }
const f = over(fg, bg.map((v,i)=> i<3?v:1));
const r = ratio(f, bg.map((v,i)=> i<3?v:1));
const min = parseFloat(need);
const tr = document.createElement('tr');
tr.innerHTML = `<td><span class="sw" style="background:rgb(${f.map(Math.round)})"></span>${label}</td>
<td>rgb(${bg.slice(0,3).map(Math.round)})</td>
<td>${r.toFixed(2)}:1</td><td>${need}</td>
<td class="pass">${r >= min ? 'PASS' : 'FAIL'}</td>`;
tb.appendChild(tr);
}
}
renderRatios();
</script>
</body>
</html>