0
mirror of https://github.com/torvalds/GuitarPedal.git synced 2026-08-19 05:33:46 +00:00
Files
torvalds-GuitarPedal/WebMIDI/style.css
Linus Torvalds 0c1b9c3db3 Split Software/ into the four things it actually was
'Software' was the directory everything that was not KiCad ended up in,
which stopped describing anything a while ago - Validation and the web
app are software too.  Worse, it put the shared parts inside the
firmware, where they read as the firmware's own.

They are not.  Effects/ has three consumers built from it: the firmware,
Validation's bench, and the web app's controls, all generated from the
same POT: comments by gen_effects.py.  Audio/ has two - the bench
compiles the same biquads, the same envelope followers and the same
single_sample(), which is the whole reason a measurement on a
workstation says anything about the pedal.  Neither belongs under
Firmware/, so neither is under it any more:

  Effects/    one file per effect
  Audio/      the DSP they are built from, and the audio loop
  Firmware/   the rest of what runs on the pedal, and the submodules
  WebMIDI/    the web app
  scripts/    what the build runs
  Validation/ unchanged
  Hardware/, Documentation/, Images/

CMakeLists.txt and the wrapper Makefile move to the top with them,
because the build now consumes four of those directories and generates
into a fifth.  board.local and build/ come along; MIDI_CC_MAP.md is
generated into Documentation/ rather than into the old Software/ root.

scripts/ goes with the build rather than staying under the firmware,
because six of the ten had nothing to do with the firmware: gen_effects.py
reads Effects/ and writes to three different places, pow2/log2/quarter_sine
generate Audio/'s tables, check-readme.py compares Effects/ against the
README, and server.py serves the web app.  Four of them are invoked from
Validation, which was reaching into Firmware/ for tooling - the same
burying this commit is undoing.  The four that really are about the
firmware are ELF checks the top-level build drives anyway, and a second
scripts directory would only be a second place to look.

C includes say "Audio/foo.h" and the generated map says
"Effects/bar.h", with the repository root on the include path for both
the firmware and the bench.  Spelling the directory out rather than
relying on a bare name is what keeps Audio/cycles.h shimmable: a quoted
include searches the including file's own directory first.

The submodules are renamed as well as moved.  git mv updates their paths
but leaves the section names, and 'Software/pico-sdk' surviving in
.gitmodules would be the word this commit removes, still load-bearing.
That meant the nested modules under pico-sdk too - six .git files
pointing into .git/modules/Software - which is why 'git submodule update
--init --recursive' is worth running once after pulling this.

Verified rather than assumed: a clean configure and build, make check
(failing only on the missing-eeprom case it already failed on),
check-effects, all four analysis pages reproducing every series and
drawing every chart, and a flash to the board that still measures a
routed reverb where it did before.

One latent bug fell out of it.  bench/coeff declared only quarter_sine.h
of the three generated math tables, and Audio/util.h includes pow2.h and
log2.h as well - so building that target with an empty gen/ could never
have worked.  'make bench' builds bench/bench first, which generates all
three, so it stayed hidden until this rebuilt everything from nothing.

Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
2026-08-11 13:48:26 -07:00

1412 lines
31 KiB
CSS

