/* ============================================================================
   AmSpecs - Amflow geometry configurator
   ----------------------------------------------------------------------------
   Two self-hosted variable faces, subset to the glyphs this page uses and
   instanced to 400-700, which is why the pair costs 21 KB. There are no external
   requests on this page at all, so both files are relative and the @font-face
   src has no network fallback to fail over to.

   Space Grotesk carries everything a human reads as language, and its geometric
   skeleton is a deliberate nod to the brand's own Poppins without copying it.
   JetBrains Mono carries everything a machine produced: the tracked-out labels,
   the deltas, the URL, the drawing annotations. That split is the whole idea of
   the typography here - you can tell an input from an output by its shape.
   ========================================================================== */

@font-face{
  font-family:"Space Grotesk";
  src:url("SpaceGrotesk.woff2?v=20260920w") format("woff2");
  font-weight:400 700;        /* variable range, not a static cut */
  font-style:normal;
  font-display:swap;
}
@font-face{
  font-family:"JetBrains Mono";
  src:url("JetBrainsMono.woff2?v=20260920w") format("woff2");
  font-weight:400 700;
  font-style:normal;
  font-display:swap;
}

:root{
  /* ---- ink ----
     Near-black rather than pure black so the panel steps above it have somewhere
     to go, and neutral rather than blue so the light studio plate does not pick
     up a cast next to it. */
  --bg:#07090c;
  --panel:#0f1318;
  --panel-2:#141920;
  --panel-3:#1b212a;
  --line:#242c37;
  --line-soft:#191f28;
  --fg:#f1f4f8;
  --fg-dim:#9aa5b4;
  /* Every tracked-out label, table header and caption on the page is this grey,
     and all of them are small text, so it is pinned to WCAG AA against the
     LIGHTEST surface it ever sits on rather than the darkest: 4.83:1 on --panel,
     5.17:1 on --bg. The previous value cleared neither. */
  --fg-faint:#78838f;

  /* ---- accent ----
     Acid means one thing on this page: this is not how the bike ships. The
     selected bike, the changed rows in the table and the changed measurements in
     the diagram, plus the bottom bracket, which is the origin every one of those
     numbers is measured from. Everything else that needs emphasis uses white.
     That restraint is what lets a colour carry a meaning at all. */
  --acid:#d4ff45;
  --acid-ink:#0d1203;
  --acid-dim:#9ab53a;

  /* ---- deltas ----
     The single most-read pair on the page, so it is chosen rather than picked.
     Azure/rose survives all three dichromacies: simulated through Vienot, the
     worst-case separation of the pair is dE 35.5 (protanopia), against a rough
     threshold of 20 for confident discrimination, and both clear WCAG AA on the
     panel (9.7:1 and 6.9:1). Rose sits at hue 345 degrees, which is 320 degrees
     off the orange this replaces. Colour is still never the only channel: every
     delta also carries a sign and a CSS-drawn caret. */
  --up:#5cc8ff;
  --down:#fd6f93;
  --ok:#8ce8a0;

  /* ---- studio plate ----
     Exactly the flat background both bike JPEGs are cut out on, so the crop
     edge is invisible and the photo reads as a plate rather than a pasted box. */
  --plate:#f5f5f7;
  --plate-ink:#4e555f;
  /* The manufacturer's wordmark sits on the plate and is the one thing there
     that must not read as a watermark, so it gets its own ink: 15.9:1 on the
     plate, against the caption grey's 6.9:1. */
  --plate-brand:#15191e;

  /* ---- drawing ink ---- */
  --dim:#7c8898;

  --radius:12px;
  --radius-sm:8px;
  /* One gutter for the whole page: the topbar, the masthead grid and .wrap are
     separate containers, and without a shared token the wordmark, the h1 and the
     first control card drift off each other's left edge at a breakpoint. */
  --gut:22px;
  --maxw:1120px;
  --ctrl-h:40px;

  --mono:"JetBrains Mono",ui-monospace,"SF Mono",SFMono-Regular,"Cascadia Mono","Roboto Mono",Menlo,Consolas,monospace;
  --sans:"Space Grotesk",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}

*{box-sizing:border-box;margin:0;padding:0}
html{-webkit-text-size-adjust:100%}
body{
  background:var(--bg);color:var(--fg);font-family:var(--sans);
  font-size:16px;line-height:1.5;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  -webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;
}
.wrap{max-width:var(--maxw);margin:0 auto;padding:0 var(--gut) 90px}

/* One label style, used by every control label, every card heading and every
   eyebrow on the page. Declared once so the instrument voice cannot drift. */
.eyebrow,.bikerow-lab,.ctrl > label,.fp > label,.acrow label,
.stat .k,.tablecard h2,.diagcard h2,.revhead h2,.feel .k,thead th,tbody tr.sep th,
.ctrl-head,.revblock > .k{
  font-family:var(--mono);
  font-weight:600;
  letter-spacing:.17em;
  text-transform:uppercase;
}

/* ---------- masthead + hero, as one grid ----------
   Three stacked full-bleed strips is what made the top of this page read as
   three unrelated things. One grid, one gutter, one baseline. Two children at
   every width: the copy and the plate. The bike switch used to be a third, in
   the left column; it now lives in .wrap under the plate with the rest of the
   buttons, so nothing here has to reserve room for it. */
.masthead{padding:46px 0 52px}
.masthead-in{
  max-width:var(--maxw);margin:0 auto;padding:0 var(--gut);
  display:grid;gap:34px;align-items:center;
}
@media (min-width:900px){
  .masthead{padding:64px 0 72px}
  .masthead-in{grid-template-columns:minmax(0,1fr) minmax(0,.9fr);gap:60px}
}

.mh-copy{min-width:0}
.eyebrow{
  font-size:13.5px;line-height:1.25;color:var(--acid-dim);
  letter-spacing:.13em;
  margin-bottom:18px;
}
.eyebrow span{color:var(--fg-faint)}

/* Scale carries the hierarchy, colour does not: the h1 is one weight, one
   colour family and very tight tracking. The second clause steps down in tone
   rather than jumping to the accent, which is the difference between a headline
   and a highlighter. */
