:root {
  --bg: #f6f7f9;
  --panel: #ffffff;
  --text: #16191d;
  --muted: #666e79;
  --border: #e0e4e9;
  --accent: #2f6fed;
  --accent-soft: #eaf1fe;
  --grid: #e6e9ee;
  --needle: #d64545;
  --radius: 12px;

  /* The dials are a physical instrument, not a chart: they keep this palette in
     both color schemes, so they are deliberately absent from the dark-mode
     block below. Don't "fix" that omission — it is what makes them fixed. */
  --dial-face: #1a1d21;
  --dial-face-edge: #050607;
  --dial-bezel: #3d434a;
  --dial-ink: #f2f4f6;
  --dial-ink-dim: #aeb4bb;
  --dial-red: #d3232b;
  --dial-hub: #23262a;
  --dial-hub-edge: #40454b;
  --dial-amber: #f5a11d; /* classic BMW instrument backlighting */
  /* Setup B, everywhere it appears: needle, redline, trace, tab, readout. One
     fixed hue so the dial keeps its palette and the chart still matches it. */
  --dial-blue: #3b9ae1;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #0f1216;
    --panel: #171b21;
    --text: #e7eaee;
    --muted: #9aa4b1;
    --border: #2a313a;
    --accent: #6f9bff;
    --accent-soft: #1c2534;
    --grid: #262d36;
    --needle: #ff6b6b;
  }
}

* { box-sizing: border-box; }

/* Must beat the `display` rules below (e.g. .loading{display:grid}), which
   would otherwise override the UA stylesheet's [hidden]{display:none}. */
[hidden] { display: none !important; }

body {
  margin: 0;
  font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
  background: var(--bg);
  color: var(--text);
  line-height: 1.5;
}

/* ------------------------------- loading -------------------------------- */
.loading {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 1rem;
  min-height: 100vh;
  color: var(--muted);
}

.shifter { width: 9rem; height: auto; }

.shifter-gate {
  fill: none;
  stroke: var(--border);
  stroke-width: 6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.shifter-labels text {
  fill: var(--muted);
  font-size: 11px;
  font-weight: 600;
  text-anchor: middle;
}

/* Ringed in the page colour so it reads as sitting *in* the gate, not on it. */
.shifter-knob {
  fill: var(--dial-amber);
  stroke: var(--bg);
  stroke-width: 2;
  animation: shift 6.5s ease-in-out infinite alternate;
}

/*
 * The knob's path through a six-speed gate. Each gear is two keyframes — arrive,
 * then dwell — and every move between them goes through the gate at y=50, because
 * that is the only way across a real H-pattern.
 *
 * `alternate` runs it backwards on the return leg, which traces a plausible
 * downshift rather than teleporting from 6th back to 1st.
 */
@keyframes shift {
  0%,   6%  { transform: translate(20px, 15px); }  /* 1st */
  12%       { transform: translate(20px, 50px); }
  18%, 24%  { transform: translate(20px, 85px); }  /* 2nd */
  30%       { transform: translate(20px, 50px); }
  34%       { transform: translate(60px, 50px); }  /* across the gate */
  40%, 46%  { transform: translate(60px, 15px); }  /* 3rd */
  52%       { transform: translate(60px, 50px); }
  58%, 64%  { transform: translate(60px, 85px); }  /* 4th */
  70%       { transform: translate(60px, 50px); }
  74%       { transform: translate(100px, 50px); }
  80%, 86%  { transform: translate(100px, 15px); } /* 5th */
  92%       { transform: translate(100px, 50px); }
  97%, 100% { transform: translate(100px, 85px); } /* 6th */
}

@media (prefers-reduced-motion: reduce) {
  /* Parked in neutral. */
  .shifter-knob { animation: none; transform: translate(60px, 50px); }
}

/* -------------------------------- layout -------------------------------- */
main {
  max-width: 76rem;
  margin: 0 auto;
  padding: 2rem 1rem 4rem;
}

.page-header h1 {
  margin: 0;
  font-size: clamp(1.6rem, 3vw, 2.2rem);
  letter-spacing: -0.02em;
}

.subtitle {
  margin: 0.25rem 0 2rem;
  color: var(--muted);
}

.layout {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}

@media (max-width: 900px) {
  .layout { grid-template-columns: 1fr; }
}

.panel {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 1.25rem;
}

.panel h2 {
  margin: 0 0 1rem;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
}

.panel h3 {
  margin: 1.75rem 0 0.75rem;
  font-size: 0.95rem;
}

.results > .viz:first-of-type { margin-top: 1rem; }

/* Each chart collapses on its own. <details> gives keyboard support and the
   open/closed state for free, so there is no script behind this. */
.viz { margin-top: 1.75rem; }

.viz > summary {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0.75rem;
  font-size: 0.95rem;
  font-weight: 600;
  cursor: pointer;
  list-style: none; /* Firefox */
}

.viz > summary::-webkit-details-marker { display: none; }

/* Sized off the summary's own text (1em), not a fixed rem, so the caret and the
   heading stay matched. The fixed box keeps the label still while it rotates. */
.viz > summary::before {
  content: "▾";
  display: inline-block;
  width: 1em;
  font-size: 1em;
  line-height: 1;
  text-align: center;
  color: var(--muted);
  transition: transform 0.15s ease;
}

.viz:not([open]) > summary::before { transform: rotate(-90deg); }

.viz > summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .viz > summary::before { transition: none; }
}

