mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
* feat(a11y): app-wide "Interface size" setting (Accessibility) Adds a dedicated Accessibility -> Interface size control so users on large, low-DPI displays can enlarge the app's menus, buttons and text (reported: eye strain on a 32" 1440p panel with no OS scaling). Mechanism: a host-owned scale capability (window.feedBack.scale) applies a RELATIVE root font-size (a % of the user-agent base, never a px literal, so a raised browser/OS base font is respected) and publishes an always-present --fb-scale token. The rem-based v3 chrome scales together; the gameplay highway canvas (device-pixel sized) is deliberately untouched, so playback resolution and FPS are unchanged. Medium (100%) clears the override, so default rendering is byte-identical to before -- zero blast radius. - Settings -> new Accessibility tab: Small/Medium/Large/Extra-Large presets (0.90/1.00/1.15/1.30) + a fine-tune slider (to 150%). - Applied pre-paint from an inline <head> script (mirrors the ss-follower pattern) so there is no flash-of-reflow on load. - window.feedBack.scale read-API (get/set + scale:changed, fires once on load) so canvas/WebGL surfaces that cannot inherit rem can follow the size. Shape mirrors the working-tuning read-API; persists as a durable preference. - Cosmetic px->rem sweep so text scales cleanly at the larger stops (2 v3.css font-sizes + 20 text-[Npx] utilities across 8 v3 files; tailwind.min.css rebuilt byte-stable via the pinned toolchain). - One-time first-run nudge for the large/low-DPI display profile that deep-links to the control (never fires once the setting is touched, or on other displays). v3-only. Verified headless: core apply/persist/reset/reload/UI-sync + nudge gating, with no console errors. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): regenerate tailwind.min.css to satisfy tailwind-fresh CI (PR #664 review) Regenerate static/tailwind.min.css via scripts/build-tailwind.sh (tailwindcss@3.4.19) so a fresh build matches the committed artifact and the tailwind-fresh CI job's `git diff --quiet` passes. Two consecutive regenerations are byte-identical. Also (Fix 2) switch the fine-tune interface-size slider to the documented transient-preview path: oninput now calls scale.set(v, { persist:false }) so dragging previews without writing localStorage/emitting a commit each tick, and a new onchange commits with persistence on release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
1365 lines
53 KiB
CSS
1365 lines
53 KiB
CSS
/*
|
||
* fee[dB]ack v0.3.0 — non-Tailwind bits for the v3 shell.
|
||
* Body radial-gradient background + custom scrollbars. Tokens mirror the
|
||
* `fb` palette in tailwind.config.js.
|
||
*/
|
||
|
||
/* Interface-size (Accessibility): the always-present UI-scale token. JS
|
||
(capabilities/interface-scale.js) overrides it on :root and drives the
|
||
visible scaling through the root font-size; this default keeps
|
||
`var(--fb-scale)` resolvable for any canvas/CSS consumer even when unset. */
|
||
:root { --fb-scale: 1; }
|
||
|
||
/* ── Text-selection policy (v3) ──────────────────────────────────────────────
|
||
Accidental drag/double-click selection of app chrome (sidebar, transport, the
|
||
note highway/HUD, buttons, labels) makes the UI look broken and is never
|
||
useful — so default the interface to non-selectable, then opt *content* back
|
||
in. v3-only: this sheet loads only on /v3 (v2 is unchanged). The panel's
|
||
guardrails are baked in:
|
||
- NEVER a `* { user-select:none }` rule — it breaks input carets / IME
|
||
composition on WebKit (bug 82692); we scope to `html` and re-enable below.
|
||
- This is cosmetic only; it protects nothing (DevTools defeats it) and must
|
||
never be used to "lock" copy-worthy text away (a11y: keep errors, IDs,
|
||
paths, versions, metadata, lyrics selectable — incl. in modals/toasts). */
|
||
html { -webkit-user-select: none; user-select: none; }
|
||
|
||
/* Form fields are ALWAYS selectable/editable — protects the caret + IME
|
||
(including CJK / dead-key composition). The default must never swallow typing.
|
||
`.fb-selectable *` forces descendants so a child element's own non-select
|
||
can't strand copy-worthy text inside a content island. */
|
||
input, textarea, select,
|
||
[contenteditable]:not([contenteditable="false"]),
|
||
[contenteditable]:not([contenteditable="false"]) * {
|
||
-webkit-user-select: text; user-select: text;
|
||
}
|
||
|
||
/* Plugin screens are content surfaces (editor, tabview, lyrics, theory, chord
|
||
text, …). Re-enable their mounted subtree by INHERITANCE (no `*`) so the host
|
||
policy can't silently make a plugin's copyable text un-selectable — including
|
||
community / out-of-tree plugins that never adopt `.fb-selectable`. A plugin
|
||
that wants its own chrome non-selectable still wins via its own element rule
|
||
(which this inherited value doesn't override). */
|
||
.screen[id^="plugin-"] { -webkit-user-select: text; user-select: text; }
|
||
|
||
/* Core read-only content opts back in by CONTAINER (lower-drift than tagging
|
||
each value — a new setting added later inherits "selectable" for free):
|
||
the Settings panel (values, paths, device names, version, diagnostics,
|
||
About) and the now-playing song metadata (both tagged `.fb-selectable`).
|
||
Plugins re-enable their own copyable regions with this same class
|
||
(documented in CLAUDE.md).
|
||
|
||
The focused, transient surfaces below ALWAYS carry copy-worthy text (errors,
|
||
IDs, file paths, device/version strings) per the a11y guardrail, so they're
|
||
blanket-opted-in by selector rather than hand-tagged — they're single focused
|
||
panels, not dense card lists, so re-enabling selection there can't recreate
|
||
the across-cards marquee mess the policy prevents:
|
||
- modals / dialogs: `.feedBack-modal`, `[role="dialog"]` (confirm, edit-meta,
|
||
retune result/error, calibration, filter drawer);
|
||
- toasts: `#fb-notify-stack`, `#v3-fb-toast`;
|
||
- the library scan banner (`#scan-banner` — shows the current file path).
|
||
(Dense card lists — the library grid, dashboard, profile — are intentionally
|
||
left non-selectable; copy their text from the now-playing HUD / Settings.) */
|
||
.fb-selectable, .fb-selectable *,
|
||
.feedBack-modal, .feedBack-modal *,
|
||
[role="dialog"], [role="dialog"] *,
|
||
#fb-notify-stack, #fb-notify-stack *,
|
||
#v3-fb-toast, #v3-fb-toast *,
|
||
#scan-banner, #scan-banner * { -webkit-user-select: text; user-select: text; }
|
||
|
||
/* The v3 tuner card replaces the tuner plugin's floating launcher — hide it. */
|
||
#tuner-toggle-btn { display: none !important; }
|
||
|
||
/* The navy radial wash lives on the SIDEBAR (the content area is the solid
|
||
darker tone). Brighter toward the top. */
|
||
#v3-sidebar {
|
||
background-image: radial-gradient(circle at top, #1e293b 0%, #0f172a 100%);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
/* Custom 6px scrollbars on the scroll regions (WebKit/Blink). */
|
||
#v3-main::-webkit-scrollbar,
|
||
#v3-nav::-webkit-scrollbar,
|
||
.v3-scroll::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
}
|
||
#v3-main::-webkit-scrollbar-track,
|
||
#v3-nav::-webkit-scrollbar-track,
|
||
.v3-scroll::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
#v3-main::-webkit-scrollbar-thumb,
|
||
#v3-nav::-webkit-scrollbar-thumb,
|
||
.v3-scroll::-webkit-scrollbar-thumb {
|
||
background: #334155;
|
||
border-radius: 3px;
|
||
}
|
||
#v3-main::-webkit-scrollbar-thumb:hover,
|
||
#v3-nav::-webkit-scrollbar-thumb:hover,
|
||
.v3-scroll::-webkit-scrollbar-thumb:hover {
|
||
background: #475569;
|
||
}
|
||
|
||
/* Firefox scrollbars. */
|
||
#v3-main,
|
||
#v3-nav,
|
||
.v3-scroll {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: #334155 transparent;
|
||
}
|
||
|
||
/* ══ v3 player chrome (P22) ════════════════════════════════════════════════
|
||
Persistent top HUD + Up-Next pill, hover-reveal left rail with feature
|
||
popovers, and an auto-hiding bottom transport. Scoped under #player; this
|
||
markup only exists in static/v3/index.html, so the legacy player is
|
||
unaffected. Behavior: static/v3/player-chrome.js. */
|
||
|
||
#player { background: #050508; position: relative; }
|
||
#player #highway { position: relative; z-index: 1; }
|
||
|
||
/* — Venue Mood FX — Phase 1: bottom strip disabled; real scene ships in 3D bg — */
|
||
.venue-mood-fx {
|
||
display: none !important;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Edge-only wash above 3D canvas, below HUD/transport (z-index 3) */
|
||
#player .v3-venue-scene-wash {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 3;
|
||
pointer-events: none;
|
||
background:
|
||
linear-gradient(180deg, rgba(34, 211, 238, .16) 0%, transparent 14%),
|
||
linear-gradient(0deg, rgba(30, 64, 110, .2) 0%, transparent 16%),
|
||
linear-gradient(90deg, rgba(34, 211, 238, .1) 0%, transparent 10%),
|
||
linear-gradient(270deg, rgba(34, 211, 238, .1) 0%, transparent 10%);
|
||
}
|
||
|
||
/* Non-blocking Venue mode label — below HUD, above canvas */
|
||
#player .v3-venue-mode-badge {
|
||
position: absolute;
|
||
top: 4.75rem;
|
||
left: 1.25rem;
|
||
z-index: 18;
|
||
pointer-events: none;
|
||
max-width: min(22rem, calc(100% - 2.5rem));
|
||
padding: .35rem .65rem;
|
||
font-size: .7rem;
|
||
line-height: 1.35;
|
||
letter-spacing: .02em;
|
||
color: rgba(165, 243, 252, .95);
|
||
background: rgba(8, 47, 73, .78);
|
||
border: 1px solid rgba(34, 211, 238, .38);
|
||
border-radius: .375rem;
|
||
box-shadow: 0 2px 10px rgba(2, 8, 23, .35);
|
||
backdrop-filter: blur(4px);
|
||
}
|
||
|
||
#player .v3-venue-scene-wash.hidden,
|
||
#player .v3-venue-mode-badge.hidden {
|
||
display: none;
|
||
}
|
||
|
||
/* Legacy ::before wash retained for tests; superseded by .v3-venue-scene-wash */
|
||
#player.is-venue-visualization.venue-scene-pending::before {
|
||
content: none;
|
||
}
|
||
|
||
/* Legacy strip rules retained for when STRIP_OVERLAY_ENABLED returns */
|
||
.venue-mood-fx:not(.hidden) { opacity: 1; }
|
||
.venue-mood-fx.hidden { display: none; }
|
||
|
||
.venue-mood-lights,
|
||
.venue-mood-crowd,
|
||
.venue-mood-haze {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* Stage wash — original gradients only */
|
||
.venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 55% 85% at 10% 100%, rgba(251, 191, 36, .22) 0%, transparent 58%),
|
||
radial-gradient(ellipse 55% 85% at 90% 100%, rgba(56, 189, 248, .2) 0%, transparent 58%),
|
||
radial-gradient(ellipse 90% 120% at 50% 110%, rgba(30, 58, 95, .55) 0%, transparent 68%),
|
||
linear-gradient(180deg, transparent 0%, rgba(8, 12, 22, .35) 55%, rgba(4, 6, 12, .82) 100%);
|
||
opacity: .75;
|
||
}
|
||
|
||
/* Crowd silhouettes — CSS shapes, no external assets */
|
||
.venue-mood-crowd {
|
||
bottom: 0;
|
||
top: auto;
|
||
height: 72%;
|
||
background:
|
||
radial-gradient(ellipse 8% 42% at 6% 100%, rgba(48, 72, 118, .96) 0%, rgba(36, 56, 92, .96) 58%, transparent 59%),
|
||
radial-gradient(ellipse 7% 38% at 14% 100%, rgba(52, 78, 124, .94) 0%, rgba(40, 62, 98, .94) 56%, transparent 57%),
|
||
radial-gradient(ellipse 9% 44% at 24% 100%, rgba(44, 68, 110, .95) 0%, rgba(34, 54, 88, .95) 58%, transparent 59%),
|
||
radial-gradient(ellipse 8% 40% at 34% 100%, rgba(50, 74, 120, .94) 0%, rgba(38, 58, 94, .94) 57%, transparent 58%),
|
||
radial-gradient(ellipse 10% 46% at 46% 100%, rgba(42, 66, 108, .96) 0%, rgba(32, 52, 86, .96) 60%, transparent 61%),
|
||
radial-gradient(ellipse 9% 42% at 58% 100%, rgba(48, 72, 116, .95) 0%, rgba(36, 56, 90, .95) 58%, transparent 59%),
|
||
radial-gradient(ellipse 8% 40% at 68% 100%, rgba(52, 78, 122, .93) 0%, rgba(40, 62, 96, .93) 56%, transparent 57%),
|
||
radial-gradient(ellipse 9% 44% at 78% 100%, rgba(44, 68, 112, .95) 0%, rgba(34, 54, 88, .95) 58%, transparent 59%),
|
||
radial-gradient(ellipse 8% 38% at 88% 100%, rgba(50, 74, 118, .94) 0%, rgba(38, 58, 92, .94) 56%, transparent 57%),
|
||
radial-gradient(ellipse 7% 36% at 96% 100%, rgba(46, 70, 114, .93) 0%, rgba(36, 56, 90, .93) 55%, transparent 56%);
|
||
opacity: .55;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.venue-mood-haze {
|
||
background: linear-gradient(180deg, transparent 0%, rgba(15, 23, 42, .12) 40%, rgba(30, 41, 59, .28) 100%);
|
||
opacity: 0;
|
||
transition: opacity .6s ease;
|
||
}
|
||
|
||
/* Player shell atmosphere when venue enabled */
|
||
#player.venue-mood-subtle::before,
|
||
#player.venue-mood-full::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 0;
|
||
pointer-events: none;
|
||
background: radial-gradient(ellipse 120% 80% at 50% 100%, rgba(30, 64, 110, .12) 0%, transparent 55%);
|
||
transition: background .6s ease, opacity .6s ease;
|
||
}
|
||
#player.venue-mood-full::before {
|
||
background:
|
||
radial-gradient(ellipse 90% 55% at 50% 0%, rgba(34, 211, 238, .06) 0%, transparent 60%),
|
||
radial-gradient(ellipse 120% 80% at 50% 100%, rgba(30, 64, 110, .18) 0%, transparent 55%);
|
||
}
|
||
|
||
/* Subtle vs full intensity */
|
||
#player.venue-mood-subtle .venue-mood-lights { opacity: .65; }
|
||
#player.venue-mood-subtle .venue-mood-crowd { opacity: .48; }
|
||
#player.venue-mood-full .venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 52% 95% at 8% 100%, rgba(251, 191, 36, .42) 0%, transparent 58%),
|
||
radial-gradient(ellipse 52% 95% at 92% 100%, rgba(56, 189, 248, .38) 0%, transparent 58%),
|
||
radial-gradient(ellipse 95% 130% at 50% 115%, rgba(59, 130, 246, .48) 0%, transparent 72%),
|
||
linear-gradient(180deg, transparent 0%, rgba(15, 23, 42, .45) 42%, rgba(8, 12, 24, .94) 100%);
|
||
opacity: .92;
|
||
}
|
||
#player.venue-mood-full .venue-mood-crowd { opacity: .78; }
|
||
|
||
/* Full idle baseline — visible before any notes are judged */
|
||
#player.venue-mood-full .venue-mood-fx .venue-mood-lights { opacity: .88; }
|
||
#player.venue-mood-full .venue-mood-fx .venue-mood-crowd { opacity: .72; }
|
||
#player.venue-mood-full .venue-mood-fx .venue-mood-haze {
|
||
opacity: .28;
|
||
background: linear-gradient(180deg, transparent 0%, rgba(56, 189, 248, .08) 35%, rgba(30, 64, 110, .22) 100%);
|
||
}
|
||
|
||
/* Performance state visuals */
|
||
.venue-mood-fx.venue-mood-state-idle .venue-mood-lights { opacity: .45; }
|
||
.venue-mood-fx.venue-mood-state-idle .venue-mood-crowd { opacity: .32; }
|
||
#player.venue-mood-full .venue-mood-fx.venue-mood-state-idle .venue-mood-lights { opacity: .88; }
|
||
#player.venue-mood-full .venue-mood-fx.venue-mood-state-idle .venue-mood-crowd { opacity: .72; }
|
||
|
||
.venue-mood-fx.venue-mood-state-steady .venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 85% 110% at 50% 108%, rgba(51, 85, 120, .5) 0%, transparent 66%),
|
||
linear-gradient(180deg, transparent 0%, rgba(10, 16, 28, .4) 60%, rgba(4, 6, 12, .85) 100%);
|
||
}
|
||
|
||
.venue-mood-fx.venue-mood-state-strong .venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 70% 95% at 50% 105%, rgba(34, 211, 238, .28) 0%, transparent 62%),
|
||
radial-gradient(ellipse 90% 115% at 50% 110%, rgba(51, 85, 120, .58) 0%, transparent 68%),
|
||
linear-gradient(180deg, transparent 0%, rgba(8, 14, 26, .35) 55%, rgba(4, 6, 12, .88) 100%);
|
||
}
|
||
.venue-mood-fx.venue-mood-state-strong .venue-mood-crowd { opacity: .62; }
|
||
|
||
.venue-mood-fx.venue-mood-state-fire .venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 55% 80% at 50% 95%, rgba(251, 146, 60, .42) 0%, transparent 58%),
|
||
radial-gradient(ellipse 85% 110% at 50% 108%, rgba(180, 70, 20, .35) 0%, transparent 65%),
|
||
linear-gradient(180deg, transparent 0%, rgba(20, 10, 6, .3) 50%, rgba(4, 6, 12, .9) 100%);
|
||
}
|
||
.venue-mood-fx.venue-mood-state-fire .venue-mood-crowd { opacity: .78; }
|
||
|
||
.venue-mood-fx.venue-mood-state-recovery .venue-mood-lights {
|
||
background:
|
||
radial-gradient(ellipse 90% 105% at 50% 110%, rgba(120, 90, 30, .32) 0%, transparent 66%),
|
||
linear-gradient(180deg, transparent 0%, rgba(18, 14, 8, .42) 58%, rgba(4, 6, 12, .86) 100%);
|
||
}
|
||
.venue-mood-fx.venue-mood-state-recovery .venue-mood-crowd { opacity: .38; }
|
||
|
||
.venue-mood-fx.venue-mood-state-smoke .venue-mood-lights { opacity: .35; }
|
||
.venue-mood-fx.venue-mood-state-smoke .venue-mood-crowd { opacity: .22; transform: translateY(6%); }
|
||
.venue-mood-fx.venue-mood-state-smoke .venue-mood-haze {
|
||
opacity: .72;
|
||
background:
|
||
linear-gradient(180deg, transparent 0%, rgba(71, 85, 105, .22) 35%, rgba(51, 65, 85, .45) 100%),
|
||
radial-gradient(ellipse 100% 80% at 50% 100%, rgba(100, 116, 139, .25) 0%, transparent 70%);
|
||
}
|
||
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.venue-mood-fx.venue-mood-state-steady .venue-mood-crowd,
|
||
#player.venue-mood-full .venue-mood-fx.venue-mood-state-steady .venue-mood-crowd {
|
||
animation: v3-venue-crowd-sway 4.8s ease-in-out infinite;
|
||
}
|
||
.venue-mood-fx.venue-mood-state-strong .venue-mood-crowd,
|
||
#player.venue-mood-full .venue-mood-fx.venue-mood-state-strong .venue-mood-crowd {
|
||
animation: v3-venue-crowd-sway 3.6s ease-in-out infinite;
|
||
}
|
||
.venue-mood-fx.venue-mood-state-fire .venue-mood-crowd,
|
||
#player.venue-mood-full .venue-mood-fx.venue-mood-state-fire .venue-mood-crowd {
|
||
animation: v3-venue-crowd-bounce 2.2s ease-in-out infinite;
|
||
}
|
||
.venue-mood-fx.venue-mood-state-fire .venue-mood-lights {
|
||
animation: v3-venue-fire-glow 2.8s ease-in-out infinite;
|
||
}
|
||
.venue-mood-fx.venue-mood-state-smoke .venue-mood-haze {
|
||
animation: v3-venue-smoke-drift 5.5s ease-in-out infinite;
|
||
}
|
||
}
|
||
|
||
@keyframes v3-venue-crowd-sway {
|
||
0%, 100% { transform: translateY(0); }
|
||
50% { transform: translateY(-2px); }
|
||
}
|
||
@keyframes v3-venue-crowd-bounce {
|
||
0%, 100% { transform: translateY(0) scaleY(1); }
|
||
35% { transform: translateY(-4px) scaleY(1.02); }
|
||
65% { transform: translateY(-1px) scaleY(1); }
|
||
}
|
||
@keyframes v3-venue-fire-glow {
|
||
0%, 100% { filter: brightness(1); }
|
||
50% { filter: brightness(1.12); }
|
||
}
|
||
@keyframes v3-venue-smoke-drift {
|
||
0%, 100% { opacity: .65; transform: translateY(0); }
|
||
50% { opacity: .78; transform: translateY(-3px); }
|
||
}
|
||
|
||
@media (prefers-reduced-motion: reduce) {
|
||
.venue-mood-fx .venue-mood-crowd,
|
||
.venue-mood-fx .venue-mood-lights,
|
||
.venue-mood-fx .venue-mood-haze {
|
||
animation: none !important;
|
||
}
|
||
}
|
||
|
||
/* — Up Next pill (top-right, persistent) — */
|
||
#player-hud .v3-upnext {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: .35rem;
|
||
padding: .45rem .9rem;
|
||
border-radius: .75rem;
|
||
background: rgba(15, 23, 42, .7);
|
||
border: 1px solid rgba(51, 65, 85, .5);
|
||
backdrop-filter: blur(6px);
|
||
font-size: .85rem;
|
||
pointer-events: auto;
|
||
}
|
||
#player-hud .v3-upnext.hidden { display: none; }
|
||
/* Text row keeps the original inline layout untouched. */
|
||
#player-hud .v3-upnext .v3-upnext-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
}
|
||
/* Progress bar under the text — fills as the current section elapses. */
|
||
#player-hud .v3-upnext .v3-upnext-bar {
|
||
height: 4px;
|
||
border-radius: 999px;
|
||
background: rgba(148, 163, 184, .25);
|
||
overflow: hidden;
|
||
}
|
||
#player-hud .v3-upnext .v3-upnext-bar-fill {
|
||
height: 100%;
|
||
/* Fill is driven via transform: scaleX(0..1) from player-chrome.js —
|
||
compositor-only, unlike the previous width writes which re-ran
|
||
layout on every update tick. */
|
||
width: 100%;
|
||
transform: scaleX(0);
|
||
transform-origin: left;
|
||
border-radius: inherit;
|
||
background: linear-gradient(90deg, #22d3ee, #a855f7, #f472b6);
|
||
transition: transform .12s linear;
|
||
}
|
||
|
||
/* — Live performance HUD (top-right, read-only) — */
|
||
.v3-live-performance-hud {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: flex-end;
|
||
gap: .2rem;
|
||
min-width: 9.5rem;
|
||
padding: .55rem .85rem;
|
||
border-radius: .85rem;
|
||
background: rgba(15, 23, 42, .78);
|
||
border: 1px solid rgba(71, 85, 105, .55);
|
||
backdrop-filter: blur(8px);
|
||
box-shadow: 0 8px 24px rgba(0, 0, 0, .35);
|
||
pointer-events: none;
|
||
transition: border-color .35s ease, box-shadow .35s ease, background .35s ease;
|
||
}
|
||
.v3-live-performance-hud.hidden { display: none; }
|
||
|
||
/* Highway scoreboard preference (static/v3/scoreboard-pref.js).
|
||
One note-detection scoreboard at a time on the highway:
|
||
core (default) → core live-performance HUD; hide the note_detect plugin HUD
|
||
detailed → note_detect .nd-hud; hide the core HUD
|
||
off → hide both
|
||
The default rule keys off "not detailed and not off" so it's correct even
|
||
before the pref script sets data-scoreboard (avoids a flash of both). */
|
||
html:not([data-scoreboard="detailed"]):not([data-scoreboard="off"]) .nd-hud { display: none !important; }
|
||
html[data-scoreboard="detailed"] #v3-live-performance-hud { display: none !important; }
|
||
html[data-scoreboard="off"] .nd-hud,
|
||
html[data-scoreboard="off"] #v3-live-performance-hud { display: none !important; }
|
||
|
||
.v3-live-performance-heading {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: .45rem;
|
||
line-height: 1;
|
||
}
|
||
.v3-live-performance-tag {
|
||
font-size: .68rem;
|
||
font-weight: 800;
|
||
letter-spacing: .14em;
|
||
color: #94a3b8;
|
||
}
|
||
.v3-live-performance-percent {
|
||
font-size: 2rem;
|
||
font-weight: 900;
|
||
letter-spacing: -.03em;
|
||
color: #f8fafc;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.v3-live-performance-hits,
|
||
.v3-live-performance-streak {
|
||
font-size: .78rem;
|
||
font-weight: 600;
|
||
color: #cbd5e1;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
.v3-live-performance-state {
|
||
font-size: .72rem;
|
||
font-weight: 700;
|
||
color: #e2e8f0;
|
||
min-height: 1rem;
|
||
}
|
||
.v3-live-performance-hud.is-idle .v3-live-performance-percent { color: #94a3b8; }
|
||
.v3-live-performance-hud.is-fire {
|
||
border-color: rgba(251, 146, 60, .75);
|
||
background: linear-gradient(145deg, rgba(67, 20, 7, .82), rgba(15, 23, 42, .88));
|
||
box-shadow: 0 0 22px rgba(251, 146, 60, .28);
|
||
animation: v3-live-fire-pulse 2.4s ease-in-out infinite;
|
||
}
|
||
.v3-live-performance-hud.is-fire .v3-live-performance-percent {
|
||
color: #fdba74;
|
||
text-shadow: 0 0 12px rgba(251, 146, 60, .45);
|
||
}
|
||
.v3-live-performance-hud.is-fire .v3-live-performance-state { color: #fb923c; }
|
||
.v3-live-performance-hud.is-strong {
|
||
border-color: rgba(34, 211, 238, .55);
|
||
box-shadow: 0 0 16px rgba(34, 211, 238, .18);
|
||
}
|
||
.v3-live-performance-hud.is-strong .v3-live-performance-percent { color: #67e8f9; }
|
||
.v3-live-performance-hud.is-steady {
|
||
border-color: rgba(100, 116, 139, .55);
|
||
}
|
||
.v3-live-performance-hud.is-recovery {
|
||
border-color: rgba(250, 204, 21, .45);
|
||
background: rgba(30, 27, 12, .72);
|
||
}
|
||
.v3-live-performance-hud.is-recovery .v3-live-performance-percent { color: #fde68a; }
|
||
.v3-live-performance-hud.is-smoke {
|
||
border-color: rgba(100, 116, 139, .35);
|
||
background: rgba(15, 23, 42, .62);
|
||
opacity: .92;
|
||
animation: v3-live-smoke-haze 3.2s ease-in-out infinite;
|
||
}
|
||
.v3-live-performance-hud.is-smoke .v3-live-performance-percent { color: #94a3b8; }
|
||
.v3-live-performance-hud.is-smoke .v3-live-performance-state { color: #cbd5e1; }
|
||
|
||
@keyframes v3-live-fire-pulse {
|
||
0%, 100% { box-shadow: 0 0 18px rgba(251, 146, 60, .22); }
|
||
50% { box-shadow: 0 0 26px rgba(251, 146, 60, .38); }
|
||
}
|
||
@keyframes v3-live-smoke-haze {
|
||
0%, 100% { transform: translateY(0); opacity: .92; }
|
||
50% { transform: translateY(-1px); opacity: .84; }
|
||
}
|
||
|
||
.v3-upnext-name {
|
||
font-weight: 800;
|
||
background: linear-gradient(90deg, #22d3ee, #a855f7, #f472b6);
|
||
-webkit-background-clip: text;
|
||
background-clip: text;
|
||
color: transparent;
|
||
}
|
||
|
||
/* — Left rail hover-zone — */
|
||
.v3-railzone {
|
||
position: absolute;
|
||
left: 0; top: 0; bottom: 0;
|
||
z-index: 30;
|
||
display: flex;
|
||
align-items: center;
|
||
pointer-events: none; /* only the catcher + rail take pointer events */
|
||
}
|
||
.v3-railzone::before { /* invisible left-edge strip that reveals the rail */
|
||
content: "";
|
||
position: absolute;
|
||
left: 0; top: 0; bottom: 0;
|
||
width: 96px;
|
||
pointer-events: auto;
|
||
}
|
||
/* The Section Map plugin pins a ~20px clickable bar to the very top of #player
|
||
(#section-map, z-index:5). The rail catcher above is full-height at z-index:30,
|
||
so its top-left corner swallows clicks on the section map's first section. When
|
||
the bar is present, drop the catcher below it so the top strip stays clickable;
|
||
the rail still reveals from anywhere below the bar. Mirrors the core
|
||
`#section-map ~ #player-hud` special-case in static/style.css. */
|
||
#section-map ~ #v3-railzone::before { top: 20px; }
|
||
|
||
.v3-rail {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: .25rem;
|
||
padding: 1.5rem .75rem;
|
||
pointer-events: auto;
|
||
opacity: 0;
|
||
transform: translateX(-12px);
|
||
transition: opacity .25s ease, transform .25s ease;
|
||
}
|
||
.v3-railzone:hover .v3-rail,
|
||
.v3-railzone:focus-within .v3-rail,
|
||
#player.pop-open .v3-rail { opacity: 1; transform: none; }
|
||
|
||
.v3-rail-icon {
|
||
position: relative;
|
||
width: 70px; height: 70px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
border: 0;
|
||
cursor: pointer;
|
||
color: #8ba3ac;
|
||
transition: transform .18s ease, color .18s ease;
|
||
}
|
||
.v3-rail-border {
|
||
position: absolute;
|
||
inset: 0;
|
||
border: 2px solid rgba(139, 163, 172, .45);
|
||
border-radius: 50%;
|
||
transition: border-color .18s ease;
|
||
}
|
||
.v3-rail-border-gear {
|
||
border-color: rgba(139, 163, 172, .45);
|
||
}
|
||
.v3-rail-svg { width: 32px; height: 32px; fill: currentcolor; z-index: 1; }
|
||
.v3-rail-dot { width: 6px; height: 6px; background: #4a5d6e; border-radius: 2px; margin: 1rem 0; }
|
||
|
||
/* pop-out + active state */
|
||
.v3-rail-icon:hover,
|
||
.v3-rail-icon:focus-visible { color: #e8eef1; }
|
||
.v3-rail-icon:hover .v3-rail-border,
|
||
.v3-rail-icon:focus-visible .v3-rail-border { border-color: rgba(203, 213, 225, .85); }
|
||
.v3-rail-icon.is-active { color: #e2e8f0; }
|
||
.v3-rail-icon.is-active .v3-rail-border { border-color: #22d3ee; }
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
.v3-rail-icon:hover,
|
||
.v3-rail-icon:focus-visible { transform: translateX(6px) scale(1.12); }
|
||
}
|
||
|
||
/* — Section Practice circular rail icon (v3 chrome) — */
|
||
#v3-player-rail .section-practice-control--v3 {
|
||
padding: 0;
|
||
margin: 0;
|
||
width: auto;
|
||
display: flex;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill {
|
||
position: relative;
|
||
width: 70px;
|
||
height: 70px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0;
|
||
gap: 0;
|
||
min-width: 0;
|
||
color: #8ba3ac;
|
||
background: transparent;
|
||
border: 0;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
cursor: pointer;
|
||
transition: transform .18s ease, color .18s ease;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill-text,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill-caret {
|
||
display: none !important;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
z-index: 1;
|
||
font-size: 2rem;
|
||
line-height: 1;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill-icon .v3-rail-svg,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill-svg {
|
||
width: 32px;
|
||
height: 32px;
|
||
fill: currentcolor;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:hover,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:focus-visible {
|
||
color: #e8eef1;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:hover .v3-rail-border,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:focus-visible .v3-rail-border {
|
||
border-color: rgba(203, 213, 225, .85);
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill[aria-expanded="true"],
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill.section-practice-pill--active {
|
||
color: #e2e8f0;
|
||
background: transparent;
|
||
box-shadow: none;
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill[aria-expanded="true"] .v3-rail-border,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill.section-practice-pill--active .v3-rail-border {
|
||
border-color: #22d3ee;
|
||
}
|
||
@media (prefers-reduced-motion: no-preference) {
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:hover,
|
||
#v3-player-rail .section-practice-control--v3 .section-practice-pill:focus-visible {
|
||
transform: translateX(6px) scale(1.12);
|
||
}
|
||
}
|
||
#v3-player-rail .section-practice-control--v3 #section-practice-bar {
|
||
left: calc(100% + 10px);
|
||
right: auto;
|
||
bottom: auto;
|
||
top: 0;
|
||
min-width: 230px;
|
||
max-width: min(340px, calc(100vw - 120px));
|
||
padding: .9rem;
|
||
border-radius: .9rem;
|
||
border: 1px solid rgba(51, 65, 85, .6);
|
||
background: rgba(15, 23, 42, .96);
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
|
||
}
|
||
|
||
/* — Rail feature popovers — */
|
||
.v3-rail-pop {
|
||
position: absolute;
|
||
left: 92px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
min-width: 230px;
|
||
max-width: 340px;
|
||
pointer-events: auto;
|
||
background: rgba(15, 23, 42, .96);
|
||
border: 1px solid rgba(51, 65, 85, .6);
|
||
border-radius: .9rem;
|
||
box-shadow: 0 12px 40px rgba(0, 0, 0, .5);
|
||
padding: .9rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: .6rem;
|
||
z-index: 40;
|
||
}
|
||
.v3-rail-pop.hidden { display: none; }
|
||
.v3-pop-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: .75rem;
|
||
font-size: .8rem;
|
||
color: #cbd5e1;
|
||
}
|
||
.v3-pop-label {
|
||
color: #94a3b8;
|
||
font-size: .72rem;
|
||
text-transform: uppercase;
|
||
letter-spacing: .04em;
|
||
white-space: nowrap;
|
||
}
|
||
.v3-pop-select,
|
||
.v3-pop-btn {
|
||
background: #1e293b;
|
||
border: 1px solid #334155;
|
||
color: #e2e8f0;
|
||
border-radius: .5rem;
|
||
padding: .35rem .6rem;
|
||
font-size: .8rem;
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
.v3-pop-btn:hover { background: #334155; }
|
||
.v3-pop-pin {
|
||
background: #1e293b;
|
||
border: 1px solid #334155;
|
||
color: #94a3b8;
|
||
border-radius: .5rem;
|
||
width: 2rem; height: 2rem;
|
||
cursor: pointer;
|
||
}
|
||
.v3-pop-val { color: #94a3b8; font-size: .75rem; min-width: 3rem; text-align: right; }
|
||
.v3-pop-close {
|
||
margin-top: .25rem;
|
||
background: #1e293b;
|
||
border: 1px solid #334155;
|
||
color: #94a3b8;
|
||
border-radius: .5rem;
|
||
padding: .4rem;
|
||
font-size: .8rem;
|
||
cursor: pointer;
|
||
}
|
||
.v3-pop-close:hover { background: rgba(127, 29, 29, .5); color: #fca5a5; }
|
||
|
||
/* — Bottom transport (auto-hide) — */
|
||
#player .v3-transport {
|
||
position: absolute;
|
||
left: 0; right: 0; bottom: 0;
|
||
z-index: 20;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1.25rem;
|
||
padding: 1rem;
|
||
background: transparent;
|
||
opacity: 0;
|
||
transform: translateY(12px);
|
||
transition: opacity .25s ease, transform .25s ease;
|
||
}
|
||
#player.chrome-active .v3-transport,
|
||
#player.pop-open .v3-transport,
|
||
#player .v3-transport:hover { opacity: 1; transform: none; }
|
||
#player.chrome-idle { cursor: none; }
|
||
|
||
.v3-transport-mid { display: flex; align-items: center; gap: .75rem; }
|
||
.v3-seek {
|
||
width: 2.5rem; height: 2.5rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: rgba(30, 41, 59, .7);
|
||
border: 1px solid rgba(51, 65, 85, .6);
|
||
border-radius: 999px;
|
||
color: #cbd5e1;
|
||
cursor: pointer;
|
||
transition: background .15s;
|
||
}
|
||
.v3-seek:hover { background: rgba(51, 65, 85, .9); }
|
||
.v3-play {
|
||
width: 4rem; height: 2.6rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(180deg, #475569, #334155);
|
||
border: 1px solid rgba(100, 116, 139, .6);
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, .4);
|
||
}
|
||
.v3-play:hover { filter: brightness(1.15); }
|
||
|
||
/* speed widget (bars + slider + label + presets) */
|
||
.v3-speed-cluster {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: .5rem;
|
||
flex-shrink: 1;
|
||
min-width: 0;
|
||
max-width: min(52vw, 28rem);
|
||
}
|
||
.v3-speed { display: flex; align-items: center; gap: .5rem; flex-shrink: 0; }
|
||
.v3-speed-presets {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
align-items: center;
|
||
gap: .2rem;
|
||
min-width: 0;
|
||
overflow-x: auto;
|
||
scrollbar-width: none;
|
||
}
|
||
.v3-speed-presets::-webkit-scrollbar { display: none; }
|
||
.v3-speed-preset-btn {
|
||
flex: 0 0 auto;
|
||
padding: .15rem .45rem;
|
||
min-width: 1.75rem;
|
||
font-size: .7rem;
|
||
font-weight: 700;
|
||
line-height: 1.25;
|
||
color: #cbd5e1;
|
||
background: rgba(30, 41, 59, .9);
|
||
border: 1px solid rgba(71, 85, 105, .85);
|
||
border-radius: 999px;
|
||
cursor: pointer;
|
||
transition: background .15s, border-color .15s, color .15s, box-shadow .15s;
|
||
}
|
||
.v3-speed-preset-btn:hover {
|
||
color: #e2e8f0;
|
||
background: rgba(51, 65, 85, .9);
|
||
border-color: rgba(100, 116, 139, .8);
|
||
}
|
||
.v3-speed-preset-btn[data-sweet-spot="1"]:not(.v3-speed-preset-active) {
|
||
border-color: rgba(34, 211, 238, .35);
|
||
}
|
||
.v3-speed-preset-active {
|
||
color: #0f172a;
|
||
background: #22d3ee;
|
||
border-color: #22d3ee;
|
||
}
|
||
.v3-speed-preset-btn[data-sweet-spot="1"].v3-speed-preset-active {
|
||
box-shadow: 0 0 8px rgba(34, 211, 238, .45);
|
||
}
|
||
.v3-speed-bars { display: inline-flex; align-items: flex-end; gap: 2px; height: 22px; }
|
||
.v3-speed-bars i {
|
||
width: 3px;
|
||
background: #475569;
|
||
border-radius: 1px;
|
||
transition: background .15s, height .15s;
|
||
}
|
||
.v3-speed-bars i:nth-child(1) { height: 5px; }
|
||
.v3-speed-bars i:nth-child(2) { height: 7px; }
|
||
.v3-speed-bars i:nth-child(3) { height: 9px; }
|
||
.v3-speed-bars i:nth-child(4) { height: 11px; }
|
||
.v3-speed-bars i:nth-child(5) { height: 13px; }
|
||
.v3-speed-bars i:nth-child(6) { height: 16px; }
|
||
.v3-speed-bars i:nth-child(7) { height: 19px; }
|
||
.v3-speed-bars i:nth-child(8) { height: 22px; }
|
||
.v3-speed-bars i.on { background: #22d3ee; }
|
||
.v3-speed-slider { width: 90px; }
|
||
.v3-speed-label { color: #94a3b8; font-size: .8rem; min-width: 2.4rem; }
|
||
@media (max-width: 720px) {
|
||
.v3-speed-cluster {
|
||
flex-wrap: wrap;
|
||
max-width: min(62vw, 18rem);
|
||
}
|
||
.v3-speed-presets { flex-wrap: wrap; overflow-x: visible; }
|
||
}
|
||
|
||
/* rainbow chevrons (speed-level indicator) */
|
||
.v3-chevrons { display: inline-flex; align-items: center; gap: 1px; flex-shrink: 0; }
|
||
.v3-chevrons i {
|
||
width: 10px; height: 14px;
|
||
background: #475569;
|
||
opacity: .35;
|
||
transition: opacity .15s;
|
||
clip-path: polygon(0 0, 55% 50%, 0 100%, 45% 100%, 100% 50%, 45% 0);
|
||
}
|
||
.v3-chevrons i.on { opacity: 1; }
|
||
.v3-chevrons i:nth-child(1).on { background: #f87171; }
|
||
.v3-chevrons i:nth-child(2).on { background: #fb923c; }
|
||
.v3-chevrons i:nth-child(3).on { background: #facc15; }
|
||
.v3-chevrons i:nth-child(4).on { background: #4ade80; }
|
||
.v3-chevrons i:nth-child(5).on { background: #22d3ee; }
|
||
|
||
/* — Plugin-control slot (host re-homes plugin controls here from the transport) — */
|
||
.v3-plugin-slot {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
gap: .4rem;
|
||
}
|
||
.v3-plugin-slot > * { margin: 0 !important; } /* neutralize legacy ml-1/ml-auto spacing */
|
||
.v3-pop-empty { color: #64748b; font-size: .75rem; font-style: italic; }
|
||
/* small count badge on the Plugins rail icon */
|
||
.v3-rail-badge {
|
||
position: absolute;
|
||
top: 6px; right: 6px;
|
||
min-width: 16px; height: 16px;
|
||
padding: 0 4px;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: #22d3ee; color: #0f172a;
|
||
font-size: 0.625rem; font-weight: 800; line-height: 1;
|
||
border-radius: 999px; z-index: 2;
|
||
}
|
||
.v3-rail-badge[hidden] { display: none; }
|
||
|
||
/* ── v3 UI font: Rubik ──────────────────────────────────────────────────────
|
||
`font-display` is a SHARED Tailwind token (Inter, used by the v2 body too), so
|
||
it can't be repointed in tailwind.config without changing v2. v3.css is loaded
|
||
only by the v3 page, so scope Rubik here. `body.font-display` (element+class)
|
||
outranks Tailwind's `.font-display` (class) by specificity. The Rubik webfont
|
||
is loaded via a <link> in static/v3/index.html. */
|
||
body.font-display { font-family: Rubik, system-ui, sans-serif; }
|
||
|
||
/* ── v3 Plugins page — Pedalboard ────────────────────────────────────────────
|
||
Plugins grouped onto category "pedalboards"; each plugin is a draggable
|
||
stompbox pedal with decorative patch cables (static/v3/pedal-cables.js)
|
||
sagging between them. Custom CSS (not Tailwind utilities) so the prebuilt
|
||
stylesheet needn't be regenerated. Palette mirrors the fb-* tokens. */
|
||
/* The title sits on the board's top-right as a browser-style tab; it also
|
||
doubles as the collapse toggle. The section reserves a row of height for it. */
|
||
.v3-board-section { position: relative; padding-top: 30px; margin-bottom: 2rem; }
|
||
.v3-board-title {
|
||
position: absolute; top: 0; right: 22px; z-index: 3;
|
||
display: flex; align-items: center; gap: .4rem;
|
||
height: 31px; box-sizing: border-box; padding: 0 .75rem;
|
||
cursor: pointer; text-align: left; font-family: inherit;
|
||
font-size: .72rem; font-weight: 700; letter-spacing: .08em; text-transform: uppercase;
|
||
color: #cbd5e1;
|
||
/* tab body: matches the board's top gradient stop, rounded only on top, and
|
||
its bottom overlaps the board's top edge so they read as one piece. */
|
||
background: #1c2740;
|
||
border: 1px solid rgba(51, 65, 85, .55);
|
||
border-bottom: none;
|
||
border-radius: .55rem .55rem 0 0;
|
||
}
|
||
.v3-board-title:hover { color: #fff; background: #243150; }
|
||
.v3-board-title:focus-visible { outline: 2px solid #0ea5e9; outline-offset: 1px; }
|
||
/* When collapsed there's no board below, so close the tab into a full pill. */
|
||
.v3-board-section.collapsed .v3-board-title {
|
||
border-bottom: 1px solid rgba(51, 65, 85, .55);
|
||
border-radius: .55rem;
|
||
}
|
||
.v3-board-chevron {
|
||
width: 14px; height: 14px; flex: none; color: #64748b;
|
||
transition: transform .15s ease;
|
||
}
|
||
.v3-board-title:hover .v3-board-chevron { color: #94a3b8; }
|
||
.v3-board-section.collapsed .v3-board-chevron { transform: rotate(-90deg); }
|
||
.v3-board-section.collapsed .v3-pedalboard { display: none; }
|
||
.v3-board-count {
|
||
font-size: .65rem; font-weight: 700; color: #cbd5e1;
|
||
background: rgba(51, 65, 85, .5); border-radius: 999px; padding: .05rem .45rem;
|
||
}
|
||
|
||
/* The board surface: a dark, subtly textured panel the pedals sit on. */
|
||
.v3-pedalboard {
|
||
position: relative;
|
||
border-radius: 1rem;
|
||
border: 1px solid rgba(51, 65, 85, .5); /* fb-border */
|
||
background:
|
||
repeating-linear-gradient(45deg, rgba(255, 255, 255, .012) 0 2px, transparent 2px 9px),
|
||
radial-gradient(120% 140% at 50% 0%, #1c2740 0%, #131b2e 60%, #0d1422 100%);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .03), inset 0 -18px 40px rgba(0, 0, 0, .35);
|
||
min-height: 220px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Decorative patch-cable overlay — never blocks pedal clicks. */
|
||
.v3-cable-layer {
|
||
position: absolute; inset: 0;
|
||
width: 100%; height: 100%;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
.v3-cable {
|
||
fill: none;
|
||
stroke: #e2683a; /* warm 1/4" cable */
|
||
stroke-width: 4;
|
||
stroke-linecap: round;
|
||
opacity: .85;
|
||
filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .45));
|
||
}
|
||
|
||
/* Stompbox pedal — a transparent pedal photo "skin" (static/v3/pedals/*) set as
|
||
the card background; the plugin's name + description overlay in a label panel.
|
||
The skin is random-per-plugin and persisted (see plugins-page.js). The card
|
||
dimensions must match PEDAL_W/PEDAL_H in plugins-page.js. */
|
||
.v3-pedal {
|
||
position: absolute;
|
||
/* width/height are set responsively per board in plugins-page.js so a fixed
|
||
number of pedals fit per row. */
|
||
box-sizing: border-box;
|
||
color: #f8fafc;
|
||
cursor: grab;
|
||
user-select: none;
|
||
touch-action: none; /* let pointer-drag own the gesture */
|
||
z-index: 2;
|
||
transition: filter .12s ease;
|
||
}
|
||
/* The skin photo lives on ::before so the disabled-dim filter only touches it,
|
||
not the glow ring / label / badges (which must stay full-colour). */
|
||
.v3-pedal::before {
|
||
content: ''; position: absolute; inset: 0; z-index: 0;
|
||
background-image: var(--skin);
|
||
background-size: 100% 100%; background-position: center; background-repeat: no-repeat;
|
||
}
|
||
.v3-pedal:hover { filter: brightness(1.06); }
|
||
.v3-pedal:focus-visible { outline: 2px solid #0ea5e9; outline-offset: 3px; border-radius: .8rem; }
|
||
.v3-pedal-dragging { cursor: grabbing; z-index: 5; }
|
||
|
||
/* Footswitch status glow: green when the plugin is active, red when disabled.
|
||
Full-pedal overlay (same crop+pad as the skins) so the ring sits on the
|
||
footswitch and scales with the pedal. */
|
||
.v3-pedal-glow {
|
||
position: absolute; inset: 0; z-index: 1; pointer-events: none;
|
||
background-image: url('/static/v3/overlay-active.png');
|
||
background-size: 100% 100%; background-position: center; background-repeat: no-repeat;
|
||
}
|
||
.v3-pedal-off .v3-pedal-glow { background-image: url('/static/v3/overlay-inactive.png'); }
|
||
|
||
/* Footswitch click hotspot over the photo's stomp button (toggles on/off). */
|
||
.v3-pedal-foot {
|
||
position: absolute; left: 50%; bottom: 16%; transform: translate(-50%, -2px);
|
||
width: 16%; aspect-ratio: 1 / 1; border-radius: 50%; /* scales with pedal */
|
||
background: transparent; border: 0; padding: 0; cursor: pointer; z-index: 5;
|
||
}
|
||
.v3-pedal-foot:focus-visible { outline: 2px solid #0ea5e9; outline-offset: 2px; border-radius: 50%; }
|
||
|
||
/* Disabled plugin: dim + desaturate only the SKIN (so the red glow stays vivid),
|
||
and show an "off" badge. */
|
||
.v3-pedal-off::before { filter: grayscale(.85) brightness(.5); }
|
||
.v3-pedal-offbadge {
|
||
position: absolute; top: 5px; left: 5px; z-index: 6; display: none;
|
||
font-size: .55rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
|
||
color: #fecaca; background: rgba(60, 10, 10, .8);
|
||
border: 1px solid rgba(239, 68, 68, .6); border-radius: .3rem; padding: .05rem .35rem;
|
||
}
|
||
.v3-pedal-off .v3-pedal-offbadge { display: block; }
|
||
|
||
/* The drawn cable plug picks up a soft drop shadow so it lifts off the cable. */
|
||
.v3-cable-plug { filter: drop-shadow(0 1px 1.5px rgba(0, 0, 0, .55)); }
|
||
|
||
/* Overlay label on the pedal face — over the artwork band between the photo's
|
||
knobs (top) and footswitch (bottom). Translucent so the skin still reads. */
|
||
.v3-pedal-label {
|
||
position: absolute; left: 9%; right: 9%; top: 31%; bottom: calc(28% + 4px); z-index: 2;
|
||
transform: translateY(-10px); /* nudge up */
|
||
display: flex; flex-direction: column; align-items: center; gap: .3rem;
|
||
padding: .55rem .7rem;
|
||
background: rgba(7, 10, 16, .74);
|
||
border: 1px solid rgba(255, 255, 255, .1);
|
||
border-radius: .5rem;
|
||
backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
|
||
box-shadow: 0 4px 14px rgba(0, 0, 0, .5);
|
||
overflow: hidden; text-align: center;
|
||
}
|
||
/* Thumbnail fills the panel (description is now a hover tooltip, not on the face). */
|
||
.v3-pedal-thumb {
|
||
flex: 1 1 auto; min-height: 0;
|
||
width: 100%; height: auto; object-fit: contain; border-radius: .35rem;
|
||
display: block; -webkit-user-drag: none;
|
||
}
|
||
.v3-pedal-name {
|
||
flex: none;
|
||
font-size: .86rem; font-weight: 700; line-height: 1.15;
|
||
max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||
}
|
||
.v3-pedal-err { font-size: .6rem; color: #fca5a5; overflow-wrap: break-word; }
|
||
.v3-pedal-pill {
|
||
display: inline-block;
|
||
font-size: .58rem; font-weight: 700; padding: .08rem .4rem; border-radius: 999px;
|
||
}
|
||
.v3-pill-bad { background: rgba(239, 68, 68, .25); color: #fca5a5; }
|
||
.v3-pill-wait { background: rgba(234, 179, 8, .25); color: #fde68a; }
|
||
.v3-pedal-bundled {
|
||
position: absolute; top: 4px; right: 4px; z-index: 4;
|
||
font-size: .52rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
|
||
color: #ddd6fe; background: rgba(76, 29, 149, .6);
|
||
border: 1px solid rgba(139, 92, 246, .4); border-radius: .3rem; padding: .04rem .28rem;
|
||
}
|
||
|
||
/* ════════════════════════════════════════════════════════════════════════
|
||
Tabbed settings page (v3) — feat/v3-settings-tabbed
|
||
Card-row layout: leading icon + title/description + right-aligned control,
|
||
grouped under a horizontal tab bar. Pure CSS (no Tailwind rebuild); colors
|
||
mirror the fb-* design tokens so the page matches the rest of the v3 shell.
|
||
════════════════════════════════════════════════════════════════════════ */
|
||
#settings .fb-settings { max-width: 56rem; margin: 0 auto; padding: 6rem 1.5rem 4rem; }
|
||
.fb-settings-back {
|
||
display: inline-flex; align-items: center; gap: .25rem;
|
||
font-size: .8rem; color: #94a3b8; background: none; border: none; cursor: pointer;
|
||
padding: 0; margin-bottom: 1rem; transition: color .15s;
|
||
}
|
||
.fb-settings-back:hover { color: #f8fafc; }
|
||
.fb-settings-back svg { width: 1rem; height: 1rem; }
|
||
.fb-settings-title { font-size: 1.875rem; font-weight: 800; color: #f8fafc; }
|
||
|
||
/* Tab bar */
|
||
.fb-tabbar {
|
||
display: flex; flex-wrap: wrap; gap: .25rem;
|
||
border-bottom: 1px solid rgba(51, 65, 85, .6);
|
||
margin: 1.25rem 0 1.5rem;
|
||
}
|
||
.fb-tab {
|
||
appearance: none; background: none; border: none; cursor: pointer;
|
||
padding: .55rem .85rem; font-size: .85rem; font-weight: 600;
|
||
color: #94a3b8; border-bottom: 2px solid transparent;
|
||
margin-bottom: -1px; transition: color .15s, border-color .15s; white-space: nowrap;
|
||
}
|
||
.fb-tab:hover { color: #e2e8f0; }
|
||
.fb-tab.active { color: #f8fafc; border-bottom-color: #0ea5e9; }
|
||
|
||
/* Panels */
|
||
.fb-tabpanel { display: none; }
|
||
.fb-tabpanel.active { display: block; }
|
||
.fb-tabpanel-head { display: flex; align-items: center; justify-content: space-between; gap: 1rem; margin-bottom: 1rem; }
|
||
.fb-tabpanel-head h3 { font-size: 1.1rem; font-weight: 700; color: #f8fafc; }
|
||
|
||
/* Card rows */
|
||
.fb-srows { display: flex; flex-direction: column; gap: .6rem; }
|
||
.fb-srow {
|
||
display: flex; align-items: center; gap: 1rem;
|
||
background: #1e293b; border: 1px solid rgba(51, 65, 85, .55);
|
||
border-radius: .75rem; padding: .85rem 1rem;
|
||
}
|
||
.fb-srow-stack { flex-direction: column; align-items: stretch; gap: .65rem; }
|
||
.fb-srow-icon {
|
||
flex: none; width: 2.25rem; height: 2.25rem; border-radius: .6rem;
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: rgba(14, 165, 233, .12); color: #38bdf8;
|
||
}
|
||
.fb-srow-icon svg { width: 1.15rem; height: 1.15rem; }
|
||
.fb-srow-main { flex: 1 1 auto; min-width: 0; }
|
||
.fb-srow-title { font-size: .9rem; font-weight: 600; color: #e2e8f0; display: flex; align-items: center; }
|
||
.fb-srow-desc { font-size: .75rem; color: #94a3b8; margin-top: .15rem; }
|
||
.fb-srow-control { flex: none; display: flex; align-items: center; gap: .5rem; }
|
||
.fb-srow-control select,
|
||
.fb-srow-control input[type="text"] { min-width: 11rem; }
|
||
.fb-srow-stack .fb-srow-control { width: 100%; }
|
||
.fb-srow-stack .fb-srow-control input[type="text"] { flex: 1 1 auto; min-width: 0; }
|
||
.fb-srow-wide { width: 100%; }
|
||
|
||
/* Segmented control + fine-tune (Accessibility → Interface size) */
|
||
.fb-seg {
|
||
display: inline-flex; flex-wrap: wrap; gap: .25rem;
|
||
background: #0f172a; border: 1px solid rgba(51, 65, 85, .55);
|
||
border-radius: .7rem; padding: .3rem;
|
||
}
|
||
.fb-seg-btn {
|
||
appearance: none; border: none; cursor: pointer;
|
||
padding: .5rem .9rem; min-height: 2rem; border-radius: .5rem;
|
||
font-size: .82rem; font-weight: 600; color: #94a3b8; background: transparent;
|
||
transition: background .15s, color .15s;
|
||
}
|
||
.fb-seg-btn:hover { color: #e2e8f0; }
|
||
.fb-seg-btn.active { background: #0ea5e9; color: #04263a; }
|
||
.fb-seg-btn:focus-visible { outline: 2px solid #38bdf8; outline-offset: 2px; }
|
||
.fb-finetune { margin-top: .6rem; }
|
||
.fb-finetune > summary {
|
||
font-size: .78rem; font-weight: 600; color: #94a3b8; cursor: pointer;
|
||
list-style: none; display: inline-flex; align-items: center; gap: .35rem;
|
||
}
|
||
.fb-finetune > summary::-webkit-details-marker { display: none; }
|
||
.fb-finetune > summary::before { content: "\25B8"; font-size: .7rem; transition: transform .15s; }
|
||
.fb-finetune[open] > summary::before { transform: rotate(90deg); }
|
||
.fb-finetune-body { display: flex; align-items: center; gap: 1rem; margin-top: .6rem; }
|
||
.fb-seg-val { font-size: .8rem; color: #94a3b8; min-width: 3rem; text-align: right; }
|
||
|
||
/* Toggle switch */
|
||
.fb-switch { position: relative; display: inline-block; width: 2.6rem; height: 1.5rem; flex: none; }
|
||
.fb-switch input { position: absolute; opacity: 0; width: 0; height: 0; }
|
||
.fb-switch .fb-switch-track {
|
||
position: absolute; inset: 0; cursor: pointer;
|
||
background: #334155; border-radius: 999px; transition: background .15s;
|
||
}
|
||
.fb-switch .fb-switch-track::before {
|
||
content: ""; position: absolute; height: 1.1rem; width: 1.1rem; left: .2rem; top: .2rem;
|
||
background: #f8fafc; border-radius: 50%; transition: transform .15s;
|
||
}
|
||
.fb-switch input:checked + .fb-switch-track { background: #0ea5e9; }
|
||
.fb-switch input:checked + .fb-switch-track::before { transform: translateX(1.1rem); }
|
||
.fb-switch input:focus-visible + .fb-switch-track { box-shadow: 0 0 0 2px rgba(56, 189, 248, .5); }
|
||
|
||
/* "Not yet active" stub badge */
|
||
.fb-stub-note {
|
||
font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
|
||
color: #fde68a; background: rgba(234, 179, 8, .12);
|
||
border-radius: 999px; padding: .1rem .45rem; margin-left: .5rem; white-space: nowrap;
|
||
}
|
||
|
||
/* Per-category reset */
|
||
.fb-reset-btn {
|
||
display: inline-flex; align-items: center; gap: .35rem;
|
||
font-size: .8rem; color: #94a3b8; background: none; border: none; cursor: pointer;
|
||
padding: .35rem .5rem; border-radius: .5rem; transition: color .15s, background .15s;
|
||
}
|
||
.fb-reset-btn:hover { color: #fca5a5; background: rgba(239, 68, 68, .08); }
|
||
.fb-reset-btn svg { width: .9rem; height: .9rem; }
|
||
|
||
/* Keybinds reference */
|
||
.fb-kbd {
|
||
display: inline-block; font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
font-size: .72rem; color: #e2e8f0; background: #0b1220;
|
||
border: 1px solid rgba(51, 65, 85, .8); border-bottom-width: 2px;
|
||
border-radius: .35rem; padding: .1rem .4rem; min-width: 1.4rem; text-align: center;
|
||
}
|
||
.fb-kbd-group-title {
|
||
font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em;
|
||
color: #94a3b8; margin: 1.25rem 0 .5rem;
|
||
}
|
||
.fb-settings-note { font-size: .75rem; color: #64748b; margin-top: 1rem; }
|
||
.fb-tabpanel-empty { font-size: .8rem; color: #64748b; padding: .5rem 0; }
|
||
|
||
/* ── Immersive (full-screen) plugin screens ──────────────────────────────────
|
||
A plugin that declares `"fullscreen": true` in its manifest gets the whole
|
||
content area when its screen is active. shell.js toggles `html.fb-immersive`
|
||
on navigation (see syncActive). DAW-style plugin UIs (e.g. a practice studio)
|
||
need the viewport, not a scrolling content page below the topbar — the cause
|
||
of the "cut off at the bottom / too much top padding" reports on the embedded
|
||
layout. Mirrors the proven chrome-hide pattern in static/v3/index.html
|
||
(`html.ss-follower-pre …`), but keeps the sidebar as a functional icon rail
|
||
so the user is never trapped (Escape is bound only on player/settings
|
||
scopes, not plugin screens). */
|
||
html.fb-immersive #v3-topbar { display: none !important; }
|
||
|
||
/* Sidebar → icon rail: narrow it, drop the wordmark + group headers + labels,
|
||
center the remaining icons. The .v3-nav-label / .v3-nav-group hooks are added
|
||
by shell.js so these rules don't depend on Tailwind utility class strings. */
|
||
html.fb-immersive #v3-sidebar { width: 4.5rem; }
|
||
html.fb-immersive #v3-brand { display: none; }
|
||
html.fb-immersive #v3-nav { padding-left: .5rem; padding-right: .5rem; }
|
||
html.fb-immersive #v3-nav .v3-nav-group,
|
||
html.fb-immersive #v3-nav .v3-nav-label { display: none; }
|
||
html.fb-immersive #v3-nav a {
|
||
justify-content: center;
|
||
gap: 0;
|
||
padding-left: 0;
|
||
padding-right: 0;
|
||
}
|
||
|
||
/* The active plugin screen fills the now-topbar-less content area. #v3-main is
|
||
position:relative, so pinning the screen avoids relying on the auto-height
|
||
block flow that made a 100vh plugin overflow #v3-main and force a scroll. */
|
||
html.fb-immersive #v3-main > .screen.active {
|
||
position: absolute;
|
||
inset: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* — A–Z jump rail (v3 Songs grid; static/v3/songs.js) — */
|
||
/* Fixed to the right edge next to the scroller's scrollbar; vertically
|
||
centered. Shown only for the grid view + alphabetical (artist/title) sorts. */
|
||
.v3-azrail {
|
||
position: fixed;
|
||
right: 4px; /* off the very edge so letters aren't clipped */
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
z-index: 25;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: stretch; /* equal-width buttons → one wide, even hit column */
|
||
max-height: 92vh;
|
||
padding: 4px 1px;
|
||
user-select: none;
|
||
-webkit-user-select: none;
|
||
touch-action: none; /* let a drag scrub the rail without scrolling the page */
|
||
}
|
||
.v3-azrail-letter {
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
background: none;
|
||
border: 0;
|
||
color: #94a3b8; /* fb-textDim */
|
||
/* Scale with viewport height so the 27-letter rail grows on tall / hi-res
|
||
displays (a fixed size looked tiny at 1440p) while still fitting 27 rows
|
||
within max-height on short screens. */
|
||
font-size: clamp(.72rem, 1.4vh, 1.05rem);
|
||
font-weight: 700;
|
||
line-height: 1.04;
|
||
/* Vertical padding fattens the tap target (was ~13px tall → easy to miss). */
|
||
padding: clamp(2px, .55vh, 6px) 9px;
|
||
margin: 0;
|
||
cursor: pointer;
|
||
border-radius: 4px;
|
||
text-align: center;
|
||
}
|
||
.v3-azrail-letter:hover:not([disabled]),
|
||
.v3-azrail-letter.is-active {
|
||
color: #0ea5e9; /* fb-primary */
|
||
background: rgba(14, 165, 233, .15); /* visible target under the scrub */
|
||
}
|
||
.v3-azrail-letter:focus-visible {
|
||
outline: 2px solid #38bdf8; /* fb-primaryHi */
|
||
outline-offset: 1px;
|
||
}
|
||
.v3-azrail-letter[disabled] {
|
||
color: rgba(148, 163, 184, .28);
|
||
cursor: default;
|
||
}
|
||
/* Drag indicator bubble (Android fast-scroll pattern). */
|
||
.v3-azbubble {
|
||
position: fixed;
|
||
right: 2.9rem; /* clear the (now wider) rail */
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
z-index: 26;
|
||
width: 2.6rem;
|
||
height: 2.6rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: .7rem;
|
||
background: #0ea5e9; /* fb-primary */
|
||
color: #f8fafc; /* fb-text */
|
||
font-size: 1.15rem;
|
||
font-weight: 800;
|
||
box-shadow: 0 6px 22px rgba(0, 0, 0, .45);
|
||
pointer-events: none;
|
||
}
|
||
.v3-azrail.hidden,
|
||
.v3-azbubble.hidden { display: none; }
|
||
/* Coarse-pointer / short viewports: the 27-letter rail can crowd a phone edge.
|
||
Tighten it; a collapse-to-anchors pass is a follow-up. */
|
||
@media (max-height: 640px) {
|
||
.v3-azrail-letter { font-size: clamp(.5rem, 1.3vh, .62rem); padding: 0 7px; }
|
||
}
|
||
|
||
/* — Practice-aware library home: repertoire meter + "Keep practicing" shelf — */
|
||
#v3-lib-home.hidden { display: none; }
|
||
.v3-rep-meter { max-width: 30rem; }
|
||
.v3-rep-track {
|
||
height: 6px;
|
||
border-radius: 999px;
|
||
background: rgba(148, 163, 184, .22); /* fb-textDim @ low alpha */
|
||
overflow: hidden;
|
||
}
|
||
.v3-rep-fill {
|
||
height: 100%;
|
||
border-radius: 999px;
|
||
background: #0ea5e9; /* fb-primary */
|
||
transition: width .4s ease;
|
||
}
|
||
/* Horizontal, scroll-snapping shelf of fixed-width cards. */
|
||
.v3-kp-row {
|
||
display: flex;
|
||
gap: .75rem;
|
||
overflow-x: auto;
|
||
scroll-snap-type: x proximity;
|
||
padding-bottom: 6px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
.v3-kp-card {
|
||
flex: 0 0 8.5rem;
|
||
width: 8.5rem;
|
||
scroll-snap-align: start;
|
||
}
|
||
|
||
/* — Windowed (virtualized) Songs grid (#636 item 3 stage 2) — */
|
||
/* The grid is absolutely positioned inside #v3-songs-gridsizer, whose height is
|
||
set to the FULL library (ceil(total/cols)*rowH) so the scrollbar reflects the
|
||
whole library while only the visible window's cards are in the DOM. The inline
|
||
`top` (set by renderWindow) offsets the window to the first visible row. */
|
||
.v3-grid-window {
|
||
position: absolute;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
}
|
||
/* The arrangement-chip row is rendered on EVERY card (even when empty) at a fixed
|
||
single-line height — uniform card height is what makes the window's
|
||
absolute-position math exact. Extra chips are clipped rather than wrapping. */
|
||
.v3-card-chips {
|
||
height: 1.5rem;
|
||
overflow: hidden;
|
||
flex-wrap: nowrap;
|
||
}
|
||
/* Skeleton placeholder shown only if a window's fetch hasn't landed; mirrors a
|
||
real card's vertical structure so it occupies an identical row height. */
|
||
.v3-card-skel { pointer-events: none; }
|