feedBack/static/v3/v3.css
ChrisBeWithYou 727b8c8f24
feat(a11y): app-wide "Interface size" setting (Accessibility) (#664)
* 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>
2026-07-02 14:00:30 +02:00

1365 lines
53 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

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

/*
* 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;
}
/* — AZ 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; }