:root {
--bg-color: #0f1115;
--panel-bg: rgba(255, 255, 255, 0.03);
--panel-border: rgba(255, 255, 255, 0.05);
--text-main: #e2e8f0;
--text-muted: #94a3b8;
--accent: #3b82f6;
--accent-hover: #60a5fa;
--danger: #ef4444;
--success: #10b981;
/* A locally served copy, in place of --success/--text-muted */
--dev: #f59e0b;
--dev-muted: #8a7040;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-main);
line-height: 1.5;
padding: 20px;
}
.app-container {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 20px;
}
.glass-panel {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 12px;
padding: 20px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
header {
display: flex;
justify-content: space-between;
align-items: center;
}
h1 {
font-size: 1.5rem;
font-weight: 600;
}
h2 {
font-size: 1.2rem;
margin-bottom: 15px;
color: var(--text-muted);
}
/*
* Two rows: what the header does, then what it says.
*
* It was one row, and it did not fit on a phone - the title alone was
* most of a 360px screen, and the controls did not fit in what was left
* even without it. Widening the page to hold it all is the one outcome
* worth ruling out, since it costs you every card below as well.
*
* So the controls get the row, at the size a thumb needs, and the title
* and the fault chips go underneath in small text. Nothing was lost:
* the title is a device name that does not change while you look at it,
* the chips are absent nearly always, and what you actually read in the
* moment is the colour - which a small word carries exactly as well as
* a large one.
*/
.app-header {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 10px;
/*
* The menu hangs out of the bottom of this, and has to be able to
* cover the cards below. It can't do that on its own z-index:
* .glass-panel sets backdrop-filter, which creates a stacking
* context, so the menu's z-index only ranks it inside this header.
* The cards are .glass-panel too and come later in the document, so
* without this they paint over the menu - and being backdrop
* filters, they blur it while they're at it.
*
* Stays well below the backdrop (900) and the panels above it.
*/
position: relative;
z-index: 20;
}
/*
* The heading stays first in the document and second on the screen.
* Ordering it here rather than in the markup keeps the page's one <h1>
* ahead of a row of buttons for anything reading the document instead
* of looking at it.
*/
.header-controls {
order: 1;
display: flex;
align-items: center;
gap: 15px;
}
.header-status {
order: 2;
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
/*
* The name gives way and the faults do not. A truncated device name is
* still the same colour and still says what it needs to; a truncated
* CLIP is a fault you were not told about.
*/
#app-title {
font-size: 0.8rem;
font-weight: 500;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.3s;
}
/*
* The title is the pedal-state light, and it carries where the page
* came from as well: hue says which copy this is, brightness says
* whether the pedal is answering. Two signals, one word, no badge.
*/
.title-connected {
color: var(--success);
}
.title-disconnected {
color: var(--text-muted);
}
body.local-dev .title-connected {
color: var(--dev);
}
body.local-dev .title-disconnected {
color: var(--dev-muted);
}
.control-group label {
font-size: 0.9rem;
color: var(--text-muted);
}
select {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid var(--panel-border);
color: var(--text-main);
padding: 10px 15px;
border-radius: 8px;
font-size: 1rem;
outline: none;
cursor: pointer;
appearance: none;
}
select:focus {
border-color: var(--accent);
}
option {
background-color: var(--bg-color);
color: var(--text-main);
}
/* Toggle Switch */
.switch {
position: relative;
display: inline-block;
width: 50px;
height: 28px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0; left: 0; right: 0; bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .3s;
}
.slider:before {
position: absolute;
content: "";
height: 20px;
width: 20px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .3s;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(22px);
}
.slider.round {
border-radius: 34px;
}
.slider.round:before {
border-radius: 50%;
}
.enable-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.action-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--panel-border);
color: var(--text-muted);
font-size: 1.1rem;
width: 32px;
height: 32px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
padding-bottom: 2px;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.15);
color: var(--text-main);
transform: scale(1.1);
}
.action-btn:active {
transform: scale(0.95);
}
/* Pots */
.pots-section {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
.pot-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.pot-label {
font-size: 0.9rem;
color: var(--text-muted);
text-align: center;
}
/*
* A pot that can explain itself says so, faintly. Dotted rather than
* a cursor change, because half of these are used on a touchscreen
* where there is no hover and no pointer to change.
*/
.pot-control.has-info > .pot-label {
text-decoration: underline dotted;
text-underline-offset: 0.2em;
}
.pot-value {
font-size: 1.1rem;
font-weight: 500;
}
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
margin-top: -6px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
}
/*
* Focus still needs to be visible - it's the only cue a keyboard user
* gets - but the default outline looks wrong on a round thumb, so put a
* ring on the thumb instead.
*/
input[type=range]:focus {
outline: none;
}
input[type=range]:focus::-webkit-slider-thumb {
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
}
input[type=range]:focus::-moz-range-thumb {
box-shadow: 0 0 0 5px rgba(59, 130, 246, 0.35);
}
/* Diagnostics */
.diag-grid {
display: flex;
gap: 30px;
}
.diag-item {
display: flex;
align-items: center;
gap: 10px;
}
.diag-label {
font-size: 0.9rem;
color: var(--text-muted);
}
.indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
}
.indicator.active {
background-color: var(--danger);
box-shadow: 0 0 10px var(--danger);
}
.value {
font-family: monospace;
font-size: 1.1rem;
background: rgba(0, 0, 0, 0.2);
padding: 4px 8px;
border-radius: 4px;
}
/* Effect Cards */
#effects-container {
display: flex;
flex-direction: column;
gap: 20px;
}
.effect-card {
display: flex;
flex-direction: column;
gap: 15px;
}
.effect-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--panel-border);
padding-bottom: 10px;
/*
* The header is what you grab to reorder, so none of it may behave
* like text. A long press on text is the browser's select gesture,
* which on a phone is what you got instead of a drag, and iOS puts
* its callout menu up on top of that. This has to be here rather
* than switched on once a drag is recognised: by then the selection
* has already started.
*/
user-select: none;
-webkit-user-select: none;
-webkit-touch-callout: none;
}
/*
* A mouse grabs the whole header. A finger grabs the handle, and only
* the handle - see cardDragStart().
*
* The header used to be the grip for both, on the reasoning that a
* header is finger-sized and a ~20px glyph is not. That is true and it
* was still the wrong trade: touch-action has to be on whatever the
* touch lands on, so making the whole header the grip made the whole
* header a place the page could not be scrolled from - and a card
* header is a wide bar that a thumb lands on constantly on the way past.
* Reaching for a scroll and picking a card up instead is a worse failure
* than a grip that has to be aimed at, because you did not ask for it.
*
* A mouse has no scroll-by-dragging gesture to be confused with, so it
* loses nothing by keeping the big grip.
*/
/*
* pan-y, not none: the page still scrolls vertically from a card header,
* which is the point of narrowing the grip. Sideways is ours, and the
* page has nowhere to go sideways, so claiming it costs nothing - that
* is the room the flick-to-unroute gesture lives in. See cardSwipeStart().
*/
.effect-header.draggable {
cursor: grab;
touch-action: pan-y;
}
/* Only while it springs back - a card following a finger must not lag */
.effect-card.swipe-return {
transition: transform 180ms ease-out, opacity 180ms ease-out;
}
/*
* The right answer to a 20px glyph was always a 44px target around it,
* which is what this is. touch-action lives here now: this is the one
* place in a card that a drag can start, so it is the one place that
* does not scroll.
*/
.drag-handle {
display: flex;
align-items: center;
justify-content: center;
min-width: 44px;
min-height: 44px;
margin-right: 4px;
font-size: 1.4em;
opacity: 0.7;
cursor: grab;
touch-action: none;
}
.effect-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--accent);
}
.effect-controls {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 20px;
}
/* Graphic EQ specific styles */
.effect-controls.eq-container {
display: block;
position: relative;
padding-bottom: 20px;
}
.eq-curve-wrapper {
position: relative;
width: 100%;
height: 300px;
margin-top: 20px;
z-index: 1;
pointer-events: auto;
cursor: crosshair;
}
/*
* The canvas is a drag surface in both directions - a node carries a
* frequency sideways and a gain vertically - so there is no axis left to
* give the browser, and it gets none.
*
* This is not new behaviour. The old touchstart handler called
* preventDefault() unconditionally, which claimed the gesture the same
* way; saying it in touch-action is what pointer events need instead,
* and it says it where it can be seen.
*/
.eq-canvas {
width: 100%;
height: 100%;
display: block;
touch-action: none;
}
/*
* The sliders behind the curve.
*
* Every graphed pot still has one, because the slider is what holds the
* value and what the pedal's updates arrive on - the node just moves it.
* None of them are ever seen, so this is the whole of their styling.
*/
.eq-sliders {
display: none;
}
.eq-curve-svg {
width: 100%;
height: 100%;
overflow: visible;
}
.eq-path {
fill: rgba(59, 130, 246, 0.15); /* var(--accent) transparent */
stroke: var(--accent);
stroke-width: 3px;
vector-effect: non-scaling-stroke;
}
/*
* The row under the curve: the Mix control, and the switches for how
* the graph behaves.
*
* Mix takes the width it needs and the switches go to the far end, so
* that the two are read as the different things they are - one is a
* parameter the pedal stores, the others are how this screen behaves
* and go no further than this browser.
*
* It wraps, because on a narrow screen a row of both is neither.
*/
.eq-footer {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px 24px;
margin-top: 10px;
}
.eq-footer .pot-control {
flex: 1 1 180px;
}
.eq-options {
margin-left: auto;
display: flex;
flex-wrap: wrap;
gap: 4px 20px;
}
/* The label is part of the target, which is what gets it to 44px */
.eq-option {
display: flex;
align-items: center;
gap: 8px;
min-height: 44px;
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.eq-option input {
width: 18px;
height: 18px;
accent-color: var(--accent);
cursor: pointer;
}
/* Bottom Panels (Generic) */
/* Panel Backdrop */
#panel-backdrop {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
z-index: 900;
transition: opacity 0.3s;
}
#panel-backdrop.hidden {
display: none !important;
}
.bottom-panel {
position: fixed;
bottom: 0;
left: 0;
right: 0;
margin: 0;
border-radius: 20px 20px 0 0;
border-bottom: none;
z-index: 1000;
padding: 20px 30px;
background: rgba(15, 17, 21, 0.95);
box-shadow: 0 -10px 40px rgba(0, 0, 0, 0.8);
transform: translateY(110%);
transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.bottom-panel:not(.hidden) {
transform: translateY(0);
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 25px;
}
.panel-header h2 {
margin: 0;
color: var(--text-main);
}
/*
* The menu proper: a dropdown hung off the burger, holding actions and
* nothing else. It deliberately does not dim the page behind it - it
* isn't modal, and a menu that darkens the whole app to offer four
* items reads as heavier than it is.
*/
.burger-wrap {
position: relative;
}
.menu-dropdown {
position: absolute;
z-index: 1000;
top: calc(100% + 8px);
/*
* Anchored to the left edge of the button, because that is the edge
* the button is now against. Anchored right, a menu wider than the
* burger grows leftwards - which was correct while it sat in the top
* right corner, and walks straight off the screen from the top left.
*/
left: 0;
/*
* Sized to whatever the longest item happens to be, rather than to a
* number that was right until someone added a word. "Reboot to
* Programming Mode" wrapped at 260px, and the scene items change
* width as you pick different scenes, so nothing here has a length
* worth hard-coding. The floor keeps a short menu from looking
* mean; the ceiling keeps a long one on the screen.
*/
width: max-content;
min-width: 260px;
max-width: calc(100vw - 32px);
padding: 6px;
background: rgba(20, 23, 29, 0.98);
border: 1px solid var(--panel-border);
border-radius: 12px;
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.65);
}
/* The bottom sheet hid itself by sliding off the bottom edge, so there
has never been a general .hidden rule to inherit. */
.menu-dropdown.hidden,
.dialog-panel.hidden {
display: none;
}
.menu-dropdown button {
display: block;
width: 100%;
text-align: left;
background: none;
border: 0;
color: var(--text-main);
padding: 11px 14px;
border-radius: 8px;
cursor: pointer;
font: inherit;
font-size: 0.98rem;
white-space: nowrap;
}
.menu-dropdown button:hover {
background: rgba(255, 255, 255, 0.08);
}
.menu-dropdown button.danger-item {
color: var(--danger);
}
.menu-dropdown button.danger-item:hover {
background: rgba(239, 68, 68, 0.12);
}
.menu-dropdown hr {
border: 0;
border-top: 1px solid var(--panel-border);
margin: 6px 8px;
}
/* Which scene you're on, promoted out of the menu into the header.
Saving and loading stayed behind the menu - see index.html. */
/*
* The gap in the row, and so what splits it in two: the menu and the
* scene on the left, because those are about editing, and the tuner and
* the bypass on the right, because those are what the pedal is doing
* right now.
*
* One auto margin rather than space-between, which would have spread all
* four evenly across a desktop's worth of empty header.
*/
.header-scenes {
display: flex;
align-items: center;
margin-right: auto;
}
.header-scenes select {
background: rgba(255, 255, 255, 0.06);
color: var(--text-main);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 6px 10px;
font: inherit;
font-size: 0.88rem;
}
/*
* Settings, which is a dialog rather than a sheet: it is centred and
* sized to its content instead of anchored to the bottom edge, because
* nothing about it is phone-shaped any more.
*/
.dialog-panel {
position: fixed;
z-index: 1000;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: min(460px, calc(100vw - 40px));
max-height: calc(100vh - 40px);
overflow-y: auto;
padding: 0;
background: rgba(15, 17, 21, 0.97);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7);
}
.dialog-panel .panel-header {
margin: 0;
padding: 18px 22px;
border-bottom: 1px solid var(--panel-border);
position: sticky;
top: 0;
background: rgba(15, 17, 21, 0.97);
}
.dialog-body {
padding: 6px 22px 22px;
}
.menu-section {
margin-top: 22px;
}
.menu-section > h3 {
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.09em;
text-transform: uppercase;
color: var(--text-muted);
margin: 0 0 10px;
padding-bottom: 6px;
border-bottom: 1px solid var(--panel-border);
}
.menu-field {
margin-bottom: 10px;
}
.menu-field > label {
display: block;
font-size: 0.82rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.menu-hint {
font-size: 0.8rem;
color: var(--text-muted);
margin-top: 6px;
}
.menu-select {
width: 100%;
background: #1a1d23;
color: var(--text-main);
padding: 9px 10px;
border-radius: 8px;
border: 1px solid var(--panel-border);
font: inherit;
font-size: 0.95rem;
}
/*
* showButtonSuccess()/showButtonError() swap a button's label for a
* moment and add one of these. They aren't tied to a particular kind
* of button, because the buttons that report back like this are now
* both menu items and header controls - hence !important, which beats
* the two-selector rules those live under without having to enumerate
* every context a reporting button might turn up in.
*/
.success {
background: rgba(16, 185, 129, 0.2) !important;
color: var(--success) !important;
border-color: var(--success) !important;
}
.error {
background: rgba(239, 68, 68, 0.2) !important;
color: var(--danger) !important;
border-color: var(--danger) !important;
}
.active-pot-info {
display: flex;
align-items: center;
gap: 15px;
}
#active-pot-title {
font-size: 1.2rem;
font-weight: 600;
color: var(--text-main);
}
#active-pot-value {
font-size: 1.3rem;
color: var(--accent);
background: rgba(0, 0, 0, 0.3);
padding: 4px 12px;
border-radius: 8px;
}
.active-pot-slider-container {
padding-bottom: 20px;
}
/* The knob shortcut, and what it is about to take the knob away from. */
.active-pot-knob {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 18px;
}
.active-pot-knob > .menu-hint {
margin-top: 0;
}
#assign-knob-btn {
flex: none;
width: auto;
padding: 6px 12px;
font-size: 0.85rem;
white-space: nowrap;
}
#assign-knob-btn:disabled {
opacity: 0.45;
cursor: default;
}
/* One gesture, and everything it does. */
.rule-group {
margin-bottom: 20px;
}
/*
* The gesture's name and its add button, laid out like a rule row so
* that the + lands in the same column as the x below it - which is
* also what makes it obvious which gesture it adds to.
*
* Both keep .action-btn's own size. They are circles, and a circle
* told to be any other shape stops being one.
*/
.rule-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.rule-head > label {
flex: 1;
min-width: 0;
font-size: 0.82rem;
color: var(--text-muted);
}
.rule-add {
flex: none;
opacity: 0.5;
}
.rule-add:hover {
opacity: 1;
}
.rule-add:disabled {
opacity: 0.2;
cursor: default;
}
/*
* A rule reads left to right as a sentence - what it does, what it does
* it to, and the values it does it with - wrapping onto a second line
* when there are values. The remove button is outside that, beside the
* whole rule, because it belongs to the rule rather than to any line of
* it. It also has to keep .action-btn's fixed size: it is a circle,
* and a circle stretched to fill a grid cell is an oval.
*/
.rule-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.rule-body {
flex: 1;
min-width: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
align-items: center;
}
/* An action with nothing to point at gets the line to itself. */
.rule-body:not(.has-target) > .menu-select {
grid-column: 1 / -1;
}
.rule-remove {
flex: none;
font-size: 0.95rem;
opacity: 0.5;
}
.rule-remove:hover {
opacity: 1;
}
/*
* A value is the pot's own control in miniature - the slider and the
* readout it would have on its card, in the units it would use there.
*/
.binding-value {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
}
.binding-value > input[type="range"] {
flex: 1;
min-width: 0;
}
.binding-value > .pot-value {
flex: none;
font-size: 0.8rem;
color: var(--text-muted);
white-space: nowrap;
}
/* Larger slider for fine adjustments */
input[type=range].large-slider {
height: 40px;
}
input[type=range].large-slider::-webkit-slider-thumb {
height: 32px;
width: 32px;
margin-top: -14px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
input[type=range].large-slider::-webkit-slider-runnable-track {
height: 8px;
border-radius: 4px;
}
/* Make pot-controls clickable */
.pot-control, .pot-control * {
cursor: pointer;
}
/* Tuner Overlay */
#tuner-panel {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: var(--bg-color);
z-index: 2000;
display: flex;
flex-direction: column;
padding: 30px;
border-radius: 0;
border: none;
justify-content: center;
gap: 30px;
}
#tuner-panel.hidden {
display: none !important;
}
.tuner-header {
position: absolute;
top: 30px;
left: 30px;
right: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.tuner-chromatic-display {
display: flex;
justify-content: center;
align-items: baseline;
gap: 15px;
margin-top: 50px;
}
.tuner-note {
font-size: 5rem;
font-weight: bold;
color: var(--accent);
text-align: right;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.tuner-cents {
font-size: 2.5rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
line-height: 1;
}
.tuner-vol {
font-size: 1.2rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
line-height: 1;
opacity: 0.7;
}
.tuner-meter-container {
position: relative;
height: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 12px;
overflow: hidden;
margin-top: 10px;
}
.tuner-meter-center {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 2px;
background-color: rgba(255, 255, 255, 0.2);
transform: translateX(-50%);
}
.tuner-needle {
position: absolute;
top: 0;
bottom: 0;
width: 6px;
background-color: var(--success);
left: 50%;
transform: translateX(-50%);
transition: left 0.1s linear, background-color 0.2s;
border-radius: 3px;
}
.tuner-strings {
display: flex;
justify-content: space-around;
margin-top: 40px;
}
.tuner-string {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
width: 60px;
}
.tuner-string-name {
font-weight: bold;
font-size: 1.5rem;
color: var(--text-muted);
}
.tuner-string-name.active {
color: var(--text-main);
}
.tuner-string-arrow {
width: 0;
height: 0;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
transition: all 0.1s;
}
.tuner-string-arrow.up {
border-bottom: 20px solid var(--danger);
border-top: none;
background: transparent;
width: 0; height: 0; border-radius: 0;
}
.tuner-string-arrow.down {
border-top: 20px solid var(--danger);
border-bottom: none;
background: transparent;
width: 0; height: 0; border-radius: 0;
}
.tuner-string-arrow.perfect {
border: none;
width: 24px;
height: 24px;
border-radius: 50%;
background-color: var(--success);
}
.tuner-string-arrow.inactive {
border: none;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: rgba(255, 255, 255, 0.1);
}
.tuner-string-vol {
font-size: 0.9rem;
color: var(--text-muted);
font-variant-numeric: tabular-nums;
opacity: 0.7;
}
#tuner-btn.active {
background: rgba(59, 130, 246, 0.5);
color: white;
border-color: var(--accent);
}
/*
* The pool of effects that are not in the chain.
*
* These used to be full cards below a divider, greyed out and collapsed,
* which is a lot of screen for a list of things the pedal is not running.
* A chip carries everything true about an unrouted effect - its name -
* and the whole pool now costs about what one card used to.
*
* No general .hidden rule to inherit, so declare it.
*/
.effect-pool {
margin-top: 1.5rem;
}
.effect-pool.hidden {
display: none;
}
.pool-label {
color: var(--text-muted);
font-size: 0.9rem;
margin-bottom: 0.75rem;
}
.pool-chips {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
/*
* Finger-sized from the start. 44px is the smallest target worth
* offering on a touchscreen, and a chip has no reason to be smaller -
* unlike the controls it sits under, which still need that pass.
*/
.effect-chip {
min-height: 44px;
padding: 0 16px;
border: 1px solid var(--panel-border);
border-radius: 22px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-main);
font-size: 1rem;
font-family: inherit;
cursor: pointer;
transition: background 0.2s, border-color 0.2s, transform 0.1s;
}
.effect-chip:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.12);
border-color: var(--accent);
}
.effect-chip:active:not(:disabled) {
transform: scale(0.96);
}
.effect-chip:disabled {
opacity: 0.4;
cursor: default;
}
/*
* Parked: routed effects are the ones on screen, but the card of an
* unrouted one stays in the document because the pot elements inside it
* are what the pedal's PARAM_UPDATE messages address. See applyRouting().
*/
.effect-card.parked {
display: none;
}
/*
* What the pedal is up to.
*
* Two different things with two different visual languages, because they
* are two different kinds of news. A fault - the output clipping, the
* audio core missing its deadline - is a warning and belongs in the
* header. An effect asking for attention is that effect *working*, and
* belongs on that effect.
*
* No general .hidden rule to inherit, so declare it (see above).
*/
#pedal-status.hidden,
#status-clip.hidden,
#status-drop.hidden {
display: none;
}
.pedal-status {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.06em;
/* The title next to this is what shrinks - see #app-title */
flex-shrink: 0;
}
.status-clip,
.status-drop {
padding: 2px 7px;
border-radius: 4px;
white-space: nowrap;
}
/* Clipping is your fault and fixable by playing softer or trimming. */
.status-clip {
color: var(--danger);
border: 1px solid var(--danger);
}
/* Missing the deadline is the pedal's, and means the chain is too much. */
.status-drop {
color: var(--dev);
border: 1px solid var(--dev);
}
/*
* An effect wants attention: the gate has closed, the compressor is
* compressing, the boost is folding.
*
* The asymmetric transition is the whole trick, and it is the same one
* the pedal's LED gets for free from being updated slowly. Switching on
* is instant and switching off takes a moment, so a single event is
* visible at all rather than a 40ms flicker nobody catches, while
* something happening constantly reads as solid instead of a strobe.
*/
.effect-card {
transition: box-shadow 400ms ease-out;
}
.effect-card.attention {
box-shadow: 0 0 0 1px var(--accent), 0 0 12px -2px var(--accent);
transition: box-shadow 0ms;
}
/*
* The pedal is not the hardware this firmware was built for - an early
* board answering on i2c, or the other eeprom. Filled rather than
* outlined like the other two, because it is not a transient to glance
* at: it will not clear itself, and the symptoms are quiet ones. Hover
* for which, or open MIDI Connection.
*/
#status-board.hidden {
display: none;
}
.status-board {
padding: 2px 7px;
border-radius: 4px;
white-space: nowrap;
color: var(--bg-color);
background: var(--danger);
}
/*
* An early board: information rather than a fault.
*
* Those still play - the pedal sets the TAC5112 up over i2c, and the
* eeprom geometry is worked out rather than compiled in, so a 2kbit part
* gets its one scene. Mono, because they never routed the second channel.
* Worth knowing which board is on the bench; not worth the red one.
*/
#status-early.hidden {
display: none;
}
.status-early {
padding: 2px 7px;
border-radius: 4px;
white-space: nowrap;
color: var(--text-muted);
border: 1px solid var(--text-muted);
}
/*
* The Signal Chain's meters.
*
* Deliberately plain text rather than bars. The number is what you act on
* - Level is set in dB and the floor is reported in dB, so the useful
* gesture is comparing two numbers, and a bar would make that harder
* rather than easier.
*
* Tabular figures so the digits do not shuffle sideways five times a
* second while you watch them.
*/
.meters {
font-size: 0.75rem;
color: var(--text-muted);
border-top: 1px solid var(--panel-border);
padding-top: 10px;
font-variant-numeric: tabular-nums;
font-feature-settings: "tnum";
}
/*
* A phone, or something near enough.
*
* Last in the file deliberately: a media query adds no specificity, so
* anything it overrides has to have been declared already or the plain
* rule wins and this looks like it does nothing.
*
* 20px of body padding either side of 20px of panel padding is 80px of
* nothing, which is a fifth of a 360px screen and reads as generous only
* on a desktop. This gives the content back the width it was spending
* on margins. It deliberately does not shrink any control: that is the
* direction the rest of this work is trying to go the other way.
*/
@media (max-width: 600px) {
body {
padding: 10px;
}
.glass-panel {
padding: 14px;
}
/* Horizontal only - the height of a select is its tap target */
select {
padding: 10px 12px;
}
}
/*
* A rule this scene did not set, shown because the control still does
* it - from the pedal-wide table or from the built-in defaults. Faded
* rather than hidden: "nothing here" and "nothing of its own here" are
* different, and only one of them means the footswitch does nothing.
*/
.rule-row.inherited {
opacity: 0.55;
}