/* -------------------------------- inputs -------------------------------- */
.field {
  display: flex;
  flex-direction: column;
  gap: 0.3rem;
  margin-bottom: 0.9rem;
}

.field label {
  font-size: 0.85rem;
  color: var(--muted);
}

.unit { opacity: 0.75; }

input[type="number"],
input[type="text"],
select {
  width: 100%;
  padding: 0.5rem 0.6rem;
  font: inherit;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}

/* Derived from the tire size, so it reads as output rather than a field. */
input[readonly] {
  color: var(--muted);
  border-style: dashed;
  cursor: default;
}

/* 225 / 45 R 17 — inputs sized to content, separators between them. */
.tire-size {
  display: grid;
  grid-template-columns: 1fr auto 1fr auto 1fr;
  align-items: center;
  gap: 0.35rem;
}

.tire-size span {
  color: var(--muted);
  font-size: 0.85rem;
}

input:focus-visible,
select:focus-visible,
button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

input[type="range"] { width: 100%; accent-color: var(--dial-amber); }

output {
  font-variant-numeric: tabular-nums;
  color: var(--text);
  font-weight: 600;
}

fieldset.units {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 0.6rem 0.75rem;
  margin: 0 0 1rem;
}

fieldset.units legend {
  font-size: 0.8rem;
  color: var(--muted);
  padding: 0 0.3rem;
}

fieldset.units label {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  font-size: 0.9rem;
}

/* -------------------------------- setups -------------------------------- */
.compare-toggle {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 1rem;
  font-size: 0.9rem;
  color: var(--muted);
}

.tabs {
  display: flex;
  gap: 0.25rem;
  margin-bottom: 1rem;
  border-bottom: 1px solid var(--border);
}

