Core ride logic, FTMS client, FIT encoder and probe CLI

Adds backing state for Resistance and Erg control modes, which had no
value to hold and so could never satisfy FR-4.3/FR-4.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 13:34:27 +02:00
co-authored by Claude Opus 5
parent 3e106de2c5
commit 7c17ca6158
61 changed files with 20933 additions and 55 deletions
+230
View File
@@ -0,0 +1,230 @@
/*
* Dark theme for a screen someone stares at while suffering (FR-9.12).
*
* Rules this stylesheet follows:
* - Near-black ground, bright data. Contrast comes from the numbers, not
* from boxes.
* - No borders unless they carry meaning. Structure comes from space.
* - Numbers are tabular so they do not jitter as digits change.
* - Type scales with the viewport: the primary readouts must be legible from
* a riding position about a metre away (FR-9.5).
*/
:root {
--bg: #05070a;
--bg-lift: #0b0f15;
--hairline: #161c25;
--ink: #f4f7fb;
--ink-soft: #9fb0c2;
--ink-dim: #5d6c7d;
--ink-faint: #313d4a;
--route: #45d0ff;
--route-deep: #123a4d;
--climb: #ff9a3c;
--climb-deep: #3d2712;
--ok: #35d9a0;
--warn: #ffcf4a;
--bad: #ff5a52;
--power: #dfe8f3;
--power-raw: #3f4d5d;
--gap: clamp(0.75rem, 1.4vw, 1.5rem);
--edge: clamp(1rem, 2.4vw, 2.75rem);
color-scheme: dark;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
margin: 0;
overflow: hidden;
}
body {
background: var(--bg);
color: var(--ink);
font-family:
'Inter var', 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', system-ui, sans-serif;
font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum' 1, 'cv01' 1;
letter-spacing: -0.01em;
user-select: none;
-webkit-user-select: none;
}
#app {
height: 100%;
}
button {
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
padding: 0;
}
/* ---------- shared primitives ---------------------------------------- */
.label {
font-size: clamp(0.6rem, 0.72vw, 0.78rem);
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--ink-dim);
white-space: nowrap;
}
.hairline {
border-top: 1px solid var(--hairline);
}
.chip {
display: inline-flex;
align-items: center;
gap: 0.45em;
padding: 0.3em 0.7em;
border-radius: 999px;
background: var(--bg-lift);
font-size: 0.8rem;
font-weight: 600;
letter-spacing: 0.02em;
color: var(--ink-soft);
white-space: nowrap;
}
.dot {
width: 0.5em;
height: 0.5em;
border-radius: 50%;
background: currentColor;
flex: none;
}
.tone-ok {
color: var(--ok);
}
.tone-warn {
color: var(--warn);
}
.tone-bad {
color: var(--bad);
}
.tone-idle {
color: var(--ink-dim);
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
padding: 0.62em 1.05em;
border-radius: 0.5rem;
background: var(--bg-lift);
color: var(--ink-soft);
font-size: 0.92rem;
font-weight: 600;
transition:
background 120ms ease,
color 120ms ease,
transform 90ms ease;
}
.btn:hover {
background: #131a24;
color: var(--ink);
}
.btn:active {
transform: translateY(1px);
}
.btn.primary {
background: var(--route);
color: #04121a;
}
.btn.primary:hover {
background: #6cdcff;
color: #04121a;
}
.btn.ghost {
background: transparent;
}
.btn.ghost:hover {
background: var(--bg-lift);
}
.btn.danger:hover {
background: #2a1113;
color: var(--bad);
}
.btn[disabled] {
opacity: 0.35;
pointer-events: none;
}
.kbd {
display: inline-block;
min-width: 1.5em;
padding: 0.1em 0.35em;
border-radius: 0.28em;
background: #10161f;
color: var(--ink-dim);
font-size: 0.68rem;
font-weight: 700;
text-align: center;
letter-spacing: 0.02em;
}
/* ---------- uPlot, restyled for the dark theme ------------------------ */
.uplot,
.u-wrap {
width: 100% !important;
}
.u-title,
.u-legend {
display: none;
}
.u-axis {
color: var(--ink-dim);
}
.u-select {
background: rgba(69, 208, 255, 0.12);
}
.u-cursor-x,
.u-cursor-y {
border-color: var(--ink-faint) !important;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: #1b232e;
border-radius: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}