Player: light/dark theme so the device stands out from the background

The device case was nearly the same tone as the room — the body's
radial-gradient even peaked *lighter* than the case — so the unit blended
into the background. Added a theme toggle mirroring the editor (◐/☀/☾,
cycles system → light → dark, shares the "metronome.theme" key, with a
pre-paint head script to avoid a flash) and reworked the palette around it:

- dark: a charcoal device sits a clear step lighter than a near-black room,
  with a rim highlight + drop shadow + faint cyan glow so it reads as an object;
- light: the dark device sits on a bright "desk" card (panel + fields go light).

Device internals (OLED, beat LEDs, buttons, knob, screws) keep fixed
dark-hardware colours in both themes via --dtxt/--dmuted, so only the
environment switches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Me Here 2026-05-25 19:16:13 -05:00
parent 553a56d1ec
commit a86421eb5c

View file

@ -16,29 +16,57 @@
Audio here is the synthesized voice set (the firmware uses the same scheduler; Audio here is the synthesized voice set (the firmware uses the same scheduler;
on hardware the voices map to CC0 samples on the I2S DAC). One file, no deps. on hardware the voices map to CC0 samples on the I2S DAC). One file, no deps.
--> -->
<script>
// Set theme before first paint (avoids a flash). Shares the editor's "metronome.theme".
(function(){ try{
var p = localStorage.getItem("metronome.theme");
if (p!=="light" && p!=="dark" && p!=="system") p = "system";
document.documentElement.dataset.theme = p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark") : p;
} catch(e){ document.documentElement.dataset.theme = "dark"; } })();
</script>
<style> <style>
/*@BUILD:include:src/base.css@*/ /*@BUILD:include:src/base.css@*/
:root{ :root{
--case:#1b1f27; --case2:#11141a; --edge:#0b0d11; --bezel:#0a0c10; /* environment (themed): room background + page/panel chrome */
--txt:#c7d0db; --muted:#7f8b9a; --cyan:#0AB3F7; --amber:#ffd166; --bg1:#12151c; --bg2:#05070a;
--txt:#c7d0db; --muted:#7f8b9a; --link:#6cb6ff;
--panel-bg:#171b22; --panel-bd:#2a313c; --field-bg:#0e1116; --field-bd:#222a36;
/* the device case — kept a clear step lighter than the room so it reads as an object */
--case:#232a36; --case2:#151a22; --device-bd:#39434f;
--device-shadow:0 0 0 1px rgba(120,150,190,.06), 0 30px 70px rgba(0,0,0,.62), 0 0 46px rgba(10,179,247,.05);
/* device internals — fixed dark-hardware colours in BOTH themes */
--dtxt:#c7d0db; --dmuted:#7f8b9a;
--cyan:#0AB3F7; --amber:#ffd166; --edge:#0b0d11; --bezel:#0a0c10;
--screen:#04140f; --phos:#34e0a0; --phos-dim:#1f6e54; --led-off:#1b2a23; --screen:#04140f; --phos:#34e0a0; --phos-dim:#1f6e54; --led-off:#1b2a23;
} }
:root[data-theme="light"]{
/* light "desk": the dark device sits on a bright surface → strong contrast */
--bg1:#f5f8fc; --bg2:#dde4ec;
--txt:#1e2630; --muted:#5c6776; --link:#1769c4;
--panel-bg:#ffffff; --panel-bd:#d2dae4; --field-bg:#f1f4f8; --field-bd:#cdd6e0;
--case:#2b3340; --case2:#1b212b; --device-bd:#0f141b;
--device-shadow:0 0 0 1px rgba(0,0,0,.05), 0 26px 50px rgba(20,30,50,.30);
}
body{ body{
margin:0; min-height:100vh; padding:28px 16px 48px; margin:0; min-height:100vh; padding:28px 16px 48px;
background:radial-gradient(circle at 50% -8%, #20242c, #0c0e12); background:radial-gradient(circle at 50% -8%, var(--bg1), var(--bg2));
color:var(--txt); color:var(--txt);
display:flex; flex-direction:column; align-items:center; gap:20px; display:flex; flex-direction:column; align-items:center; gap:20px;
} }
a{color:#6cb6ff} a{color:var(--link)}
.topbar{width:100%; max-width:560px; display:flex; align-items:center; justify-content:space-between; font-size:13px; color:var(--muted)} .topbar{width:100%; max-width:560px; display:flex; align-items:center; justify-content:space-between; font-size:13px; color:var(--muted)}
.topbar b{color:var(--txt)} .topbar b{color:var(--txt)}
.topbar-right{ display:flex; align-items:center; gap:12px }
.tbtn{ background:transparent; color:var(--muted); border:1px solid var(--panel-bd); border-radius:8px;
padding:3px 9px; font-size:14px; line-height:1; cursor:pointer }
.tbtn:hover{ color:var(--txt) }
/* ---- the device ---- */ /* ---- the device ---- */
.device{ .device{
width:100%; max-width:560px; position:relative; width:100%; max-width:560px; position:relative;
background:linear-gradient(180deg,var(--case),var(--case2)); background:linear-gradient(180deg,var(--case),var(--case2));
border:1px solid #2a313c; border-radius:22px; padding:22px 22px 26px; border:1px solid var(--device-bd); border-radius:22px; padding:22px 22px 26px;
box-shadow:0 26px 60px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.05), inset 0 -2px 8px rgba(0,0,0,.6); box-shadow:var(--device-shadow), inset 0 1px 0 rgba(255,255,255,.06), inset 0 -2px 8px rgba(0,0,0,.55);
} }
.device::before, .device::after, .device::before, .device::after,
.device .screw{ content:""; position:absolute; width:9px; height:9px; border-radius:50%; .device .screw{ content:""; position:absolute; width:9px; height:9px; border-radius:50%;
@ -50,8 +78,8 @@
.logo{ display:flex; align-items:baseline; gap:9px } .logo{ display:flex; align-items:baseline; gap:9px }
.logo .vk{ font-weight:800; letter-spacing:.22em; color:#fff; font-size:17px; .logo .vk{ font-weight:800; letter-spacing:.22em; color:#fff; font-size:17px;
background:var(--cyan); padding:2px 8px; border-radius:4px; box-shadow:0 0 10px rgba(10,179,247,.5) } background:var(--cyan); padding:2px 8px; border-radius:4px; box-shadow:0 0 10px rgba(10,179,247,.5) }
.logo .model{ color:var(--muted); font-size:12px; letter-spacing:.04em } .logo .model{ color:var(--dmuted); font-size:12px; letter-spacing:.04em }
.pwr{ display:flex; align-items:center; gap:7px; font-size:10px; color:var(--muted); text-transform:uppercase; letter-spacing:.12em } .pwr{ display:flex; align-items:center; gap:7px; font-size:10px; color:var(--dmuted); text-transform:uppercase; letter-spacing:.12em }
.pwr .dot{ width:8px; height:8px; border-radius:50%; background:#2fe07a; box-shadow:0 0 8px #2fe07a } .pwr .dot{ width:8px; height:8px; border-radius:50%; background:#2fe07a; box-shadow:0 0 8px #2fe07a }
/* ---- OLED ---- */ /* ---- OLED ---- */
@ -84,44 +112,48 @@
/* ---- controls ---- */ /* ---- controls ---- */
.controls{ display:flex; align-items:center; justify-content:center; gap:12px; margin:14px 4px 4px; flex-wrap:wrap } .controls{ display:flex; align-items:center; justify-content:center; gap:12px; margin:14px 4px 4px; flex-wrap:wrap }
.btn{ background:linear-gradient(180deg,#2b323d,#1b212a); color:var(--txt); border:1px solid #39424f; .btn{ background:linear-gradient(180deg,#2b323d,#1b212a); color:var(--dtxt); border:1px solid #39424f;
border-radius:11px; padding:12px 14px; font-size:15px; cursor:pointer; min-width:48px; border-radius:11px; padding:12px 14px; font-size:15px; cursor:pointer; min-width:48px;
box-shadow:0 3px 0 #0c0f14, inset 0 1px 0 rgba(255,255,255,.06); user-select:none; transition:transform .04s, box-shadow .04s } box-shadow:0 3px 0 #0c0f14, inset 0 1px 0 rgba(255,255,255,.06); user-select:none; transition:transform .04s, box-shadow .04s }
.btn:active{ transform:translateY(2px); box-shadow:0 1px 0 #0c0f14, inset 0 1px 0 rgba(255,255,255,.06) } .btn:active{ transform:translateY(2px); box-shadow:0 1px 0 #0c0f14, inset 0 1px 0 rgba(255,255,255,.06) }
.btn small{ display:block; font-size:9px; color:var(--muted); letter-spacing:.08em; margin-top:2px } .btn small{ display:block; font-size:9px; color:var(--dmuted); letter-spacing:.08em; margin-top:2px }
.btn.play{ min-width:74px; font-size:20px; background:linear-gradient(180deg,#1f7a4d,#155f3b); border-color:#2e7d32; color:#eafff3 } .btn.play{ min-width:74px; font-size:20px; background:linear-gradient(180deg,#1f7a4d,#155f3b); border-color:#2e7d32; color:#eafff3 }
.btn.play.on{ background:linear-gradient(180deg,#b23b3b,#8f2d2d); border-color:#c0392b } .btn.play.on{ background:linear-gradient(180deg,#b23b3b,#8f2d2d); border-color:#c0392b }
.knob{ width:52px; height:52px; border-radius:50%; background:radial-gradient(circle at 38% 32%,#3a424e,#171b22 72%); .knob{ width:52px; height:52px; border-radius:50%; background:radial-gradient(circle at 38% 32%,#3a424e,#171b22 72%);
border:1px solid #444c58; box-shadow:0 3px 8px rgba(0,0,0,.5), inset 0 1px 1px rgba(255,255,255,.08); position:relative; margin-left:4px } border:1px solid #444c58; box-shadow:0 3px 8px rgba(0,0,0,.5), inset 0 1px 1px rgba(255,255,255,.08); position:relative; margin-left:4px }
.knob::after{ content:""; position:absolute; left:50%; top:6px; width:2px; height:13px; background:var(--cyan); .knob::after{ content:""; position:absolute; left:50%; top:6px; width:2px; height:13px; background:var(--cyan);
border-radius:2px; transform-origin:50% 20px; transform:rotate(var(--a,0deg)); box-shadow:0 0 5px var(--cyan) } border-radius:2px; transform-origin:50% 20px; transform:rotate(var(--a,0deg)); box-shadow:0 0 5px var(--cyan) }
.knob-wrap{ display:flex; flex-direction:column; align-items:center; gap:4px; font-size:9px; color:var(--muted); letter-spacing:.1em } .knob-wrap{ display:flex; flex-direction:column; align-items:center; gap:4px; font-size:9px; color:var(--dmuted); letter-spacing:.1em }
/* ---- speaker grille ---- */ /* ---- speaker grille ---- */
.grille{ height:14px; margin:18px 6px 2px; border-radius:6px; .grille{ height:14px; margin:18px 6px 2px; border-radius:6px;
background:radial-gradient(circle, #000 1.1px, transparent 1.4px) 0 0/9px 9px; opacity:.5 } background:radial-gradient(circle, #000 1.1px, transparent 1.4px) 0 0/9px 9px; opacity:.5 }
/* ---- load panel ---- */ /* ---- load panel ---- */
.panel{ width:100%; max-width:560px; background:#171b22; border:1px solid #2a313c; border-radius:14px; padding:16px } .panel{ width:100%; max-width:560px; background:var(--panel-bg); border:1px solid var(--panel-bd); border-radius:14px; padding:16px }
.panel h2{ margin:0 0 4px; font-size:15px } .panel h2{ margin:0 0 4px; font-size:15px }
.panel p.sub{ margin:0 0 12px; font-size:12px; color:var(--muted); line-height:1.45 } .panel p.sub{ margin:0 0 12px; font-size:12px; color:var(--muted); line-height:1.45 }
.panel label{ font-size:12px; color:var(--muted); display:block; margin:10px 0 5px } .panel label{ font-size:12px; color:var(--muted); display:block; margin:10px 0 5px }
textarea{ width:100%; background:#0e1116; color:var(--txt); border:1px solid var(--edge); border-radius:9px; textarea{ width:100%; background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd); border-radius:9px;
padding:10px; font-family:"Courier New",monospace; font-size:12px; resize:vertical; min-height:58px } padding:10px; font-family:"Courier New",monospace; font-size:12px; resize:vertical; min-height:58px }
select, .ld{ background:#0e1116; color:var(--txt); border:1px solid #39424f; border-radius:9px; padding:8px 10px; font-size:13px } select, .ld{ border-radius:9px; padding:8px 10px; font-size:13px }
.ld{ cursor:pointer; background:linear-gradient(180deg,#2b323d,#1b212a) } select{ background:var(--field-bg); color:var(--txt); border:1px solid var(--field-bd) }
.ld{ cursor:pointer; color:var(--dtxt); background:linear-gradient(180deg,#2b323d,#1b212a); border:1px solid #39424f }
.row{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px } .row{ display:flex; gap:8px; align-items:center; flex-wrap:wrap; margin-top:8px }
.status{ margin-top:10px; font-size:12px; min-height:1.2em; font-family:"Courier New",monospace } .status{ margin-top:10px; font-size:12px; min-height:1.2em; font-family:"Courier New",monospace }
.status.ok{ color:#5fd08a } .status.err{ color:#ff7a7a } .status.ok{ color:#5fd08a } .status.err{ color:#ff7a7a }
.hint{ font-size:11px; color:var(--muted) } .hint{ font-size:11px; color:var(--muted) }
code{ background:#0e1116; border:1px solid var(--edge); border-radius:4px; padding:1px 5px; font-size:11px } code{ background:var(--field-bg); border:1px solid var(--field-bd); border-radius:4px; padding:1px 5px; font-size:11px }
</style> </style>
</head> </head>
<body> <body>
<div class="topbar"> <div class="topbar">
<span><b>VARASYS PM1</b> · hardware player (mockup)</span> <span><b>VARASYS PM1</b> · hardware player (mockup)</span>
<a href="/index.html">Open editor ↗</a> <span class="topbar-right">
<button id="themeBtn" class="tbtn" title="toggle light / dark theme"></button>
<a href="/index.html">Open editor ↗</a>
</span>
</div> </div>
<!-- ===================== THE DEVICE ===================== --> <!-- ===================== THE DEVICE ===================== -->
@ -333,6 +365,20 @@ $("bLoad").onclick=()=>loadConfig($("cfg").value);
$("storedSel").onchange=(e)=>{ const v=e.target.value; if(!v) return; $("storedSel").onchange=(e)=>{ const v=e.target.value; if(!v) return;
const sl = v[0]==="b" ? e.target._builtin[+v.slice(1)] : e.target._lists[+v.slice(1)]; if(!sl) return; const sl = v[0]==="b" ? e.target._builtin[+v.slice(1)] : e.target._lists[+v.slice(1)]; if(!sl) return;
loadSetlistObj({title:sl.title,items:(sl.items||[]).map(it=>({...it}))}); setStatus("✓ Loaded set list “"+(sl.title||"set list")+"”.",true); }; loadSetlistObj({title:sl.title,items:(sl.items||[]).map(it=>({...it}))}); setStatus("✓ Loaded set list “"+(sl.title||"set list")+"”.",true); };
/* theme toggle — cycles system → light → dark; shares the editor's "metronome.theme" key */
const THEMES = ["system","light","dark"];
function effectiveTheme(p){ return p==="system" ? (matchMedia("(prefers-color-scheme: light)").matches?"light":"dark") : p; }
function themePref(){ try{ const p=localStorage.getItem("metronome.theme"); return (p==="light"||p==="dark"||p==="system")?p:"system"; }catch(e){ return "system"; } }
function applyTheme(p){
try{ localStorage.setItem("metronome.theme",p); }catch(e){}
document.documentElement.dataset.theme = effectiveTheme(p);
$("themeBtn").textContent = p==="system" ? "◐" : p==="light" ? "☀" : "☾";
$("themeBtn").title = "Theme: "+p+" (click to cycle: system → light → dark)";
}
$("themeBtn").onclick = ()=> applyTheme(THEMES[(THEMES.indexOf(themePref())+1)%THEMES.length]);
matchMedia("(prefers-color-scheme: light)").addEventListener("change", ()=>{ if(themePref()==="system") applyTheme("system"); });
applyTheme(themePref());
addEventListener("keydown",(e)=>{ addEventListener("keydown",(e)=>{
const t=e.target, tag=t?t.tagName:""; if(tag==="TEXTAREA"||tag==="INPUT"||tag==="SELECT") return; const t=e.target, tag=t?t.tagName:""; if(tag==="TEXTAREA"||tag==="INPUT"||tag==="SELECT") return;
const k=e.key; const k=e.key;