.masthead h1{
  font-size:clamp(31px,4.9vw,48px);line-height:1.02;letter-spacing:-.038em;
  font-weight:700;text-wrap:balance;
}
.masthead h1 span{color:var(--fg-faint)}

#masthead-blurb{
  margin-top:18px;color:var(--fg-dim);max-width:46ch;
  font-size:15.5px;line-height:1.6;text-wrap:pretty;
}

/* Geometry is .moderow's, to the pixel, because the two rows are now adjacent
   siblings at the top of .wrap and any difference between them would read as a
   misalignment rather than as a distinction. The 24px margin-top this carried
   in the masthead is gone with the position that earned it: .masthead's own
   bottom padding is the seam above, exactly as it was for #mode-intro when that
   was .wrap's first child, and the 18px below is the gap .moderow already
   leaves before the thing under it. Do not reintroduce a margin here - it would
   collapse against nothing above and double the seam on the phone. */
.bikerow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:2px 0 18px}
.bikerow-lab{flex:0 0 auto;font-size:10.5px;line-height:1;color:var(--fg-faint)}
/* The track hugs its two labels rather than stretching: two half-width buttons
   for a two-word choice reads as a nav bar, which is what this must not be. */
.seg-bike{
  flex:0 1 auto;min-width:0;gap:3px;padding:3px;
  background:var(--panel);border:1px solid var(--line-soft);border-radius:10px;
}
/* Doubled class: the generic `.seg button` rules below sit at the same
   specificity, so a single class here loses to whichever came last. */
.seg.seg-bike button{
  min-height:34px;padding:0 26px;
  background:transparent;border-color:transparent;color:var(--fg-dim);
  border-radius:7px;font-size:13.5px;letter-spacing:-.005em;
}
.seg.seg-bike button:hover{background:var(--panel-2);border-color:transparent;color:var(--fg)}
/* The bike selector takes the accent unconditionally, and so does the mode
   strip under it. Those two are the page's root switches - they choose WHAT is
   being looked at, not what it is set to - and neither can ever be "off stock",
   so neither has a white state to fall back to. Whichever bike is pressed IS
   the stock geometry everything below is measured against, and whichever mode
   is pressed is the direction the tool is being run in.

   Everything below the mode strip is an adjuster: frame size, fork, travel,
   rocker, chainstay, wheel, link. Those select in WHITE and only turn acid once
   they leave the value the bike ships with, which is the rule the rest of the
   page runs on - acid there means "this is not how the bike ships".

   So acid carries two jobs, separated by rank rather than by hue: on the two
   root switches it means "this is the current subject", below them it means
   "changed from stock". Adding a third accented control below this line would
   collapse that distinction - don't. */
.seg.seg-bike button[aria-pressed="true"],
.seg.seg-bike button[aria-pressed="true"]:hover{
  background:var(--acid);border-color:var(--acid);color:var(--acid-ink);
}

/* ---------- hero plate ----------
   A light rectangle held inside a black layout, not dissolved into it. The
   background is the exact flat tone the JPEGs are cut out on, so there is no
   visible crop edge and no scrim is needed to hide one.

   4:3 shows the middle 75% of the square source. The bike occupies the middle
   50%, centred at ~52.8% of frame height, so at object-position 57% it clears
   by 13.5% above and 11.5% below. 17:10 on a phone shows 58.8% and clears by
   about 4.5% each way, which is why the ratio does not go tighter than that. */
.hero{
  position:relative;background:var(--plate);
  border-radius:var(--radius);overflow:hidden;
}
.hero img{
  display:block;width:100%;height:auto;
  aspect-ratio:17/10;object-fit:cover;object-position:50% 57%;
}
@media (min-width:900px){ .hero img{aspect-ratio:4/3} }
/* Wordmark then model, on one line. Flex rather than inline flow because the
   space between them in the markup is there for the spoken name, not for the
   layout: a flex container ignores a whitespace-only child, so the visible
   distance is this gap and nothing else. */
.hero-tag{
  position:absolute;top:0;left:0;padding:14px 16px;
  display:flex;align-items:center;gap:10px;
  font:600 10px/1 var(--mono);letter-spacing:.2em;text-transform:uppercase;
  color:var(--plate-ink);
}
/* Sized to a badge on the frame rather than to the caption: at 92px the mark
   is legible at arm's length on a phone and still smaller than the bike's own
   down-tube decal, so it identifies the machine without claiming the page. */
.amflowmark{
  width:92px;height:auto;flex:0 0 auto;display:block;
  color:var(--plate-brand);
}

/* ---------- controls ----------
   align-items:start is load-bearing: the default stretch made every card in a
   row as tall as the tallest, leaving a void under the short ones. */
.controls{
  display:grid;gap:14px;padding:4px 0 8px;align-items:start;
  grid-template-columns:repeat(auto-fit,minmax(248px,1fr));
}
.ctrl{
  background:var(--panel);border:1px solid var(--line-soft);
  border-radius:var(--radius);padding:14px 15px 15px;
}
.ctrl > label{display:block;font-size:10.5px;line-height:1;color:var(--fg-faint);margin-bottom:11px}

/* Equal columns, never content-sized: minmax(0,1fr) is what stops "−1.0°"
   claiming more room than "0°". No wrapping, so five cups stay one clean row. */
