A lit pad kept its grey text and only its border turned orange, so pads
looked unhighlighted next to every other key. Cause was cascade order,
not logic: .pad is a single-class selector like .lit, so equal
specificity meant whichever came last won, and .pad was declared after.
The state classes now sit at the bottom of the file with a comment
saying why, and a lit pad computes rgb(255,74,0) for text and border.
native.ts reported nothing when haptics failed because I wrote
catch(() => {}) and discarded the reason. It now reports once per
distinct failure - enough to read in logcat, quiet enough not to spam on
every key press.
That reporting shows the software path is fine: isNativePlatform true,
Haptics available and bridged, VIBRATE granted, and both impact and
vibrate resolve without rejecting. So the silence is below the bridge -
most likely the phone's own touch-vibration setting - and not something
this code can detect.
141 tests, typecheck clean, installed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
304 lines
5.6 KiB
CSS
304 lines
5.6 KiB
CSS
/* Measured geometry: 8 + 36 + 8 + 124 + 10 + 134 + 10 + 187 + 8 = 525.
|
|
*
|
|
* Ordering matters here: the state classes (.lit, .sel, .held, .down) live at
|
|
* the BOTTOM of this file. They are single-class selectors, same specificity as
|
|
* .pad or .fx, so whichever comes last wins. With .pad declared after .lit, a
|
|
* lit pad kept its grey text and only the border turned orange — pads looked
|
|
* unhighlighted next to every other key. */
|
|
|
|
.panel {
|
|
height: var(--panel-h);
|
|
padding: 8px;
|
|
background: var(--body);
|
|
border: var(--outline);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
flex-direction: column;
|
|
/* Without this the browser claims a second finger as a pinch or a pan, and a
|
|
chord can never be formed. This is what makes multitouch work at all. */
|
|
touch-action: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
|
|
/* ---- shared key look ---- */
|
|
|
|
.key,
|
|
.pad,
|
|
.side,
|
|
.fx,
|
|
.knob,
|
|
.valueKnob {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
text-align: center;
|
|
border: var(--outline);
|
|
background: var(--key);
|
|
color: var(--ink);
|
|
/* 7px with no tracking: a left-block key is 35.9px wide and only 27.9px of
|
|
that is usable, so 8px made SAMPLE sit flush against the border. */
|
|
font-size: 7px;
|
|
line-height: 1.05;
|
|
letter-spacing: 0;
|
|
padding: 2px 1px;
|
|
border-radius: 3px;
|
|
hyphens: manual;
|
|
/* Lit keys show the entry's short label, which comes from the data and can be
|
|
one long unbreakable word — SHIFT + LOOP is PINGPONG. Soft hyphens only
|
|
cover the printed labels, so allow a break anywhere and clip what is left,
|
|
rather than letting it spill outside the key. */
|
|
overflow-wrap: anywhere;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.badge {
|
|
position: absolute;
|
|
top: 1px;
|
|
right: 2px;
|
|
font-family: var(--mono);
|
|
font-size: 6px;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* ---- knobs ---- */
|
|
|
|
.knobs {
|
|
height: 36px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.knob {
|
|
width: 34px;
|
|
height: 34px;
|
|
flex: 0 0 auto;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
}
|
|
|
|
/* ---- fx and the screen ---- */
|
|
|
|
.screenRow {
|
|
height: 124px;
|
|
margin-top: 8px;
|
|
display: grid;
|
|
grid-template-columns: 62px minmax(0, 1fr) 62px;
|
|
gap: 8px;
|
|
}
|
|
|
|
.fxCol {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.fx {
|
|
flex: 1;
|
|
background: var(--pad);
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
font-size: 7.5px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.screenWrap {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.circle {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 0;
|
|
width: 124px;
|
|
height: 124px;
|
|
margin-left: -62px;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
border: var(--outline);
|
|
}
|
|
|
|
.lcd {
|
|
position: relative;
|
|
width: 168px;
|
|
height: 84px;
|
|
background: var(--lcd);
|
|
border: var(--outline);
|
|
color: #111111;
|
|
padding: 4px 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.lcdRow {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 3px;
|
|
font-family: var(--mono);
|
|
font-size: 6.5px;
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
.lcdMain {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
text-align: center;
|
|
}
|
|
|
|
.chip {
|
|
background: #111111;
|
|
color: var(--lcd);
|
|
padding: 1px 4px;
|
|
}
|
|
|
|
/* ---- control block ---- */
|
|
|
|
.ctl {
|
|
height: 134px;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 7px;
|
|
}
|
|
|
|
.group {
|
|
height: 40px;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 3fr) minmax(0, 5fr);
|
|
gap: 10px;
|
|
}
|
|
|
|
.col {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.caption {
|
|
height: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-family: var(--mono);
|
|
font-size: 6px;
|
|
letter-spacing: 0.11em;
|
|
color: var(--muted);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.rule {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--rule);
|
|
}
|
|
|
|
.btnRow {
|
|
height: 32px;
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
|
|
.key {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* SHIFT is the longest label in the six-button bank row, as on the hardware. */
|
|
.btnRow .key:last-child:nth-child(6) {
|
|
flex: 1.25;
|
|
}
|
|
|
|
.valueKnob {
|
|
width: 30px;
|
|
flex: 0 0 auto;
|
|
border-radius: 50%;
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
font-size: 6.5px;
|
|
}
|
|
|
|
/* ---- pads ---- */
|
|
|
|
.pads {
|
|
height: 187px;
|
|
margin-top: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
}
|
|
|
|
.padRow {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr)) 0.9fr;
|
|
gap: 5px;
|
|
}
|
|
|
|
.pad,
|
|
.side {
|
|
background: var(--pad);
|
|
color: var(--pad-ink);
|
|
font-size: 9px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.side {
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* The pad's number, as printed on the hardware. */
|
|
.num {
|
|
font-family: var(--mono);
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
color: var(--muted);
|
|
}
|
|
|
|
/* ---- state: LAST in the file, so it wins over .pad, .fx, .side and .knob ---- */
|
|
|
|
/* Lit is an outline, not a fill. The SHIFT layer lights 35 of 54 controls, and
|
|
35 solid orange keys is a wall; solid is reserved for the selected one. */
|
|
.lit {
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.sel {
|
|
background: var(--accent);
|
|
border-color: var(--accent);
|
|
color: var(--on-accent);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.held {
|
|
background: var(--ink);
|
|
color: var(--page);
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* A round key cannot hold a lit function's label at full size: SHIFT + VALUE is
|
|
MARK FN, which spilled outside the circle. Shrink it when lit. */
|
|
.knob.lit,
|
|
.knob.sel,
|
|
.valueKnob.lit,
|
|
.valueKnob.sel {
|
|
font-size: 6px;
|
|
}
|
|
|
|
/* Pressed, whether or not it is lit. */
|
|
.down {
|
|
transform: scale(0.95);
|
|
}
|