Rebuild the review page as a channel strip
The app is about operating something — playing a track and setting a level on it — but every screen looked like a form. One metaphor now does three jobs. - Reviewing: a vertical fader beside the text, so the two things you do at once stop being a screen apart. Native range input, so keyboard, focus and form submission are unchanged; on mobile it lies down and the ticks reverse - The reveal: everyone's scores as a row of channels. The silhouette of that row is the spread, which the stats page can only tell you as a number - Profiles: given versus received as two faders, the one comparison that says something about a person The player is now a transport: play/pause, a range input for seeking so arrow keys come free, and a stereo level meter driven by a real AnalyserNode. It is progressive enhancement — the page ships native audio controls and the script takes over, so no JS means the browser's own player. The meter is dark until audio actually plays and stops when it does; reduced motion skips it entirely. Also: hidden scores are hatched rather than blank, the nav carries the queue count, "Seuraava jonossa" keeps the loop going after a review, leaderboards gained level bars and a range bar where divisive is the point, durations read 3:54, both lists can get back to the start, and the admin invite table lists unused codes instead of silently truncating at 50. Slogan restored from the original app, three decades on.
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
// Progressive enhancement: the page ships <audio controls>. If this script runs, it takes the
|
||||
// controls off and drives the same element itself — playback, buffering, seeking and Range
|
||||
// requests are untouched, because the element never changes.
|
||||
(function () {
|
||||
'use strict'
|
||||
|
||||
const fmt = (s) => {
|
||||
if (!isFinite(s)) return '–:––'
|
||||
const m = Math.floor(s / 60)
|
||||
return m + ':' + String(Math.floor(s % 60)).padStart(2, '0')
|
||||
}
|
||||
|
||||
const SEGMENTS = 18
|
||||
const quiet = window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
function enhance(wrap) {
|
||||
const audio = wrap.querySelector('audio')
|
||||
if (!audio) return
|
||||
audio.removeAttribute('controls')
|
||||
|
||||
const total = Number(wrap.dataset.duration) || 0
|
||||
wrap.insertAdjacentHTML('beforeend', `
|
||||
<div class="transport">
|
||||
<button type="button" class="tp-play" aria-label="Toista">
|
||||
<span class="tp-icon" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="tp-mid">
|
||||
<input type="range" class="tp-seek" min="0" max="${total || 100}" step="0.1" value="0"
|
||||
aria-label="Kelaus">
|
||||
<div class="meter" aria-hidden="true">
|
||||
<div class="meter-row"><span class="lbl">L</span><span class="segs"></span></div>
|
||||
<div class="meter-row"><span class="lbl">R</span><span class="segs"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="tp-time"><b>0:00</b> / ${fmt(total)}</span>
|
||||
</div>`)
|
||||
|
||||
const play = wrap.querySelector('.tp-play')
|
||||
const seek = wrap.querySelector('.tp-seek')
|
||||
const time = wrap.querySelector('.tp-time b')
|
||||
const rows = wrap.querySelectorAll('.meter .segs')
|
||||
for (const row of rows) {
|
||||
row.innerHTML = '<span class="seg"></span>'.repeat(SEGMENTS)
|
||||
}
|
||||
const segs = [...rows].map((r) => [...r.children])
|
||||
|
||||
// --- transport ---
|
||||
|
||||
play.addEventListener('click', () => (audio.paused ? audio.play() : audio.pause()))
|
||||
|
||||
const setPlaying = (playing) => {
|
||||
wrap.classList.toggle('playing', playing)
|
||||
play.setAttribute('aria-label', playing ? 'Tauko' : 'Toista')
|
||||
}
|
||||
audio.addEventListener('play', () => { setPlaying(true); startMeter() })
|
||||
audio.addEventListener('pause', () => setPlaying(false))
|
||||
audio.addEventListener('ended', () => setPlaying(false))
|
||||
|
||||
audio.addEventListener('loadedmetadata', () => {
|
||||
if (isFinite(audio.duration)) {
|
||||
seek.max = audio.duration
|
||||
wrap.querySelector('.tp-time').lastChild.textContent = ' / ' + fmt(audio.duration)
|
||||
}
|
||||
})
|
||||
|
||||
let scrubbing = false
|
||||
seek.addEventListener('input', () => {
|
||||
scrubbing = true
|
||||
time.textContent = fmt(Number(seek.value))
|
||||
})
|
||||
seek.addEventListener('change', () => {
|
||||
audio.currentTime = Number(seek.value)
|
||||
scrubbing = false
|
||||
})
|
||||
|
||||
audio.addEventListener('timeupdate', () => {
|
||||
if (scrubbing) return
|
||||
seek.value = audio.currentTime
|
||||
time.textContent = fmt(audio.currentTime)
|
||||
seek.style.setProperty('--pct', (audio.currentTime / (Number(seek.max) || 1)) * 100 + '%')
|
||||
})
|
||||
|
||||
// --- meter ---
|
||||
//
|
||||
// A real analyser, not a decorative loop: it is dark until the audio actually plays, and it
|
||||
// stops the moment playback does. The AudioContext can only start from a gesture, so it is
|
||||
// created on first play. MediaElementSource reroutes the audio, so the graph must reach the
|
||||
// destination or the sound stops.
|
||||
|
||||
let ctx, analysers, raf
|
||||
function startMeter() {
|
||||
if (quiet || raf) return
|
||||
if (!ctx) {
|
||||
try {
|
||||
ctx = new (window.AudioContext || window.webkitAudioContext)()
|
||||
const src = ctx.createMediaElementSource(audio)
|
||||
const split = ctx.createChannelSplitter(2)
|
||||
analysers = [ctx.createAnalyser(), ctx.createAnalyser()]
|
||||
analysers.forEach((a, i) => {
|
||||
a.fftSize = 256
|
||||
split.connect(a, i)
|
||||
})
|
||||
src.connect(split)
|
||||
src.connect(ctx.destination)
|
||||
} catch (e) {
|
||||
return // no Web Audio: the transport still works, the meter simply never lights
|
||||
}
|
||||
}
|
||||
ctx.resume()
|
||||
const buf = new Uint8Array(analysers[0].fftSize)
|
||||
const held = [0, 0]
|
||||
|
||||
const draw = () => {
|
||||
if (audio.paused) {
|
||||
segs.forEach((row) => row.forEach((s) => (s.className = 'seg')))
|
||||
raf = null
|
||||
return
|
||||
}
|
||||
analysers.forEach((a, ch) => {
|
||||
a.getByteTimeDomainData(buf)
|
||||
let peak = 0
|
||||
for (let i = 0; i < buf.length; i++) {
|
||||
const v = Math.abs(buf[i] - 128) / 128
|
||||
if (v > peak) peak = v
|
||||
}
|
||||
// Fall slower than it rises, the way a real meter behaves.
|
||||
held[ch] = peak > held[ch] ? peak : held[ch] * 0.88
|
||||
const lit = Math.round(held[ch] * SEGMENTS)
|
||||
segs[ch].forEach((s, i) => {
|
||||
s.className = 'seg' +
|
||||
(i < lit ? ' on' + (i >= SEGMENTS - 3 ? ' peak' : i >= SEGMENTS - 7 ? ' hot' : '') : '')
|
||||
})
|
||||
})
|
||||
raf = requestAnimationFrame(draw)
|
||||
}
|
||||
raf = requestAnimationFrame(draw)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.playerwrap').forEach(enhance)
|
||||
})
|
||||
})()
|
||||
+351
-7
@@ -21,6 +21,7 @@
|
||||
--hairline: #2a2a2a;
|
||||
--divider: #333333;
|
||||
--input-border: #444444;
|
||||
--track: #2a2a2a;
|
||||
|
||||
/* Text */
|
||||
--text: #e0e0e0;
|
||||
@@ -366,14 +367,171 @@ button.link { background: none; border: 0; color: var(--primary); padding: 0;
|
||||
font-weight: 400; font-size: 0.9rem; text-decoration: underline; }
|
||||
button.link:hover { background: none; color: var(--primary-hover); }
|
||||
|
||||
.scorerow { display: flex; align-items: center; gap: var(--space-4); }
|
||||
.scorerow input[type="range"] { flex: 1; accent-color: var(--primary); }
|
||||
.scorerow output { font-family: var(--font-display); font-size: 2rem; font-weight: 700;
|
||||
color: var(--gold-1); min-width: 3ch; text-align: right; }
|
||||
/* --- the channel strip --- */
|
||||
|
||||
/* Pulled up so it hugs the control. */
|
||||
.scorescale { display: flex; justify-content: space-between; font-size: 0.9rem;
|
||||
color: var(--muted); opacity: 0.6; margin-top: calc(-1 * var(--space-2)); }
|
||||
/* Score and text side by side: the two things you do at once stop being a screen apart. */
|
||||
.strip {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--space-5);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-5);
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: var(--space-5);
|
||||
}
|
||||
|
||||
.deck { display: flex; flex-direction: column; gap: var(--space-3); min-width: 0; }
|
||||
.deck .grow { flex: 1; }
|
||||
.deck .player { margin: 0; }
|
||||
.deckfoot { display: flex; align-items: center; gap: var(--space-4); flex-wrap: wrap; }
|
||||
|
||||
.fader { display: grid; grid-template-columns: auto auto; grid-template-rows: 1fr auto;
|
||||
gap: var(--space-2); align-items: stretch; }
|
||||
|
||||
.ticks { display: flex; flex-direction: column; justify-content: space-between; text-align: right;
|
||||
font-size: 0.7rem; color: var(--muted); font-family: var(--font-display); }
|
||||
|
||||
/* Vertical is native: writing-mode plus rtl gives bottom-to-top travel with no custom widget. */
|
||||
.fader input[type="range"] {
|
||||
writing-mode: vertical-lr;
|
||||
direction: rtl;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 2rem;
|
||||
height: 100%;
|
||||
min-height: 15rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.fader input[type="range"]::-webkit-slider-runnable-track {
|
||||
width: 8px;
|
||||
background: var(--track);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
|
||||
.fader input[type="range"]::-moz-range-track {
|
||||
width: 8px;
|
||||
background: var(--track);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
|
||||
/* A cap, not a knob: this is a fader. */
|
||||
.fader input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 2rem;
|
||||
height: 14px;
|
||||
margin-left: -12px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(var(--gold-1), var(--gold-2));
|
||||
border: 1px solid var(--bar);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.fader input[type="range"]::-moz-range-thumb {
|
||||
width: 2rem;
|
||||
height: 14px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(var(--gold-1), var(--gold-2));
|
||||
border: 1px solid var(--bar);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
.readout {
|
||||
grid-column: 1 / -1;
|
||||
font-family: var(--font-display);
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--gold-1);
|
||||
text-align: center;
|
||||
background: var(--bar);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 0 var(--space-2);
|
||||
}
|
||||
|
||||
/* --- the reveal: one channel per reviewer --- */
|
||||
|
||||
.channels { display: flex; gap: var(--space-3); flex-wrap: wrap; align-items: flex-end;
|
||||
margin-bottom: var(--space-5); }
|
||||
|
||||
.chan { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); width: 2.6rem; }
|
||||
|
||||
.chan-track {
|
||||
position: relative;
|
||||
width: 8px;
|
||||
height: 8rem;
|
||||
background: var(--track);
|
||||
border: 1px solid var(--input-border);
|
||||
border-radius: var(--radius-pill);
|
||||
animation: chan-rise var(--duration-fast) var(--ease-out) backwards;
|
||||
animation-delay: calc(var(--i, 0) * 40ms);
|
||||
}
|
||||
|
||||
/* The cap sits at the score: the row's silhouette is the spread. */
|
||||
.chan-cap {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: calc(var(--v) * 1%);
|
||||
transform: translate(-50%, 50%);
|
||||
width: 1.8rem;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--primary);
|
||||
border: 1px solid var(--bar);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* The filled part below the cap reads as level. */
|
||||
.chan-cap::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
transform: translateX(-50%);
|
||||
width: 4px;
|
||||
height: 8rem;
|
||||
background: linear-gradient(var(--primary), transparent);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.chan.own .chan-cap { background: var(--gold-1); }
|
||||
.chan.own .chan-cap::after { background: linear-gradient(var(--gold-1), transparent); }
|
||||
.chan.own .chan-score { color: var(--gold-1); }
|
||||
.chan.avg .chan-track { background: transparent; border-left: 1px dashed var(--input-border); width: 1px; }
|
||||
.chan.avg .chan-cap { background: var(--gold-2); width: 2rem; height: 2px; border: 0; }
|
||||
.chan.avg .chan-cap::after { display: none; }
|
||||
|
||||
.chan-score { font-family: var(--font-display); font-size: 0.95rem; color: var(--text); }
|
||||
.chan-who { font-size: 0.7rem; color: var(--muted); }
|
||||
|
||||
@keyframes chan-rise {
|
||||
from { opacity: 0; transform: translateY(8px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* Hidden scores are hatched rather than absent: "locked", not "no data". */
|
||||
.sealed {
|
||||
display: inline-block;
|
||||
width: 2.2rem;
|
||||
height: 1rem;
|
||||
vertical-align: -2px;
|
||||
border-radius: 2px;
|
||||
background: repeating-linear-gradient(-45deg, rgba(224, 224, 224, 0.18) 0 6px, transparent 6px 12px),
|
||||
var(--bar);
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.sealed-note { color: var(--muted); display: flex; align-items: center; gap: var(--space-2); }
|
||||
.reviewtext { white-space: pre-wrap; line-height: 1.6; }
|
||||
|
||||
.nextup { font-family: var(--font-display); text-transform: uppercase; font-size: 1.1rem; }
|
||||
|
||||
/* --- drop zone --- */
|
||||
|
||||
@@ -539,6 +697,16 @@ code { background: var(--surface-raised); padding: 0.1rem var(--space-1);
|
||||
|
||||
main { padding: var(--space-5) var(--space-4); }
|
||||
.toasts { left: var(--space-4); max-width: none; }
|
||||
|
||||
/* A 200px fader on a phone is worse than a horizontal one. */
|
||||
.strip { grid-template-columns: 1fr; gap: var(--space-3); padding: var(--space-4); }
|
||||
.fader { grid-template-columns: 1fr auto; grid-template-rows: auto; align-items: center; }
|
||||
.fader input[type="range"] { writing-mode: horizontal-tb; direction: ltr;
|
||||
width: 100%; height: auto; min-height: 0; }
|
||||
/* row-reverse: the markup is top-to-bottom (100 first), a horizontal fader runs 1 → 100. */
|
||||
.ticks { flex-direction: row-reverse; justify-content: space-between; order: 2;
|
||||
grid-column: 1 / -1; }
|
||||
.readout { grid-column: auto; font-size: 1.5rem; }
|
||||
}
|
||||
|
||||
@keyframes panel-down {
|
||||
@@ -590,3 +758,179 @@ img.avatar { object-fit: cover; }
|
||||
padding: var(--space-5); text-align: center; display: flex; flex-direction: column;
|
||||
gap: var(--space-1); box-shadow: var(--shadow-card); }
|
||||
.statvalue { font-family: var(--font-display); font-size: 2rem; font-weight: 700; color: var(--gold-1); }
|
||||
|
||||
/* --- leaderboard bars --- */
|
||||
|
||||
.bar { grid-column: 2 / -1; position: relative; height: 4px; margin-top: var(--space-1);
|
||||
background: var(--track); border-radius: var(--radius-pill); overflow: hidden; }
|
||||
.bar .fill { position: absolute; top: 0; bottom: 0; left: 0; background: var(--primary);
|
||||
border-radius: var(--radius-pill); }
|
||||
.bar.range .fill { background: linear-gradient(90deg, var(--primary), var(--gold-1)); }
|
||||
|
||||
/* --- nav queue count --- */
|
||||
|
||||
.count { display: inline-block; min-width: 1.4em; padding: 0 0.35em; border-radius: var(--radius-pill);
|
||||
background: var(--primary); color: var(--primary-inverse); font-size: 0.75rem;
|
||||
font-weight: 700; text-align: center; }
|
||||
|
||||
/* --- profile comparison --- */
|
||||
|
||||
.statcard.wide { grid-column: span 2; }
|
||||
.channels.compare { justify-content: center; gap: var(--space-5); margin: 0; }
|
||||
.channels.compare .chan-track { height: 5rem; }
|
||||
.channels.compare .chan-cap::after { height: 5rem; }
|
||||
.channels.compare .chan { width: 4rem; }
|
||||
|
||||
/* --- submission progress --- */
|
||||
|
||||
.segments { display: flex; gap: var(--space-1); list-style: none; margin: 0 0 var(--space-3);
|
||||
padding: 0; font-family: var(--font-display); text-transform: uppercase;
|
||||
font-size: 0.75rem; letter-spacing: 0.04em; }
|
||||
.segments li { flex: 1; padding: var(--space-1) var(--space-2); text-align: center;
|
||||
color: var(--muted); background: var(--bar); border: 1px solid var(--hairline);
|
||||
border-radius: var(--radius); }
|
||||
.segments li.now { color: var(--primary-inverse); background: var(--primary); border-color: var(--primary); }
|
||||
.segments li.done { color: var(--gold-1); border-color: var(--input-border); }
|
||||
.segments li.failed { color: var(--pending); border-color: var(--pending); background: var(--pending-bg); }
|
||||
|
||||
/* The score chip on a card, sealed: the score exists, you just haven't earned it yet.
|
||||
The specificity has to beat .songcard .scorebadge, which sets its own background. */
|
||||
.songcard .scorebadge.sealed {
|
||||
display: block;
|
||||
width: 2.4rem;
|
||||
height: 1.4rem;
|
||||
padding: 0;
|
||||
background: repeating-linear-gradient(-45deg, rgba(224, 224, 224, 0.22) 0 5px, transparent 5px 10px),
|
||||
var(--bar);
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
/* --- transport --- */
|
||||
|
||||
.playerwrap { margin: 0 0 var(--space-3); }
|
||||
.playerwrap .player { width: 100%; }
|
||||
/* Once enhanced the native element is only a source of sound. */
|
||||
.playerwrap:has(.transport) .player { display: none; }
|
||||
|
||||
.transport {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
background: var(--bar);
|
||||
border: 1px solid var(--divider);
|
||||
border-radius: var(--radius);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
}
|
||||
|
||||
.tp-play {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
flex: none;
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--primary);
|
||||
border: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.tp-play:hover { background: var(--primary-hover); }
|
||||
|
||||
/* Drawn shapes, not glyphs: ▶ and ❚❚ render differently on every platform. */
|
||||
.tp-icon {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 9px 0 9px 14px;
|
||||
border-color: transparent transparent transparent var(--primary-inverse);
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.playing .tp-icon {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
margin-left: 0;
|
||||
border: 0;
|
||||
background: linear-gradient(90deg, var(--primary-inverse) 0 5px, transparent 5px 9px,
|
||||
var(--primary-inverse) 9px 14px);
|
||||
}
|
||||
|
||||
.tp-mid { display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
|
||||
|
||||
/* A range input for seeking, so arrow keys and the focus ring come free. */
|
||||
.tp-seek {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tp-seek::-webkit-slider-runnable-track {
|
||||
height: 6px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: linear-gradient(90deg, var(--primary) 0 var(--pct, 0%), var(--track) var(--pct, 0%));
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.tp-seek::-moz-range-track {
|
||||
height: 6px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--track);
|
||||
border: 1px solid var(--input-border);
|
||||
}
|
||||
|
||||
.tp-seek::-moz-range-progress { height: 6px; border-radius: var(--radius-pill); background: var(--primary); }
|
||||
|
||||
.tp-seek::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
margin-top: -4px;
|
||||
border-radius: 50%;
|
||||
background: var(--gold-1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.tp-seek::-moz-range-thumb {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: var(--gold-1);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.tp-time { font-family: var(--font-display); font-size: 0.95rem; color: var(--muted); white-space: nowrap; }
|
||||
.tp-time b { color: var(--text); font-weight: 700; }
|
||||
|
||||
/* --- level meter --- */
|
||||
|
||||
.meter { display: flex; flex-direction: column; gap: 3px; }
|
||||
.meter-row { display: flex; gap: var(--space-2); align-items: center; }
|
||||
.meter-row .lbl { font-family: var(--font-display); font-size: 0.65rem; color: var(--muted); width: 0.8rem; }
|
||||
.meter .segs { display: flex; gap: 2px; }
|
||||
|
||||
.seg { width: 6px; height: 11px; border-radius: 1px; background: var(--track);
|
||||
border: 1px solid rgba(0, 0, 0, 0.4); }
|
||||
.seg.on { background: var(--gold-2); }
|
||||
.seg.on.hot { background: var(--primary); }
|
||||
.seg.on.peak { background: #c0392b; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.transport { gap: var(--space-3); padding: var(--space-3); }
|
||||
.tp-play { width: 2.6rem; height: 2.6rem; }
|
||||
.seg { width: 4px; }
|
||||
}
|
||||
|
||||
.pager { display: flex; gap: var(--space-5); margin-top: var(--space-5); }
|
||||
|
||||
/* The slogan is a mark, so it is set in the display face rather than the body face. */
|
||||
.slogan { font-family: var(--font-display); letter-spacing: 0.02em; }
|
||||
.slogan.hero { color: var(--gold-2); font-size: 1.1rem; text-transform: uppercase;
|
||||
margin: calc(-1 * var(--space-2)) 0 var(--space-5); }
|
||||
|
||||
Reference in New Issue
Block a user