.tabs [role="tab"] {
  flex: 1;
  padding: 0.5rem 0.6rem;
  margin-bottom: -1px; /* the selected tab's underline replaces the tablist rule */
  font: inherit;
  font-size: 0.85rem;
  color: var(--muted);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

/* Each tab is underlined in its setup's color, tying the form to its trace. */
.tabs [role="tab"][aria-selected="true"] { color: var(--text); }
.tabs [data-setup="a"][aria-selected="true"] { border-bottom-color: var(--dial-amber); }
.tabs [data-setup="b"][aria-selected="true"] { border-bottom-color: var(--dial-blue); }

/* -------------------------------- gears --------------------------------- */
.gears-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.gears-header h3 { margin: 1.25rem 0 0.75rem; }

.gear-buttons { display: flex; gap: 0.35rem; }

.gear-buttons button {
  width: 1.9rem;
  height: 1.9rem;
  font-size: 1.1rem;
  line-height: 1;
  color: var(--text);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  cursor: pointer;
}

.gear-buttons button:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

.gear-buttons button:disabled { opacity: 0.4; cursor: not-allowed; }

/* Names the numbers below it, and reads "Custom" the moment they are edited. */
.preset { margin-bottom: 0.7rem; font-size: 0.9rem; }

.gear-row {
  display: grid;
  grid-template-columns: 3.4rem 1fr;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.45rem;
}

.gear-row span {
  font-size: 0.85rem;
  color: var(--muted);
}

/* ---------------------------- engine curve ------------------------------ */
.curve-head,
.curve-row {
  display: grid;
  grid-template-columns: 1fr 1fr 4.2rem;
  align-items: center;
  gap: 0.4rem;
}

.curve-head {
  margin-bottom: 0.35rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

.curve-head span:last-child,
.curve-power { text-align: right; }

.curve-row { margin-bottom: 0.45rem; }

/* Derived, not entered — same dashed-and-muted treatment as the tire diameter. */
.curve-power {
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
}

.hint {
  margin: -0.4rem 0 0.75rem;
  font-size: 0.8rem;
  color: var(--muted);
}

/* -------------------------------- gauges -------------------------------- */
.gauges {
  position: relative;
  container-type: inline-size;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0.5rem;
}

/* Lifted out of the grid so it costs the dials no width. The dials are circles
   in square boxes, which leaves a wedge of empty space between their lower
   edges — the readout drops into it, where the cluster puts its warning lights. */
.rpm-readout {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  display: grid;
  justify-items: center;
  gap: 0.1rem;
  padding: 0.4rem 0.75rem;
  background: var(--dial-face-edge);
  border: 1px solid var(--dial-bezel);
  border-radius: 6px;
}

/* The readout is a fixed size but the dials scale with the panel, so past a point
   the wedge between them is too small to hold it and it drops below instead.
   Keyed off the container, not the viewport: the panel is narrower at 1024px
   (two columns) than at 900px (one), so viewport width says nothing useful here. */
@container (max-width: 700px) {
  .rpm-readout {
    position: static;
    transform: none;
    grid-row: 2;
    grid-column: 1 / -1;
    justify-self: center;
    margin-top: 0.75rem;
  }
}

/* Comparing adds a second row. The taller box does technically clear the dials at
   the widest layout, but by ~3px — less than one font metric's worth — so it drops
   below at every width rather than pretending to fit. */
.gauges[data-compare] .rpm-readout {
  position: static;
  transform: none;
  grid-row: 2;
  grid-column: 1 / -1;
  justify-self: center;
  margin-top: 0.75rem;
}

.rpm-row {
  display: flex;
  align-items: baseline;
  gap: 0.3rem;
}

/* The digits are tabular, so reserving five slots keeps the box — and the dials
   either side of it — a constant width as the rpm climbs from 0 to five figures. */
.rpm-row output {
  min-width: 5ch;
  text-align: center;
  color: var(--dial-amber);
  font-size: 1.15rem;
  font-weight: 650;
  line-height: 1.1;
}

.rpm-tag {
  color: var(--dial-amber);
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.08em;
}

.rpm-row[data-setup="b"] output,
.rpm-row[data-setup="b"] .rpm-tag { color: var(--dial-blue); }

/* One slider drives both setups, so it can be pushed past a setup's own top
   speed — which would need more rpm than that engine has. Say so. */
.rpm-row[data-over] output { color: var(--dial-red); }

.rpm-unit {
  color: var(--dial-ink-dim);
  font-size: 0.6rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.gauge { width: 100%; height: auto; }

.gauge-face-center { stop-color: var(--dial-face); }
.gauge-face-edge { stop-color: var(--dial-face-edge); }
.gauge-bezel { fill: none; stroke: var(--dial-bezel); stroke-width: 2.5; }
.gauge-tick-major { stroke: var(--dial-ink); stroke-width: 3.2; stroke-linecap: butt; }
.gauge-tick-minor { stroke: var(--dial-ink-dim); stroke-width: 1.8; }
/* Stroke width spans the tick ring, so the arc reads as a band on the dial edge. */
.gauge-redline { fill: none; stroke: var(--dial-red); stroke-width: 8; stroke-linecap: butt; }
/* Numerals are sized to the widest label on the dial (see `numWidth` in app.js):
   a tach counting 0-7 can be big, a speedo counting to 300 cannot. */
.gauge-tick-label {
  fill: var(--dial-ink);
  font-weight: 600;
  text-anchor: middle;
  dominant-baseline: central;
}

.gauge[data-num-width="narrow"] .gauge-tick-label { font-size: 17px; }
.gauge[data-num-width="medium"] .gauge-tick-label { font-size: 14px; }
.gauge[data-num-width="wide"] .gauge-tick-label { font-size: 12px; }
.gauge-needle { fill: var(--dial-ink); }

/* Comparing puts a needle per setup on the one tach face. They take the setup's
   colour — the same amber and blue as its number in the readout and its trace on
   the charts — so which needle is which needs no legend. A lone needle stays the
   instrument's own white; the speedometer's always is, since road speed is shared
   and its second needle would sit exactly on the first. */
.gauges[data-compare] .gauge-needle-a { fill: var(--dial-amber); }
.gauge-needle-b { fill: var(--dial-blue); }

.gauge-hub { fill: var(--dial-hub); stroke: var(--dial-hub-edge); stroke-width: 1.5; }
.gauge-legend { fill: var(--dial-ink); font-size: 9px; text-anchor: middle; letter-spacing: 0.04em; }

.controls {
  display: grid;
  grid-template-columns: 1fr 7rem;
  gap: 1rem;
  align-items: end;
  margin-top: 0.75rem;
}

/* The gear is derived from the shift schedule rather than chosen, so it gets
   the same dashed treatment as the derived tire diameter — but keeps the box
   the old <select> had, so the control row stays aligned. */
.gear-readout {
  display: block;
  padding: 0.5rem 0.6rem;
  font: inherit;
  font-variant-numeric: tabular-nums;
  text-align: center;
  border: 1px dashed var(--border);
  border-radius: 8px;
}

/* ----------------------------- gear spread ------------------------------ */
.spread { margin-bottom: 0.9rem; }

.spread-caption {
  margin: 0 0 0.45rem;
  font-size: 0.8rem;
  color: var(--muted);
}

/* Inset to the chart's plot box (see CHART in app.js), so a gear's band sits
   directly above the stretch of the speed axis it covers. */
.spread-row {
  position: relative;
  width: var(--plot-width, 100%);
  margin: 0 0 0.3rem var(--plot-left, 0);
}

/* Lives in the chart's left margin, beside the bar rather than shortening it. */
.spread-tag {
  position: absolute;
  right: 100%;
  top: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  padding-right: 0.4rem;
  font-size: 0.7rem;
  font-weight: 700;
}

.spread-tag-a { color: var(--dial-amber); }
.spread-tag-b { color: var(--dial-blue); }

.spread-bar { display: flex; height: 1.5rem; }

/* Near-black ink: the bands are amber or blue in both colour schemes, so the
   label cannot take its colour from the theme. */
.spread-seg {
  display: flex;
  align-items: center;
  justify-content: center;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  border-right: 2px solid var(--panel);
  font-size: 0.72rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  color: var(--dial-face-edge);
}

.spread-seg:first-child { border-radius: 3px 0 0 3px; }
.spread-seg:last-child { border-right: none; border-radius: 0 3px 3px 0; }

/* -------------------------------- chart --------------------------------- */
.chart {
  width: 100%;
  height: auto;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
}

.chart-grid { stroke: var(--grid); stroke-width: 1; }
.chart-axis { stroke: var(--muted); stroke-width: 1.2; opacity: 0.7; }
.chart-label { fill: var(--muted); font-size: 11px; }
.chart-axis-title { fill: var(--muted); font-size: 11px; letter-spacing: 0.04em; }
/* The gear rays are context, not the subject: what each gear *could* do at any
   rpm. They recede to gray so the shift trace drawn over them reads first. */
.chart-line {
  fill: none;
  stroke: var(--muted);
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
  opacity: 0.4;
}

.chart-gear-label { fill: var(--muted); font-size: 11px; font-weight: 600; }
.chart-marker { fill: var(--dial-amber); stroke: var(--text); stroke-width: 1.5; }

/* The actual acceleration run — up each gear, drop at every shift. This is the
   subject of the plot, so it takes the amber. */
.chart-trace {
  fill: none;
  stroke: var(--dial-amber);
  stroke-width: 2.5;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* Setup B: a different hue *and* a dash, so the two runs stay distinguishable
   where they overlap and without relying on colour alone. */
.chart-trace-b { stroke: var(--dial-blue); stroke-dasharray: 7 5; }
.chart-marker-b { fill: var(--dial-blue); }

.chart-shift { fill: var(--text); opacity: 0.65; }

/* ------------------------- tractive effort chart ------------------------- */
/* Here the gear curves *are* the subject, so they keep the setup's colour
   rather than receding to gray the way they do on the RPM chart. */
.chart-effort {
  fill: none;
  stroke: var(--dial-amber);
  stroke-width: 1.6;
  stroke-linejoin: round;
  opacity: 0.9;
}

.chart-effort-b { stroke: var(--dial-blue); stroke-dasharray: 6 4; }
.chart-effort-label { fill: var(--dial-amber); font-size: 10px; font-weight: 600; }
.chart-effort-label-b { fill: var(--dial-blue); }

/* The crossings. Ringed in the panel colour so they read on top of the curves. */
.chart-cross { fill: var(--text); stroke: var(--panel); stroke-width: 1.5; }

.chart-torque { fill: none; stroke: var(--dial-amber); stroke-width: 2; }
.chart-torque-b { stroke: var(--dial-blue); }
/* Dashed because it is derived from the solid line beside it, not measured. */
.chart-power {
  fill: none;
  stroke: var(--dial-amber);
  stroke-width: 2;
  stroke-dasharray: 5 4;
  opacity: 0.75;
}

.chart-power-b { stroke: var(--dial-blue); }

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin-top: 0.5rem;
  font-size: 0.8rem;
  color: var(--muted);
}

.legend span { display: inline-flex; align-items: center; gap: 0.35rem; }

.legend .swatch {
  width: 0.75rem;
  height: 0.2rem;
  border-radius: 2px;
}

/* Mirror the traces so each legend entry is recognisable. */
.legend .swatch-trace { background: var(--dial-amber); }
.legend .swatch-trace-b { background: var(--dial-blue); }
.legend .swatch-effort-a { background: var(--dial-amber); }
.legend .swatch-effort-b { background: var(--dial-blue); }

/* Round, to match the crossover markers rather than the line swatches. */
.legend .swatch-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--text);
}

.legend-note { font-style: italic; opacity: 0.8; }

/* -------------------------------- table --------------------------------- */
.table-wrap { overflow-x: auto; }

table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  font-size: 0.9rem;
}