.seg{display:grid;grid-auto-flow:column;grid-auto-columns:minmax(0,1fr);gap:6px}
.seg button{
  min-width:0;min-height:var(--ctrl-h);
  display:flex;align-items:center;justify-content:center;gap:5px;
  background:var(--panel-2);color:var(--fg-dim);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:6px 7px;font:600 13.5px/1.15 var(--sans);letter-spacing:-.005em;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  cursor:pointer;transition:background .12s,color .12s,border-color .12s;
  white-space:nowrap;
}
.seg button:hover{border-color:#39434f;color:var(--fg);background:var(--panel-3)}
/* Selected is an inverted chip: maximum contrast, no hue.
   Geometry is identical to the resting state, so nothing reflows on a click. */
.seg button[aria-pressed="true"],
.seg button[aria-pressed="true"]:hover{
  background:var(--fg);border-color:var(--fg);color:var(--bg);
}
/* ...and acid once that selection leaves the stock value, which is the same
   sentence the changed table rows and the coloured dimensions in the diagram
   are saying. Set from JS against the shipped configuration, so a rider who has
   changed nothing sees a white board, and everything he has moved is lit in one
   colour from the controls at the top to the drawing at the bottom.

   Selected-white and selected-acid are both high-contrast filled chips, so the
   selection itself never depends on telling two colours apart. */
.seg button.moved[aria-pressed="true"],
.seg button.moved[aria-pressed="true"]:hover{
  background:var(--acid);border-color:var(--acid);color:var(--acid-ink);
}
.seg button:focus-visible{outline:2px solid var(--acid);outline-offset:2px}

select.pick{
  display:block;width:100%;max-width:100%;min-height:var(--ctrl-h);
  background:var(--panel-2);color:var(--fg);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:10px 34px 10px 11px;
  font:600 14px/1.2 var(--sans);font-variant-numeric:tabular-nums;
  cursor:pointer;-webkit-appearance:none;appearance:none;
  background-image:linear-gradient(45deg,transparent 50%,var(--fg-dim) 50%),
                   linear-gradient(135deg,var(--fg-dim) 50%,transparent 50%);
  background-position:calc(100% - 17px) 50%,calc(100% - 12px) 50%;
  background-size:5px 5px,5px 5px;background-repeat:no-repeat;
}
select.pick:hover{border-color:#39434f}
select.pick:focus-visible{outline:2px solid var(--acid);outline-offset:2px}
select.pick:focus{border-color:var(--line)}
select.pick option{background:var(--panel-2);color:var(--fg)}
select.pick:disabled{opacity:.45;cursor:not-allowed}
/* Off the stock fork, the same sentence the chips and the diagram say. Outlined
   rather than filled: a chip's resting state is already a filled block, so
   filling it acid only changes the hue, while these dropdowns rest dark and
   full-width, and flooding one with acid would shout an adjuster louder than the
   bike it belongs to. Border and value, which is exactly how a changed dimension
   is drawn - stroked, not flooded.
   background-color and not `background`, because the shorthand would wipe the
   two gradients that draw the arrow. */
select.pick.moved,select.pick.moved:hover{
  border-color:var(--acid);color:var(--acid);background-color:var(--panel-2);
}

/* Model then travel, in the order an owner knows the answers, inside ONE card:
   the pair stays a single slot in the outer grid, so nothing about the controls
   layout or its align-items:start changes. auto-fit sits them side by side on a
   wide card and stacks them on a narrow one without a second breakpoint. */
.forkpick{display:grid;gap:9px;align-items:start;grid-template-columns:repeat(auto-fit,minmax(168px,1fr))}
.fp{min-width:0}
.fp.hidden{display:none}
.fp > label{display:block;font-size:9.5px;line-height:1;letter-spacing:.15em;color:var(--fg-faint);margin-bottom:7px}

.customac{margin-top:11px;padding:12px;background:var(--panel-2);border:1px solid var(--line);border-radius:var(--radius-sm)}
.customac.hidden{display:none}
.acrow{display:flex;flex-wrap:wrap;align-items:center;gap:9px}
.acrow label{font-size:10.5px;line-height:1;color:var(--fg-faint)}
.acrow input{
  flex:1 1 92px;min-width:0;width:92px;
  background:var(--bg);color:var(--fg);border:1px solid var(--line);
  border-radius:var(--radius-sm);padding:9px 10px;
  font:700 16px/1.1 var(--sans);font-variant-numeric:tabular-nums;
}
.acrow input:focus-visible{outline:2px solid var(--acid);outline-offset:1px}
.acrow .uu{font:600 12px/1 var(--mono);color:var(--fg-faint)}
.customac .hint{margin-top:9px;font-size:12px;line-height:1.55;color:var(--fg-dim)}
.customac .hint b{color:var(--fg);font-weight:600}

.cap{margin-top:10px;font-size:12.2px;line-height:1.58;color:var(--fg-dim)}
.cap b{color:var(--fg);font-weight:600}

/* ---------- mode strip ----------
   Exactly the bike switch's track and chips, because it is exactly the same
   kind of choice: a root switch above the controls rather than one card among
   them. It selects in acid too, and for the same reason the bike chip does -
   see the block above .seg.seg-bike for the full rule. The short version: the
   two root switches say WHAT is on screen, the adjusters below say what it is
   set to, and only the second group uses white-until-changed.

   The specific thing that forced it: #mode-intro directly above paints the
   live mode's name in .mode-name acid, so the same word was appearing twice
   in two colours a few pixels apart. Whichever chip is pressed must match the
   word naming it. Change one and change the other - .rev-note .mode-name and
   this rule are one decision in two places. */
.moderow{display:flex;align-items:center;gap:14px;flex-wrap:wrap;padding:2px 0 18px}
.seg-mode{
  flex:0 1 auto;min-width:0;gap:3px;padding:3px;
  background:var(--panel);border:1px solid var(--line-soft);border-radius:10px;
}
/* Doubled class for the same reason .seg.seg-bike is doubled: the generic
   `.seg button` rules sit at equal specificity and would win on source order. */
.seg.seg-mode button{
  min-height:34px;padding:0 22px;
  background:transparent;border-color:transparent;color:var(--fg-dim);
  border-radius:7px;font-size:13.5px;letter-spacing:-.005em;
}
.seg.seg-mode button:hover{background:var(--panel-2);border-color:transparent;color:var(--fg)}
.seg.seg-mode button[aria-pressed="true"],
.seg.seg-mode button[aria-pressed="true"]:hover{
  background:var(--acid);border-color:var(--acid);color:var(--acid-ink);
}

.ctrl-head{font-size:10.5px;line-height:1;color:var(--fg-faint);margin:0 0 12px}
.ctrl-head.hidden{display:none}

/* ---------- reverse mode ---------- */
.reverse{margin-top:26px}
.reverse.hidden{display:none}

.goalrow{display:flex;align-items:flex-start;gap:14px;flex-wrap:wrap}
.goalrow .bikerow-lab{flex:0 0 auto;font-size:10.5px;line-height:1;color:var(--fg-faint);padding-top:13px}
/* Eight labelled chips do not fit a fixed column grid the way five cup angles
   do, so this row wraps instead. Everything else about the chip - the face, the
   height, the radius, the inverted selected state - is the shared one. */
.goals{display:flex;flex-wrap:wrap;gap:6px;flex:1 1 300px;min-width:0}
.goals button{
  min-height:var(--ctrl-h);
  display:flex;align-items:center;justify-content:center;
  background:var(--panel-2);color:var(--fg-dim);
  border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:6px 13px;font:600 13.5px/1.15 var(--sans);letter-spacing:-.005em;
  cursor:pointer;transition:background .12s,color .12s,border-color .12s,opacity .12s;
}
.goals button:hover{border-color:#39434f;color:var(--fg);background:var(--panel-3)}
.goals button[aria-pressed="true"],
.goals button[aria-pressed="true"]:hover{
  background:var(--fg);border-color:var(--fg);color:var(--bg);
}
/* Two is the maximum, so the other six go quiet rather than disappearing: a
   chip that vanished would read as a bug, and one that still reacted would
   promise a third goal the tool will not answer. */
.goals button[disabled]{opacity:.38;cursor:not-allowed}
.goals button[disabled]:hover{background:var(--panel-2);border-color:var(--line);color:var(--fg-dim)}
.goals button:focus-visible{outline:2px solid var(--acid);outline-offset:2px}

/* The conflict line and the findings. Plain text, at the blurb's size, because
   "nothing you can change for free improves both" is the answer as much as any
   card is and must not be styled as a footnote to one. */
.rev-note{margin-top:20px;color:var(--fg-dim);max-width:70ch;font-size:15.5px;line-height:1.6;text-wrap:pretty}
.rev-note span{display:block}
.rev-note span + span{margin-top:8px}
.rev-note:empty{display:none}
/* The static line that sits ABOVE what it explains, so it hands its 20px to the
   other edge. Same class, same size, same ink: a paragraph saying this kind of
   thing gets no second treatment, and the mode intro takes the shared .rev-note
   rule rather than a gap of its own. */
#mode-intro{margin-top:0;margin-bottom:20px}
/* The two mode names, spoken in the sentence above the switch, wearing the same
   acid the selected chip below wears so the eye ties each name to its button
   without a second sentence explaining that it should. Weight 600 is the page's
   existing idiom for emphasis inside a prose note (.cap b, .diagnote b), not a
   new treatment. display is restored because .rev-note span is block above -
   that rule belongs to the findings list in #rev-note, where each span IS a
   line; here they are words mid-sentence and must stay in the flow. */
.rev-note .mode-name{display:inline;color:var(--acid);font-weight:600}

/* The heading over the whole card list. It is a section title, not a label,
   and the difference is the whole point of it: written as a .ctrl-head eyebrow
   it shared one rule with .revblock > .k, so "WHAT TO CHANGE" came out
   typographically identical to "THIS ONE COSTS MONEY" below it and the eye read
   three peer labels rather than one heading over two tiers.

   So it takes the rank "Full geometry" and "Geometry diagram" already own,
   whole and by name: an h2 in the shared label voice above, with an .h2k span
   lifting the ink from --fg-faint to --fg. On this page that ink IS the section
   rank - no new size, weight, colour or spacing is spent on it, and it does not
   touch --acid, which belongs to the bike selector. The padding and the rule
   under those two headings stay behind in .tablecard/.diagcard, because those
   are the panel's chrome and this heading sits on the page.

   The sub-line stays the same .rev-note prose as the findings and the mode
   intro, so the only thing declared here is the seam between the two idioms -
   .rev-note pushes up 20px, and the heading hands down the same 9px a tier's
   own label already leaves above its note. Same move, and the same reason, as
   #mode-intro above. */
.revhead{margin-top:22px}
.revhead.hidden{display:none}
.revhead h2{font-size:10.5px;line-height:1;margin:0 0 9px}
.revhead .rev-note{margin-top:0}
/* When the heading is showing it has to read as labelling the card directly
   under it, so the gap closes to the 12px a tier's own note already leaves
   above its card. Without the heading .revcards keeps its own 22px. */
.revhead:not(.hidden) + .revcards{margin-top:12px}

.revcards{display:grid;gap:14px;margin-top:22px}
.revblock{display:block}
.revblock > .k{display:block;font-size:10.5px;line-height:1;color:var(--fg-faint);margin-bottom:9px}
/* Tier 2 is a separately headed block and never an item in the same list as the
   free answers, so its heading takes the one colour on the page that means a
   part has to be bought - the same acid-dim the masthead eyebrow uses. */
.revblock-paid{margin-top:12px;padding-top:22px;border-top:1px solid var(--line-soft)}
.revblock-paid > .k{color:var(--acid-dim)}
.revblock-note{margin-bottom:12px;font-size:12.2px;line-height:1.58;color:var(--fg-faint)}

/* The card is the control card's panel, made pressable. */
.revcard{
  display:block;width:100%;text-align:left;
  background:var(--panel);border:1px solid var(--line-soft);
  border-radius:var(--radius);padding:15px 16px 17px;
  font:inherit;color:inherit;cursor:pointer;
  transition:border-color .12s,background .12s;
}
.revcard:hover{background:var(--panel-2);border-color:var(--line)}
.revcard:focus-visible{outline:2px solid var(--acid);outline-offset:2px}
/* Same sentence the changed table rows, the lit chips and the coloured
   dimensions are saying: this is not how the bike ships. Same left rule the
   ride-feel card carries, so the accent gains no new shape here. */
.revcard.moved{border-left:2px solid var(--acid)}

.revedit{list-style:none;display:flex;flex-wrap:wrap;gap:6px}
.revedit li{
  background:var(--panel-2);border:1px solid var(--line);border-radius:var(--radius-sm);
  padding:8px 11px;font:600 12.5px/1.15 var(--mono);letter-spacing:-.005em;color:var(--fg);
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
}
.revedit li.none{background:transparent;border-color:transparent;padding-left:0;color:var(--fg-faint)}

/* The stats cluster, unchanged markup and unchanged type, only a tighter column
   floor so five cells still sit in one row inside a card. */
.revcard .stats{margin-top:14px;grid-template-columns:repeat(auto-fit,minmax(132px,1fr))}
.revfeel{margin-top:14px;font-size:15.5px;line-height:1.6;color:var(--fg-dim);text-wrap:pretty}
.revfeel b{color:var(--fg);font-weight:700}
.revalso{margin-top:11px;font-size:12.2px;line-height:1.58;color:var(--fg-faint)}

/* ---------- reset bar ---------- */
.urlbar{display:flex;flex-wrap:wrap;align-items:center;gap:8px 10px;padding:16px 0 2px}
.ubtn{
  background:var(--fg);color:var(--bg);border:1px solid var(--fg);
  border-radius:var(--radius-sm);padding:10px 16px;
  font:700 13px/1.15 var(--sans);letter-spacing:-.01em;cursor:pointer;
  transition:background .12s,color .12s,border-color .12s;
}
.ubtn:hover{background:#fff;border-color:#fff}
.ubtn:focus-visible{outline:2px solid var(--acid);outline-offset:2px}
.urlmsg{font:600 12.5px/1.4 var(--mono);color:var(--ok)}
.urlmsg.warn{color:var(--down)}
.urlmsg.hidden{display:none}

/* ---------- ride feel ----------
   The plain-English payoff, so it gets the largest body type on the page and
   the most air. No tinted gradient: a coloured wash behind a paragraph is the
   single fastest way to make a real tool look like a demo. One accent rule and
   a lot of space does the same job with more confidence. */
.feel{
  margin:30px 0 0;padding:26px 28px 28px;
  background:var(--panel);
  border:1px solid var(--line-soft);border-left:2px solid var(--acid);
  border-radius:var(--radius);
}
.feel .k{font-size:10.5px;line-height:1;color:var(--acid-dim);margin-bottom:14px;display:block}
.feel p{font-size:clamp(16.5px,1.75vw,21px);line-height:1.46;letter-spacing:-.018em;text-wrap:pretty;color:var(--fg)}
.feel p b{font-weight:700}

/* ---------- key stats ----------
   One instrument cluster, not five floating cards. The 1px grid gap over a
   line-coloured container is what draws the hairline rules between cells, so
   the dividers reflow with the grid instead of being faked with borders that
   double up at a wrap. */
.stats{
  display:grid;gap:1px;margin-top:22px;
  background:var(--line-soft);
  border:1px solid var(--line-soft);border-radius:var(--radius);overflow:hidden;
  grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
}
.stat{background:var(--panel);padding:16px 16px 17px}
.stat .k{font-size:10px;line-height:1;color:var(--fg-faint)}
.stat .v{
  display:block;margin-top:12px;
  font-size:clamp(26px,3.3vw,35px);font-weight:700;letter-spacing:-.04em;line-height:1;
}
.stat .v .u{font-size:.44em;font-weight:600;color:var(--fg-faint);letter-spacing:0;margin-left:4px}
.stat .d{display:block;margin-top:9px;font:600 12.5px/1 var(--mono);letter-spacing:-.005em}

.d.up,.delta.up{color:var(--up)}
.d.down,.delta.down{color:var(--down)}
.d.zero,.delta.zero{color:var(--fg-faint)}

/* Redundant encoding for the delta pair. The sign is already in the text; this
   adds a shape, so direction survives a monochrome screen, a colourblind reader
   and a screenshot run through a filter. Drawn with borders rather than a glyph
   so it cannot fall back to a missing character in the subset font. */
.d.up::before,.d.down::before,.delta.up::before,.delta.down::before{
  content:"";display:inline-block;width:0;height:0;
  border-left:3.5px solid transparent;border-right:3.5px solid transparent;
  margin-right:6px;vertical-align:.1em;
}
.d.up::before,.delta.up::before{border-bottom:5px solid currentColor}
.d.down::before,.delta.down::before{border-top:5px solid currentColor}
.d.zero::before,.delta.zero::before{
  content:"";display:inline-block;width:7px;height:1px;
  background:currentColor;opacity:.75;margin-right:6px;vertical-align:.28em;
}

/* ---------- table ---------- */
.tablecard,.diagcard{
  margin-top:26px;background:var(--panel);
  border:1px solid var(--line-soft);border-radius:var(--radius);overflow:hidden;
}
.tablecard h2,.diagcard h2{
  font-size:10.5px;line-height:1.6;color:var(--fg-faint);
  padding:16px 18px;border-bottom:1px solid var(--line-soft);
}
/* The one ink that separates a section heading from every other 10.5px mono
   label on the page. #revhead is in this list on purpose and not in a rule of
   its own: it is the same rank, so it has to be the same declaration. */
.tablecard h2 .h2k,.diagcard h2 .h2k,.revhead h2 .h2k{color:var(--fg)}
/* The config readout is machine output, so it stays in the mono voice, but at
   .02em rather than the label tracking: at .17em this line is ~90 characters and
   ran to four wrapped rows on a phone. */
#configline,#diagline{
  color:var(--fg-faint);letter-spacing:.02em;text-transform:none;
  font-weight:500;
}
table{width:100%;border-collapse:collapse}
th,td{text-align:right;padding:11px 18px;font-size:15px;border-bottom:1px solid var(--line-soft)}
th:first-child,td:first-child{text-align:left}
thead th{
  font-size:10px;line-height:1;letter-spacing:.15em;color:var(--fg-faint);
  padding-top:13px;padding-bottom:13px;
}
tbody tr:last-child th,tbody tr:last-child td{border-bottom:0}
/* Every row carries the tick channel as transparent, so a changed row lighting
   it up cannot shift a single character of the column beside it. */
tbody th{
  font-weight:500;color:var(--fg-dim);font-size:14.5px;
  border-left:2px solid transparent;padding-left:16px;
}
td.val{font-weight:700;font-size:17px;letter-spacing:-.025em;white-space:nowrap;color:var(--fg)}
td.val .u{font-size:.76em;font-weight:500;color:var(--fg-faint)}
td.delta{font:600 13.5px/1 var(--mono);white-space:nowrap;width:1%}
/* "This one moved" is a mark in the margin, not a colour on the number. It
   scans down the left edge in one pass and it is not a colour-only signal. */
tbody tr.changed th{border-left-color:var(--acid);color:var(--fg)}
tbody tr.sep th{
  font-size:9.5px;line-height:1;letter-spacing:.2em;color:var(--fg-faint);
  background:var(--bg);padding:10px 18px 10px 16px;border-left-color:transparent;
}
tbody tr.sep td{background:var(--bg)}

/* ---------- geometry diagram ---------- */
.diagwrap{padding:10px 12px 0}
.diagnote{padding:4px 18px 18px;font-size:12px;line-height:1.6;color:var(--fg-faint)}
.diagnote b{color:var(--fg-dim);font-weight:600}
.diagnote i{font-style:normal;color:var(--acid-dim)}

svg.geo{display:block;width:100%;height:auto;overflow:visible}
/* Strokes are declared in px and pinned, so weight stays honest whether the
   viewBox is squeezed into 320px or 1000px. */
svg.geo path,svg.geo circle{vector-effect:non-scaling-stroke}
.geo .gnd{stroke:#333c48;stroke-width:1.5;fill:none}
.geo .wheel{stroke:#8f9dae;stroke-width:2.2;fill:none}
.geo .rim{stroke:#4b5664;stroke-width:1.2;fill:none}
.geo .frame{stroke:var(--fg);stroke-width:3.2;fill:none;stroke-linecap:round;stroke-linejoin:round}
.geo .fork{stroke:#d2dae5;stroke-width:2.8;fill:none;stroke-linecap:round}
.geo .ht{stroke:var(--fg);stroke-width:5.5;fill:none;stroke-linecap:round}
.geo .bbdot{fill:var(--acid);stroke:none}
.geo .bbring{stroke:var(--acid);stroke-width:1.6;fill:none;opacity:.55}
/* The configured frame, laid over the stock one. Heavier than the old grey ghost
   it replaces, because it is now the answer to the question the reader came with
   rather than a faint reference behind the subject, and because a dash pattern
   costs contrast: half the line is not there, so the half that is has to hold.
   Butt caps, not round: round caps grow each dash by half a stroke at both ends
   and at this weight that closes the gaps into a solid line at the far zoom. */
.geo .delta{stroke:var(--acid);stroke-width:1.8;fill:none;stroke-dasharray:11 9;stroke-linecap:butt;stroke-linejoin:round}
.geo .ref{stroke:var(--dim);stroke-width:1;fill:none;stroke-dasharray:4 5;opacity:.7}
.geo .dl{stroke:var(--dim);stroke-width:1.2;fill:none}
.geo .arc{stroke:var(--dim);stroke-width:1.2;fill:none}
/* Reach and stack lead when nothing has been changed, in white rather than in
   the accent, so the drawing still has a hierarchy at stock without spending the
   one colour that now means something. */
/* path., not a bare class: .pri is carried by the reach and stack LABELS too,
   and by the backing block under each of them, so a bare .pri would outline both
   in grey - an inked edge around every glyph and a visible box around every
   slab. Same trap waits for any future class shared by a path and a label. */
.geo path.pri{stroke:#aeb9c7}

/* ---- changed vs stock ----
   The accent marks the measurements this configuration moved and nothing else.
   It has to beat .dl, .arc, .ref and .pri, all of which are two-class selectors
   declared above, so these are three-class and sit last: same weight, later in
   the file, no !important anywhere.

   Colour is not the only channel. A changed dimension is also drawn heavier than
   an unchanged one, the label's value goes bold white-on-acid against grey, the
   table above prints a signed delta on the same rows, and the diagram's
   aria-label names them in words. */
/* 1.6 and not more. These rules are scaffolding around the frame, not part of
   it, and past this weight an accented dimension line starts reading as heavy as
   the outline it is measuring. Colour is doing the work here anyway. */
.geo .dl.chg,.geo .arc.chg{stroke:var(--acid);stroke-width:1.6}
.geo .ref.chg{stroke:var(--acid);opacity:.8}

/* geometricPrecision is load-bearing, not a polish setting. Left on the default
   the engine rounds each glyph advance to a whole CSS pixel, and these labels are
   set in user units inside a viewBox that is scaled to whatever width the card
   happens to get, so the rendered type size is a fraction like 12.59px and the
   rounding is whatever that fraction leaves over. A .6em advance asked for at
   12.59px is 7.55px of ink and gets served 8, which is 6% more ink than asked
   for, per character, and the label block - whose width is computed from that
   same .6em and is honoured exactly, because a rect is geometry and not text -
   is left too narrow for the words inside it. Six percent is nothing on a word
   and 4px of overhang on a thirty-character one.
   geometricPrecision tells the engine to lay the glyphs out at their true
   fractional advances, which is what it is for on scaled vector text, and the
   arithmetic in label() becomes exactly right at every breakpoint instead of
   right at the widths where the rounding happened to fall favourably. Fixing it
   here rather than by inflating the advance constant keeps the constant an
   honest statement of what the face measures: padding chosen to swallow a
   rounding error would have to be sized for the worst breakpoint and would sit
   as dead air in the block at every other one. */
.geo text{
  font-family:var(--mono);font-size:32px;font-weight:500;
  fill:var(--dim);letter-spacing:.01em;text-rendering:geometricPrecision;
}
.geo text .v{fill:#d2dae5;font-weight:700}
.geo text.pri{fill:var(--fg-dim)}
.geo text.chg{fill:var(--acid-dim);font-weight:600}
.geo text.chg .v{fill:var(--acid)}
/* The stock figure a changed label carries in brackets after its current value.
   It is only ever emitted on a label the accent has already claimed, so the pair
   reads as one sentence: the acid number is the bike being configured, the white
   one behind it is the bike as it ships. Deliberately the lighter neutral and not
   the accent - two acid numbers side by side would say both had moved - and
   deliberately unbolded, so the figure that answers "what is it now" still wins
   the glance. */
.geo text .stk{fill:var(--fg-dim);font-weight:500}

/* The break a label knocks in the line it sits on. label() emits one of these
   immediately before each <text>, so it paints underneath, and hands over the
   label's exact width in em, which it can compute rather than measure because
   the face is monospace. Two widths, in fact: --w1 without the bracketed stock
   figure and --w2 with it. Which one is in force is decided below, at the same
   breakpoint that decides whether the figure is drawn at all.
   Everything else is here and not in the markup, because the type is resized by
   the media queries below and the diagram is rebuilt only when the configuration
   changes, never on a resize. In em these follow the type through every
   breakpoint on their own.
   1.15em of height is the cap-to-descender run of this face with a little air,
   against a y of half that: the text is set dominant-baseline:central, so the
   block is centred on the same point the glyphs are.
   x is what honours text-anchor. It has to be a rule rather than a baked-in
   offset because one label, the head angle, flips its anchor in a media query. */

/* ---- the block that will not fit ----
   Breaking a line for a label only works while the label is shorter than the
   line. Past that the block swallows the dimension whole, end ticks and all, and
   a measurement with no endpoints is worse than no measurement: the reader can
   no longer see what was measured from where. Drafting has answered this for a
   century - when the text will not fit between the ticks, the text steps outside
   and the dimension line is drawn through, unbroken.

   This is the test, and it lives here rather than in the script because it turns
   on the type size and the type size is set by the media queries below, on a
   drawing that is rebuilt when the configuration changes and never on a resize.
   In em it follows the type through every breakpoint on its own.

   --len is the length of the line the label sits on, in user units, and --ox /
   --oy the direction to step in, both handed over by label() and both absent on
   every label that sits beside its line rather than on it; the fallback length
   is bigger than the drawing, so those always fit and never move.

   --lab-fit is how much bare line each end has to keep, and it is deliberately
   two terms in two units. 34 drawing units is the width of an end tick, which is
   drawn in drawing units and does not shrink when the type grows, and a tick with
   nothing either side of it is not visibly an endpoint; on top of that sits .4em
   of air, which does follow the type, so a bigger word demands a bigger gap. The
   pair is what separates the cases that read from the cases that do not: the
   wheelbase label at the narrowest breakpoint keeps more line than this and is
   legible, reach at 600px keeps less and ends up pressed against the bottom
   bracket marker. --lab-off is how far the label then steps, and it is squeezed
   from both sides: it has to clear half a block, .575em, with enough air left
   that the words read as sitting beside the line rather than balanced on it, and
   it has to stay small enough that a label stepping aside does not walk into the
   label above it - the chainstay, which is the one that steps most often, comes
   up under the effective seat angle at the middle breakpoints.

   The clamp is the whole switch: the bracketed term is positive by however much
   the block overhangs and negative by however much it spares, multiplying it out
   drives it past either end of the range, and clamping lands on exactly 0 or
   exactly --lab-off. No branch, no measurement, no second pass. */
.geo{--lab-fit:calc(.4em + 34px);--lab-off:.95em}
/* --w is resolved here, in the stylesheet, and never written into the markup,
   because it is the one measurement on a label that changes with the viewport:
   the bracketed stock figure is shown at some widths and not at others, and a
   block sized for glyphs that are not on screen would knock a hole in its
   dimension line wider than the words standing in it. The fallback covers every
   unchanged label, which carries --w1 alone and has no second figure to lose. */
.geo .lab,.geo .labbg{
  --w:var(--w2,var(--w1));
  --push:clamp(0em, calc((var(--w) + 2 * var(--lab-fit) - var(--len,9999px)) * 999), var(--lab-off));
}
/* The block is placed by x and y, the words by their own x and y attributes, so
   the step has to be spelled once per element in the terms that element uses.
   Both read the same two custom properties, so they cannot drift apart. */
.geo .labbg{
  font-size:32px;fill:var(--panel);
  width:var(--w);height:1.15em;
  y:calc(-.575em + var(--push) * var(--oy,0));
  x:calc(var(--w) * -.5 + var(--push) * var(--ox,0));          /* text-anchor:middle, the default */
}
.geo .labbg.a-s{x:calc(-.3em + var(--push) * var(--ox,0))}               /* text-anchor:start */
.geo .labbg.a-e{x:calc(.3em - var(--w) + var(--push) * var(--ox,0))}     /* text-anchor:end */
.geo .lab{transform:translate(calc(var(--push) * var(--ox,0)), calc(var(--push) * var(--oy,0)))}

/* The type and its backing blocks resize as one thing, so every breakpoint below
   has to name both or the blocks stop covering the words they sit under. */
@media (max-width:900px){ .geo text,.geo .labbg{font-size:41px} }
/* The bracketed stock figure is the first thing to go, before either tier of
   secondary dimension, and it goes at the breakpoint above rather than at one of
   theirs because that is where the type steps up to 41px and the labels stop
   fitting. The head angle is what settles it: it is anchored at its arc and
   grows rightward, and its block ends 199 drawing units past the right edge of
   the viewBox at 760 and 12 past it at 860, which is a closing bracket sliced in
   half by the edge of the drawing. Below 901 there is simply no room for a
   second figure, and half a comparison is worse than none.
   Losing it costs the reader nothing they cannot get: the label is still in the
   accent, which is the answer to "did this move", and the table above the
   diagram prints the stock figure and a signed delta at every width.
   The block has to shrink with the words. A label reserving room for six
   characters it is no longer drawing would knock a hole in its dimension line
   wider than the words standing in it, so --w drops back to the width without
   the brackets in the same rule that hides them. The two can never disagree
   because they are declared together. */
@media (max-width:900px){
  .geo .stk{display:none}
  .geo .lab,.geo .labbg{--w:var(--w1)}
}
@media (max-width:760px){ .geo text,.geo .labbg{font-size:53px} .geo .t3{display:none} }
/* Below 520 the type is ~6% of the drawing height, so the head-angle label is
   flipped to grow leftward into the front triangle instead of off the right edge
   of the viewBox. text-anchor is a CSS property, so no re-render is needed, and
   the block under it is flipped by the same rule at the same weight as .a-s but
   later in the file, so it wins without an !important. */
@media (max-width:520px){
  .geo text,.geo .labbg{font-size:74px} .geo .t2{display:none}
  .geo text.ha{text-anchor:end}
  .geo .labbg.ha{x:calc(.3em - var(--w) + var(--push) * var(--ox,0))}
}
/* The 320-400px band. The drawing is ~280px wide there, so 96 user units in a
   2280-unit viewBox lands at about 12px of real type: the smallest size at which
   a tabular number is still readable rather than decorative. Only four labels
   survive this far down and they sit in four different corners. */
@media (max-width:400px){ .geo text,.geo .labbg{font-size:96px} }

/* ---------- footer ---------- */
footer{margin-top:40px;padding-top:20px;border-top:1px solid var(--line-soft)}
.foot-row{
  display:flex;flex-wrap:wrap;gap:10px 24px;justify-content:space-between;
  font:500 12.5px/1.5 var(--mono);color:var(--fg-faint);
}
.foot-row a{color:var(--fg-dim)}
.foot-row a:hover{color:var(--fg)}
/* Quiet, but it is there and it is unambiguous. */
/* Quiet, but 4.62:1 quiet, not invisible quiet. */
.disclaimer{margin-top:14px;font-size:11.5px;line-height:1.6;color:#707b87;max-width:62ch}

@media (min-width:780px){ .ctrl-wide{grid-column:span 2} }

/* ---------- reading order on a phone ----------
   There is deliberately no visual reorder at any width. A narrow screen reads in
   document order: controls, Reset, the ride-feel sentence, the stats, the table,
   the diagram. Both the sentence and the stat cluster are OUTPUTS - they are what
   the choices above them produced - so neither may be lifted above the controls.
   An earlier version flexed .wrap and pushed the payoff to the top of the phone
   layout; it made the page answer a question the reader had not asked yet. Do not
   reintroduce it, and if you do, move .feel and .stats together or not at all. */

/* ---------- 640: the phone ---------- */
@media (max-width:640px){
  /* One token moves the masthead grid and .wrap together, so the h1 and the
     first control card keep one left edge. */
  :root{--gut:16px;--ctrl-h:44px}     /* 44px = a comfortable tap target */
  .wrap{padding-bottom:64px}

  .masthead{padding:32px 0 34px}
  .masthead-in{gap:26px}
  .eyebrow{margin-bottom:14px;font-size:12px;letter-spacing:.12em}
  #masthead-blurb{margin-top:15px;font-size:15px}
  .bikerow{gap:12px}
  .hero-tag{padding:12px 13px;font-size:9.5px;letter-spacing:.17em}

  .feel{padding:20px 19px 22px}
  .feel .k{margin-bottom:11px}
  /* Two up, always. auto-fit would drop to one 320px column and turn five stats
     into 500px of scrolling. An odd last card takes the full width rather than
     leaving a hole beside it. */
  .stats{grid-template-columns:repeat(2,minmax(0,1fr));margin-top:18px}
  .stat{padding:13px 13px 14px}
  .stat:last-child:nth-child(odd){grid-column:span 2}
  .stat .v{margin-top:10px}

  .controls{gap:10px}
  .ctrl{padding:13px 13px 14px}
  select.pick{font-size:14.5px;padding:12px 30px 12px 10px;letter-spacing:-.01em}
  .acrow input{font-size:16px}       /* 16px keeps iOS from zooming on focus */
  .seg{gap:5px}
  .seg button{padding:6px 4px;font-size:13px;gap:4px}
  /* Compact everywhere, but a finger still has to land on it. */
  .seg.seg-bike button{min-height:40px;padding:0 22px;font-size:13.5px}
  .ubtn{min-height:var(--ctrl-h);padding:10px 18px}

  .tablecard h2,.diagcard h2{padding:14px 12px}
  th,td{padding:10px 12px;font-size:14px}
  tbody th{padding-left:10px}
  td.val{font-size:16px}
  td.delta{font-size:12.5px}
  tbody th{font-size:13.5px}
  tbody tr.sep th{padding:9px 12px 9px 10px}

  /* Give the drawing back the width the card padding was taking. */
  .diagwrap{padding:8px 6px 0}
  .diagnote{padding:4px 12px 15px}
}

/* ---------- 400: the 320-380px band ----------
   Every horizontal budget here was computed against a 320px viewport: 288px
   inside the gutter, 258px inside a control card. Five headset-cup buttons land
   at 48.4px each against a widest label of ~36px, the fork pair stacks to one
   column, and the table's value and delta columns need 151px of the 286px
   available, leaving 135px for the measurement name. */
@media (max-width:400px){
  :root{--gut:14px}
  .masthead h1{letter-spacing:-.03em}
  select.pick{font-size:13.5px;padding-right:28px}
  .customac{padding:10px}
  .seg{gap:4px}
  .seg button{font-size:12.5px;padding:6px 2px;gap:3px}
  .stat{padding:12px 11px 13px}
  .stat .v{letter-spacing:-.045em}
  .stat .d{font-size:11.5px}
  th,td{padding:9px 9px;font-size:13.5px}
  tbody th{padding-left:8px;font-size:13px}
  td.val{font-size:15.5px}
  /* "no change" is the widest string in the narrowest column, and on a stock
     bike it is on twenty-five consecutive rows. Wrapping it did buy the width
     back, at the price of a second line on every one of those rows. Dropping
     the words instead buys the same width and takes the height with it: the
     dash the rule below already draws says "unchanged" on its own, which is
     exactly what it is drawn for. The words are hidden, never unwritten, so a
     screen reader still reads them and every width above this one is
     untouched. Changed rows keep their number and caret; they are the column. */
  td.delta{font-size:11.5px}
  td.delta.zero .dtxt{
    position:absolute;width:1px;height:1px;overflow:hidden;
    clip-path:inset(50%);white-space:nowrap;
  }
  td.delta.zero::before{margin-right:0}
  tbody tr.sep th{padding:8px 9px 8px 8px}
  .diagwrap{padding:6px 4px 0}
  .foot-row{font-size:11.5px}
  .foot-row a{overflow-wrap:anywhere}
}

/* Respect a reader who has asked the OS to stop moving things. */
@media (prefers-reduced-motion:reduce){
  *{transition-duration:.01ms !important;animation-duration:.01ms !important}
}
