diff options
Diffstat (limited to 'docs/prototypes/widgets.js')
| -rw-r--r-- | docs/prototypes/widgets.js | 256 |
1 files changed, 207 insertions, 49 deletions
diff --git a/docs/prototypes/widgets.js b/docs/prototypes/widgets.js index c01972e..c12e5b7 100644 --- a/docs/prototypes/widgets.js +++ b/docs/prototypes/widgets.js @@ -234,17 +234,33 @@ GW.knob = function (host, opts = {}) { return { el: k, get: () => val, set }; }; -/* 06 segmented selector — pick one of a few */ +/* 06 segmented selector — pick one of a few. opts.accent picks the lit-segment + color from GW.segmented.STYLES (amber / green / red); defaults match the + stylesheet fallbacks. setStyle(axis, name) restyles a live instance. */ GW.segmented = function (host, opts = {}) { const onChange = opts.onChange || noop; const items = opts.items || ['TIMER', 'ALARM', 'POMO']; const seg = document.createElement('div'); seg.className = 'seg'; host.appendChild(seg); + const setStyle = (axis, name) => { + const o = (GW.segmented.STYLES[axis] || {})[name]; + if (!o) return; + for (const [k, v] of Object.entries(o.vars)) seg.style.setProperty(k, v); + }; + setStyle('accent', opts.accent || 'amber'); const btns = items.map(t => { const b = document.createElement('button'); b.textContent = t; seg.appendChild(b); return b; }); let idx; const set = i => { idx = i; btns.forEach((b, j) => b.classList.toggle('on', j === i)); onChange(i, items[i]); }; btns.forEach((b, i) => b.addEventListener('click', () => set(i))); set(opts.active || 0); - return { el: seg, get: () => idx, set }; + return { el: seg, get: () => idx, set, setStyle }; +}; +/* named styles per axis: dot = swatch color for pickers, vars = --seg-* overrides */ +GW.segmented.STYLES = { + accent: { + amber: { dot: 'var(--gold-hi)', vars: { '--seg-on-bg': 'linear-gradient(180deg,var(--amber-grad-top),var(--gold))', '--seg-on-ink': 'var(--panel)' } }, + green: { dot: '#8fb944', vars: { '--seg-on-bg': 'linear-gradient(180deg,#a9c95f,var(--pass))', '--seg-on-ink': 'var(--panel)' } }, + red: { dot: 'var(--fail)', vars: { '--seg-on-bg': 'linear-gradient(180deg,#d98a6f,var(--fail))', '--seg-on-ink': 'var(--panel)' } }, + }, }; /* 07 chip toggle — inline binary inside a line of text */ @@ -322,37 +338,89 @@ GW.rotarySelector = function (host, opts = {}) { return { el: rs, get: () => values[idx], set }; }; -/* 25 slide-rule dial — lit pointer on a printed scale; click a mark or arrow-key */ +/* 25 slide-rule dial — lit pointer on a printed scale. The printed numerals + are the majors; the integer units between them get minor ticks and are + selectable too. Click a numeral, a mark, or between marks; ←/→ (↑/↓) step + one unit. set() takes a stop index; get() returns the value. opts.skin picks + a face from GW.slideRule.STYLES (warm backlit / chrome / 80s black glass / marantz blue); + setStyle(axis, name) restyles a live instance. */ GW.slideRule = function (host, opts = {}) { const onChange = opts.onChange || noop; - const values = opts.values || [4, 6, 8, 10, 12]; + const majors = opts.values || [4, 6, 8, 10, 12]; const fmt = opts.fmt || (v => 'pos ' + v); const X = [12, 51, 90, 129, 168]; + /* expand to unit stops: every integer between adjacent numeric majors */ + const stops = []; + majors.forEach((v, i) => { + stops.push({ v, x: X[i], major: true }); + const b = majors[i + 1]; + if (typeof v === 'number' && typeof b === 'number') + for (let u = v + 1; u < b; u++) + stops.push({ v: u, x: X[i] + (X[i + 1] - X[i]) * (u - v) / (b - v), major: false }); + }); const t = document.createElement('span'); t.className = 'tuner'; t.tabIndex = 0; t.setAttribute('role', 'slider'); t.setAttribute('aria-label', 'slide-rule value'); - values.forEach((v, i) => { - t.insertAdjacentHTML('beforeend', `<span class="tick" style="left:${X[i]}px"></span><span class="mk" style="left:${X[i]}px">${v}</span>`); + const setStyle = (axis, name) => { + const o = (GW.slideRule.STYLES[axis] || {})[name]; + if (!o) return; + for (const [k, v] of Object.entries(o.vars)) t.style.setProperty(k, v); + }; + setStyle('skin', opts.skin || 'warm'); + stops.forEach(s => { + t.insertAdjacentHTML('beforeend', s.major + ? `<span class="tick" style="left:${s.x}px"></span><span class="mk" style="left:${s.x}px">${s.v}</span>` + : `<span class="tick mn" style="left:${s.x}px"></span>`); }); const ndl = document.createElement('span'); ndl.className = 'ndl'; t.appendChild(ndl); host.appendChild(t); let idx; const set = i => { - idx = Math.max(0, Math.min(values.length - 1, i)); - ndl.style.left = X[idx] + 'px'; - t.querySelectorAll('.mk').forEach((m, j) => m.classList.toggle('on', j === idx)); - onChange(values[idx], fmt(values[idx])); + idx = Math.max(0, Math.min(stops.length - 1, i)); + ndl.style.left = stops[idx].x + 'px'; + t.querySelectorAll('.mk').forEach(m => m.classList.toggle('on', +m.textContent === stops[idx].v)); + onChange(stops[idx].v, fmt(stops[idx].v)); }; t.addEventListener('click', e => { - const r = t.getBoundingClientRect(), x = e.clientX - r.left; - let best = 0, bd = 1e9; X.slice(0, values.length).forEach((tx, j) => { const d = Math.abs(tx - x); if (d < bd) { bd = d; best = j; } }); + const r = t.getBoundingClientRect(); + /* rect is visual px but stop coords are layout px — divide the CSS zoom out */ + const x = (e.clientX - r.left) * (t.offsetWidth / r.width); + let best = 0, bd = 1e9; stops.forEach((s, j) => { const d = Math.abs(s.x - x); if (d < bd) { bd = d; best = j; } }); set(best); t.focus(); }); t.addEventListener('keydown', e => { if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); set(idx - 1); } else if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); set(idx + 1); } }); - set(opts.index !== undefined ? opts.index : 2); - return { el: t, get: () => values[idx], set }; + const initV = opts.value !== undefined ? opts.value : majors[opts.index !== undefined ? opts.index : 2]; + const init = stops.findIndex(s => s.v === initV); + set(init < 0 ? 0 : init); + return { el: t, get: () => stops[idx].v, set, setStyle }; +}; +/* named faces: dot = swatch color for pickers, vars = --tn-* overrides. + warm restates the stylesheet fallbacks so switching back is exact. */ +GW.slideRule.STYLES = { + skin: { + warm: { dot: 'var(--gold-hi)', vars: { + '--tn-bg': 'linear-gradient(180deg,#191510,#0b0908)', '--tn-brd': '#2a251c', + '--tn-glow': 'rgba(var(--glow-lo),.12)', '--tn-tick': 'var(--steel)', '--tn-ink': 'var(--steel)', + '--tn-on': 'var(--gold-hi)', '--tn-onglow': 'rgba(var(--glow-hi),.6)', + '--tn-ndl': 'var(--fail)', '--tn-ndlglow': 'rgba(203,107,77,.85)' } }, + chrome: { dot: '#dfe6f2', vars: { + '--tn-bg': 'linear-gradient(180deg,#e9e9e5,#bcbcb6)', '--tn-brd': '#8e8e88', + '--tn-glow': 'rgba(255,255,255,.28)', '--tn-tick': '#3c3a34', '--tn-ink': '#3c3a34', + '--tn-on': '#14110e', '--tn-onglow': 'none', + '--tn-ndl': 'var(--fail)', '--tn-ndlglow': 'rgba(203,107,77,.45)' } }, + black: { dot: '#1c1a18', vars: { + '--tn-bg': 'linear-gradient(180deg,#17181a,#08090a)', '--tn-brd': '#000', + '--tn-glow': 'rgba(255,255,255,.05)', '--tn-tick': '#c8cac8', '--tn-ink': '#c8cac8', + '--tn-on': '#f2f4f2', '--tn-onglow': 'rgba(242,244,242,.5)', + '--tn-ndl': '#ff3b28', '--tn-ndlglow': 'rgba(255,59,40,.9)' } }, + blue: { dot: '#8fb4d8', vars: { + '--tn-bg': 'linear-gradient(180deg,#0c1520,#070c14)', '--tn-brd': '#1c2c40', + '--tn-glow': 'rgba(120,170,230,.16)', '--tn-tick': '#8fb4d8', '--tn-ink': '#8fb4d8', + '--tn-on': '#cfe4ff', '--tn-onglow': 'rgba(150,200,255,.7)', + '--tn-ndl': '#e8f2ff', '--tn-ndlglow': 'rgba(180,220,255,.9)' } }, + }, }; /* N01 rocker power switch — hard on/off, lit legend */ @@ -676,41 +744,129 @@ GW.flutedKnob = function (host, opts = {}) { return { el: s, get: () => val, set }; }; -/* R05 filter slider bank — one fader per band, teal arrow caps on black tracks */ +/* R05 filter slider bank — a dense fader wall on a real faceplate. Period skins + as constructor opts backed by GW.filterBank.STYLES, three independent axes: + panel (silver hi-fi aluminum / studio black), caps — the cap SHAPE + (chrome T / short ribbed / tall block fader), and capColor — the cap + FINISH (black white-index / color stripes / chrome / cream). opts.style + picks a native trio (silver+chrome+chrome, studio+ribbed+red); + opts.panel / opts.caps / opts.capColor override an axis; setStyle(axis, name) + restyles live (values kept). After the Pioneer SG-9500, the Technics + SH-8065, and the Zaxcom Oasis block faders. */ GW.filterBank = function (host, opts = {}) { const onChange = opts.onChange || noop; - const freqs = opts.freqs || [78, 136, 235, 406, 701, 1210, 2090, 3620]; - const vals = (opts.values || [18, 30, 42, 25, 35, 55, 20, 48]).slice(); - const fmtHz = f => f < 1000 ? f : (f / 1000) + 'k'; - const s = stageSvg(host, 'rsvg', 190, 96); - const y0 = 14, y1 = 84, x0 = 30, dx = 19.5; - for (const [lbl, y] of [['0', y0], ['20', y0 + (y1 - y0) / 3], ['40', y0 + 2 * (y1 - y0) / 3], ['60', y1]]) - svgEl(s, 'text', { x: 14, y: y + 2, 'text-anchor': 'end', 'font-size': 6, 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = lbl; - svgEl(s, 'text', { x: 14, y: 94, 'text-anchor': 'end', 'font-size': 5.5, 'font-family': 'var(--mono)', fill: 'var(--dim)' }).textContent = 'dB'; + const freqs = opts.freqs || [78, 113, 163, 235, 339, 487, 701, 1010, 1450, 2090, 3010, 4340]; + const vals = (opts.values || [18, 30, 42, 25, 35, 55, 20, 48, 38, 26, 44, 32]).slice(0, freqs.length); + while (vals.length < freqs.length) vals.push(30); + const fmtHz = f => f < 1000 ? f : (f / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; + const ST = GW.filterBank.STYLES; + const native = { + silver: { caps: 'chrome', capColor: 'chrome' }, + studio: { caps: 'ribbed', capColor: 'red' }, + }; + const nat = native[opts.style] || {}; + const cur = { + panel: ST.panel[opts.panel] ? opts.panel : (ST.panel[opts.style] ? opts.style : 'silver'), + caps: ST.caps[opts.caps] ? opts.caps : (nat.caps || 'block'), + capColor: ST.capColor[opts.capColor] ? opts.capColor : (nat.capColor || 'black'), + }; + gradDef('fbPlateSilver', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#d8d8d4'], ['.5', '#c6c6c1'], ['1', '#adada7']]); + gradDef('fbPlateStudio', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#221f1c'], ['1', '#131110']]); + gradDef('sfChrome', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f0efec'], ['.45', '#c4c1b9'], ['1', '#75726a']]); + gradDef('fbCapBlack', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#2a2724'], ['1', '#171412']]); + gradDef('fbCapCream', 'linearGradient', { x1: 0, y1: 0, x2: 0, y2: 1 }, [['0', '#f2ecd8'], ['1', '#cfc8ae']]); + const s = stageSvg(host, 'rsvg', 250, 112); + const y0 = 20, y1 = 92, x0 = 36, dx = (214 - x0) / (freqs.length - 1); + const yOf = db => y0 + db / 60 * (y1 - y0); const caps = []; + const place = i => caps[i].setAttribute('transform', `translate(0,${yOf(vals[i])})`); + const drawCap = (g, kind, x) => { + const c = ST.capColor[cur.capColor]; + if (kind === 'chrome') { + svgEl(g, 'rect', { x: x - 6, y: -3.25, width: 12, height: 6.5, rx: .9, fill: c.body, stroke: c.edge, 'stroke-width': .6 }); + /* bare metal needs no painted stripe — chrome-on-chrome stays lineless */ + if (cur.capColor !== 'chrome') + svgEl(g, 'line', { x1: x - 6, y1: 0, x2: x + 6, y2: 0, stroke: c.index, 'stroke-width': 1 }); + svgEl(g, 'line', { x1: x - 6, y1: 2, x2: x + 6, y2: 2, stroke: c.rib, 'stroke-width': .5 }); + } else if (kind === 'ribbed') { + svgEl(g, 'rect', { x: x - 5, y: -4, width: 10, height: 8, rx: 1, fill: c.body, stroke: c.edge, 'stroke-width': .6 }); + for (const dy of [-2.4, 2.4]) + svgEl(g, 'line', { x1: x - 4, y1: dy, x2: x + 4, y2: dy, stroke: c.rib, 'stroke-width': .6 }); + svgEl(g, 'line', { x1: x - 5, y1: 0, x2: x + 5, y2: 0, stroke: c.index, 'stroke-width': 1.1 }); + } else { /* block — tall ribbed fader after the Zaxcom Oasis; fine rib + groups above and below a mid-cap index line that marks the value */ + svgEl(g, 'rect', { x: x - 4.5, y: -8, width: 9, height: 16, rx: 1.1, fill: c.body, stroke: c.edge, 'stroke-width': .6 }); + for (const dy of [-6.4, -4.8, -3.2, -1.6, 1.6, 3.2, 4.8, 6.4]) + svgEl(g, 'line', { x1: x - 3.7, y1: dy, x2: x + 3.7, y2: dy, stroke: c.rib, 'stroke-width': .6 }); + svgEl(g, 'line', { x1: x - 4.5, y1: 0, x2: x + 4.5, y2: 0, stroke: c.index, 'stroke-width': 1.2 }); + } + }; const set = (i, db) => { - db = Math.max(0, Math.min(60, db)); vals[i] = db; - caps[i].setAttribute('transform', `translate(0,${14 + db / 60 * 70})`); + db = Math.max(0, Math.min(60, db)); vals[i] = db; place(i); onChange({ band: i, hz: freqs[i], db }, `${fmtHz(freqs[i])} Hz · −${Math.round(db)} dB`); }; - freqs.forEach((f, i) => { - const x = x0 + i * dx; - svgEl(s, 'text', { x, y: 8, 'text-anchor': 'middle', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: 'var(--steel)' }).textContent = fmtHz(f); - svgEl(s, 'line', { x1: x, y1: y0, x2: x, y2: y1, stroke: '#0a0908', 'stroke-width': 3.4, 'stroke-linecap': 'round' }); - svgEl(s, 'line', { x1: x - .7, y1: y0, x2: x - .7, y2: y1, stroke: 'rgba(255,255,255,.05)', 'stroke-width': .7 }); - const cap = svgEl(s, 'g', {}); - svgEl(cap, 'polygon', { points: `${x - 8.5},${-4.5} ${x - 2.5},0 ${x - 8.5},${4.5}`, fill: 'var(--vfd)', stroke: '#123028', 'stroke-width': .6 }); - svgEl(cap, 'polygon', { points: `${x + 8.5},${-4.5} ${x + 2.5},0 ${x + 8.5},${4.5}`, fill: 'var(--vfd)', stroke: '#123028', 'stroke-width': .6 }); - svgEl(cap, 'circle', { cx: x, cy: 0, r: 1.5, fill: '#123028' }); - caps.push(cap); - /* paint the initial position without announcing it */ - cap.setAttribute('transform', `translate(0,${14 + vals[i] / 60 * 70})`); - const hit = svgEl(s, 'rect', { x: x - 9.5, y: y0 - 6, width: 19, height: y1 - y0 + 12, fill: 'transparent' }); - hit.style.cursor = 'ns-resize'; - dragY(hit, pct => set(i, (100 - pct) / 100 * 60)); - }); + const render = () => { + const st = ST.panel[cur.panel]; + s.replaceChildren(); caps.length = 0; + svgEl(s, 'rect', { x: 1.5, y: 1.5, width: 247, height: 109, rx: 5, fill: st.plate, stroke: st.edge, 'stroke-width': 1 }); + svgEl(s, 'rect', { x: 3, y: 3, width: 244, height: 106, rx: 4, fill: 'none', stroke: st.bevel, 'stroke-width': .7 }); + [[9, 9], [241, 9], [9, 103], [241, 103]].forEach(([sx, sy], k) => { + svgEl(s, 'circle', { cx: sx, cy: sy, r: 2.7, fill: st.screw, stroke: 'rgba(0,0,0,.55)', 'stroke-width': .6 }); + svgEl(s, 'line', { x1: sx - 1.9, y1: sy, x2: sx + 1.9, y2: sy, stroke: 'rgba(0,0,0,.6)', 'stroke-width': .7, transform: `rotate(${25 + k * 40},${sx},${sy})` }); + }); + for (let db = 0; db <= 60; db += 10) { + const y = yOf(db), major = db % 20 === 0, len = major ? 4 : 2.5; + svgEl(s, 'line', { x1: 24 - len, y1: y, x2: 24, y2: y, stroke: st.dim, 'stroke-width': major ? .9 : .6 }); + svgEl(s, 'line', { x1: 226, y1: y, x2: 226 + len, y2: y, stroke: st.dim, 'stroke-width': major ? .9 : .6 }); + if (major) { + svgEl(s, 'text', { x: 17, y: y + 2, 'text-anchor': 'end', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: st.ink }).textContent = db; + svgEl(s, 'text', { x: 233, y: y + 2, 'text-anchor': 'start', 'font-size': 5.2, 'font-family': 'var(--mono)', fill: st.ink }).textContent = db; + } + } + svgEl(s, 'text', { x: 20, y: 100, 'text-anchor': 'end', 'font-size': 4.6, 'font-family': 'var(--mono)', fill: st.dim }).textContent = 'dB'; + freqs.forEach((f, i) => { + const x = x0 + i * dx; + svgEl(s, 'text', { x, y: 13, 'text-anchor': 'middle', 'font-size': 4.6, 'font-family': 'var(--mono)', fill: st.ink }).textContent = fmtHz(f); + svgEl(s, 'line', { x1: x, y1: y0, x2: x, y2: y1, stroke: st.track, 'stroke-width': st.trackW, 'stroke-linecap': 'round' }); + if (st.trackHi) svgEl(s, 'line', { x1: x - st.trackW / 2 - .35, y1: y0, x2: x - st.trackW / 2 - .35, y2: y1, stroke: st.trackHi, 'stroke-width': .7 }); + const cap = svgEl(s, 'g', {}); + drawCap(cap, cur.caps, x); + caps.push(cap); place(i); + const hit = svgEl(s, 'rect', { x: x - dx / 2, y: y0 - 6, width: dx, height: y1 - y0 + 12, fill: 'transparent' }); + hit.style.cursor = 'ns-resize'; + dragY(hit, pct => set(i, (100 - pct) / 100 * 60)); + }); + }; + const setStyle = (axis, name) => { + if (!ST[axis] || !ST[axis][name]) return; + cur[axis] = name; render(); + }; + render(); onChange(null, 'drag a band'); - return { el: s, get: () => vals.slice(), set }; + return { el: s, get: () => vals.slice(), set, setStyle }; +}; +/* named styles per axis: dot = swatch color for pickers. caps is the cap + shape; capColor is the cap finish (body + rib + index line), applicable to + any shape. */ +GW.filterBank.STYLES = { + panel: { + silver: { dot: '#c9c9c5', plate: 'url(#fbPlateSilver)', edge: '#8e8e88', bevel: 'rgba(255,255,255,.4)', ink: '#33332f', dim: '#5c5c56', screw: '#9c9c96', track: '#121210', trackW: 2.0, trackHi: null }, + studio: { dot: '#1c1a18', plate: 'url(#fbPlateStudio)', edge: '#000', bevel: 'rgba(255,255,255,.06)', ink: '#b9b4a6', dim: '#6f6a5e', screw: '#3c3832', track: '#050505', trackW: 2.2, trackHi: 'rgba(255,255,255,.05)' }, + }, + caps: { + block: { dot: '#2c2824' }, + ribbed: { dot: '#6f6a5e' }, + chrome: { dot: '#dfe6f2' }, + }, + capColor: { + black: { dot: '#211e1b', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: '#f2f4f2', edge: '#000' }, + red: { dot: 'var(--sevred)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--sevred)', edge: '#000' }, + green: { dot: 'var(--sevgrn)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--sevgrn)', edge: '#000' }, + blue: { dot: '#4f9fe0', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: '#4f9fe0', edge: '#000' }, + amber: { dot: 'var(--gold-hi)', body: 'url(#fbCapBlack)', rib: 'rgba(255,255,255,.16)', index: 'var(--gold-hi)', edge: '#000' }, + chrome: { dot: '#dfe6f2', body: 'url(#sfChrome)', rib: 'rgba(0,0,0,.28)', index: '#14110e', edge: '#3c3a34' }, + cream: { dot: '#efe9d4', body: 'url(#fbCapCream)', rib: 'rgba(0,0,0,.18)', index: '#3a3128', edge: '#8a8474' }, + }, }; /* R06 chicken-head selector — tapered lever aims at the position */ @@ -3953,7 +4109,7 @@ const GW_CSS = ` .seg{display:flex;border:1px solid #33302b;border-radius:8px;overflow:hidden} .seg button{font:inherit;font-size:11px;color:var(--silver);background:#191715;border:0;border-right:1px solid #33302b;padding:7px 11px;cursor:pointer} .seg button:last-child{border-right:0} -.seg button.on{background:linear-gradient(180deg,var(--amber-grad-top),var(--gold));color:var(--panel);font-weight:700} +.seg button.on{background:var(--seg-on-bg,linear-gradient(180deg,var(--amber-grad-top),var(--gold)));color:var(--seg-on-ink,var(--panel));font-weight:700} /* engraved section label */ .engrave{width:180px;color:var(--steel);font-size:.62rem;letter-spacing:.3em;text-transform:uppercase;cursor:pointer; @@ -3983,14 +4139,16 @@ const GW_CSS = ` /* slide-rule tuner dial */ .tuner{width:180px;height:46px;position:relative;border-radius:6px;overflow:hidden;cursor:pointer; - background:linear-gradient(180deg,#191510,#0b0908);border:1px solid #2a251c; - box-shadow:inset 0 0 20px rgba(var(--glow-lo),.12),inset 0 1px 0 rgba(255,255,255,.03)} -.tuner .tick{position:absolute;top:6px;width:1px;height:11px;background:var(--steel);transform:translateX(-50%)} -.tuner .mk{position:absolute;bottom:8px;transform:translateX(-50%);color:var(--steel);font-size:10px} -.tuner .mk.on{color:var(--gold-hi);text-shadow:0 0 6px rgba(var(--glow-hi),.6)} + background:var(--tn-bg,linear-gradient(180deg,#191510,#0b0908));border:1px solid var(--tn-brd,#2a251c); + box-shadow:inset 0 0 20px var(--tn-glow,rgba(var(--glow-lo),.12)),inset 0 1px 0 rgba(255,255,255,.03)} +.tuner .tick{position:absolute;top:6px;width:1px;height:11px;background:var(--tn-tick,var(--steel));transform:translateX(-50%)} +.tuner .tick.mn{height:6px;opacity:.5} +.tuner .mk{position:absolute;bottom:8px;transform:translateX(-50%);color:var(--tn-ink,var(--steel));font-size:10px} +.tuner .mk.on{color:var(--tn-on,var(--gold-hi));text-shadow:0 0 6px var(--tn-onglow,rgba(var(--glow-hi),.6))} +.tuner:focus{outline:1px solid rgba(var(--glow-lo),.5);outline-offset:2px} .tuner:focus-visible{outline:2px solid var(--gold);outline-offset:2px} .tuner .ndl{position:absolute;top:3px;bottom:3px;width:2px;margin-left:-1px;border-radius:1px; - background:var(--fail);box-shadow:0 0 7px rgba(203,107,77,.85);transition:left .25s} + background:var(--tn-ndl,var(--fail));box-shadow:0 0 7px var(--tn-ndlglow,rgba(203,107,77,.85));transition:left .25s} /* nixie tube */ .nixie{display:inline-flex;gap:5px;cursor:pointer} |