th, td {
  text-align: right;
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}

/* Only the key column is left-aligned — the leading "Gear"/"Shift" header and the
   row headers under it. Not `:first-child`: while comparing, the sub-header row
   starts with an "A", which is a value column and must stay right-aligned with
   the numbers it labels. */
thead tr:first-child > th:first-child,
tbody th[scope="row"] { text-align: left; }

th {
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  font-weight: 600;
}

/* Row headers are the gear or shift, not a heading: keep the body's weight. */
tbody th {
  font-weight: 400;
  text-transform: none;
  color: var(--text);
}

/* A group header spans its setups' sub-columns, so it centres over them. */
thead th[colspan] { text-align: center; border-bottom: none; }

/* The setup letters, tinted to match the traces on the charts. */
thead th.sub { font-size: 0.7rem; letter-spacing: 0.08em; }
thead th.sub-a { color: var(--dial-amber); }
thead th.sub-b { color: var(--dial-blue); }

/* Rules down the column-group boundaries, so the eye does not slide from A's
   top speed into B's overall ratio. */
.group-start:not(:first-child) { border-left: 1px solid var(--border); }

tbody tr:last-child td,
tbody tr:last-child th { border-bottom: none; }

/* Which gear you are in. One row per gear now holds both setups, so comparing
   highlights the cells of whichever setup is in that gear — each setup can be in
   a different one. Without comparison the whole row lights up, as it always has. */
tbody tr.is-current { background: var(--accent-soft); }
tbody td.is-current { background: var(--accent-soft); }
