:root {
  --tdf-yellow: #ffd21e;
  --tdf-yellow-dark: #e0b400;
  --ink: #1a1a1a;
  --ink-soft: #555;
  --paper: #ffffff;
  --sheet-radius: 16px;
  /* iOS 26 standalone can hold the layout viewport short of the physical
     screen bottom — phantom space for a Safari bar the app doesn't have.
     app.js measures the shortfall into --vp-deficit and sets html.vp-clipped.
     On-phone paint probes (2026-08-29, 59px deficit, iOS 26.6.1) established:
     FIXED layers are pinned to the short viewport and cannot paint the strip,
     but ABSOLUTE (document-anchored) boxes paint all the way to the screen
     edge — like page content behind Safari's real translucent bar. So
     vp-clipped mode swaps the bottom surfaces to absolute (with the root
     grown to 100lvh and scroll pinned, absolute == fixed coordinates) and
     nothing else moves. env(safe-area-inset-bottom) stays 34 in that state,
     measured against the FULL screen. --indicator is home-indicator
     clearance from the PHYSICAL bottom; --safe-bottom the same clearance
     from the VIEWPORT bottom (0 while the OS holds content above the strip);
     --safe-env is the raw inset for surfaces anchored to the physical edge.
     Healthy device: deficit 0 and all three equal env(). */
  --vp-deficit: 0px;
  --safe-env: env(safe-area-inset-bottom, 0px);
  --indicator: max(var(--safe-env), var(--vp-deficit));
  --safe-bottom: calc(var(--indicator) - var(--vp-deficit));
}

* { box-sizing: border-box; }

/* Any author `display` rule beats the UA stylesheet's [hidden] { display: none },
   so an element the JS hides with el.hidden = true stays on screen carrying the
   previous view's content. That is how a stage with no GC leader kept showing
   the last one that had one (Phinney in pink on every stage after a 2012 Giro
   stage). Everything here toggles panels with .hidden, so make it stick. */
[hidden] { display: none !important; }

/* A <button> does not inherit colour. `font: inherit` carries the face, but
   `color` stays at the UA's `buttontext` keyword, and what that keyword is
   worth is the browser's business, not ours: Chrome and Android make it
   black, so every row built as a button (.route-item, .stage-item, the year
   grid's .yg-cell) looked right and nothing in here ever named a colour. iOS
   Safari resolves it to the system link blue instead, and a famous road's
   list of appearances came out with its stage number and its from → to in
   iOS blue while the winner line beside them - which does name a colour -
   stayed black.

   So say it, once, for every button there is. :where() keeps the rule at
   zero specificity, so it only ever fills the gap: every rule that does name
   a colour still wins, wherever it sits and however plain its selector. The
   only buttons this reaches are the ones that were taking `buttontext` -
   the list rows and the year grid, which become the page ink.

   The dark theme hit the same gap first, from the other side (near-black
   paper, where `buttontext` black was black on black); its scoped copy of
   this rule is gone now that the base skin says it for both. */
:where(button) { color: inherit; }

html, body {
  margin: 0;
  padding: 0;
  height: 100%;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  color: var(--ink);
  background: #ddd; /* match the map so any safe-area edge reads as more map */
  overscroll-behavior: none;
}
/* vp-clipped: reclaim the strip with document-anchored boxes (see :root note).
   Everything else about the page is deliberately untouched — the on-phone
   paint probe that proved absolute boxes reach the screen edge ran with the
   root at its default height and overflow (root overflow:hidden propagates
   to the viewport and could re-clip the strip, so it must NOT be set). The
   absolute overflow makes the document scrollable by the deficit; app.js
   snaps a settled stray scroll back to 0. White canvas stays as the
   under-layer during load.

   Every bottom anchor here is written as calc(100% - 100lvh) + offset: the
   physical screen bottom expressed as an offset from the CURRENT viewport
   height. It resolves correctly at the held-back height AND after the
   phantom bar collapses, so when iOS flaps innerHeight between the two
   nothing re-anchors — offsets tied to --vp-deficit re-anchored on every
   flap and bounced the sheet by the deficit (seen on-phone 2026-08-29).
   The class itself latches on for the session (app.js) for the same reason. */
html.vp-clipped, html.vp-clipped body { background: #fff; }
html.vp-clipped .map-wrap, html.vp-clipped .sheet { position: absolute; }
html.vp-clipped .map-wrap,
html.vp-clipped .sheet { bottom: calc(100% - 100lvh); }
/* map bottom sits exactly on the screen edge here (no healthy-mode
   overshoot), so the attribution only needs home-indicator clearance */
html.vp-clipped #map .leaflet-bottom,
html.vp-clipped .map3d-attrib { bottom: calc(var(--safe-env) + 6px); }

/* ---------- Top bar ---------- */
.topbar {
  position: fixed;

  z-index: 1000;
}
.topbar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.topbar-title {
  display: flex;
  align-items: center;
  gap: 8px;
}
.topbar-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
.year-btn {
  font: inherit;
  font-size: 13px;
  padding: 4px 12px;
  cursor: pointer;
  white-space: nowrap;
}
/* the arrow points the way the panel goes: down while it is shut, up while
   it is open (aria-expanded, kept by the app) */
.year-btn-caret {
  display: inline-block;
  transition: transform 0.2s ease;
}
.year-btn[aria-expanded="true"] .year-btn-caret { transform: rotate(180deg); }
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px; height: 28px;
  padding: 0;
  cursor: pointer;
}
.icon-btn:active { transform: scale(0.94); }

/* ---------- Year filter panel ---------- */
.year-panel {
  position: fixed;
  right: 10px;
  left: 10px;
  z-index: 1100;
  padding: 14px;
  max-height: calc(100dvh - env(safe-area-inset-top, 0px) - 72px);
  overflow-y: auto;
  overscroll-behavior: contain; /* never chain a panel scroll into the document */
  -webkit-overflow-scrolling: touch;
}
.race-toggles {
  display: flex;
  gap: 6px;
  margin-bottom: 12px;
}
.race-chip {
  flex: 1 1 0;
  min-width: 0;
  font: inherit;
  font-size: clamp(11px, 3.4vw, 12.5px);
  padding: 6px 4px;
  cursor: pointer;
  text-align: center;
  white-space: nowrap;
}
/* Giro's shorter label gets a narrower chip; TdF and Vuelta match */
.race-chip.race-tdf, .race-chip.race-vuelta { flex-grow: 1.05; }
.race-chip.race-giro { flex-grow: 0.9; }
/* the World Championships: its own row, and black — the rainbow jersey has no
   single colour a map line can wear */
.wc-toggles { margin-top: -6px; }
/* the rainbow bands across the chip's right end, rising at 45° from bottom
   left to top right in the jersey's order (blue on top); grey until chosen */
.race-chip.race-wc { position: relative; overflow: hidden; padding-left: 64px; padding-right: 64px; }
.race-chip.race-wc::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; right: 10px;
  width: 64px;
  pointer-events: none;
  background: linear-gradient(135deg,
    transparent calc(50% - 12.5px),
    #1a7dc0 calc(50% - 12.5px) calc(50% - 7.5px),
    #e3202a calc(50% - 7.5px) calc(50% - 2.5px),
    #1a1a1a calc(50% - 2.5px) calc(50% + 2.5px),
    #f7d117 calc(50% + 2.5px) calc(50% + 7.5px),
    #3aa935 calc(50% + 7.5px) calc(50% + 12.5px),
    transparent calc(50% + 12.5px));
  filter: grayscale(1);
  opacity: 0.45;
}
.race-chip.race-wc.on::after { filter: none; opacity: 1; }
/* the Grand Tour chips while the Worlds view is up: still there, still
   tappable (a tap swaps back), but plainly not what is driving the map */
.race-chip.muted { opacity: 0.4; filter: grayscale(1); }

/* men/women filter (applies to the Tour de France, which has both editions) */
.sex-toggles {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 12px;
}
/* gender-symbol icon buttons: blue = men, pink = women, in the same soft-tint
   style as the race chips (tinted fill + colour border + colour symbol).
   The symbols are inline SVGs (index.html) — the ♂/♀ characters rendered
   from a different fallback font per platform, and iOS's sat on the chip's
   bottom outline however the line box was centred. */
.sex-chip {
  font: inherit;
  width: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 30px;
  padding: 0;
  line-height: 1;
  cursor: pointer;
}
/* "This year" / "All years" presets share the Men/Women row, pushed right */
/* ---------- Year range: dual-thumb slider (inline beside the gender chips) --- */
.year-range {
  position: relative;
  flex: 1;
  min-width: 0;
  margin-left: 10px;
  padding-top: 15px; /* room for the year label sitting above each thumb */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.yr-thumb-label {
  position: absolute;
  top: 0;
  /* transform (translateX) is set per-thumb in JS so edge labels stay in view */
  font-size: 10px;
  font-weight: 700;
  pointer-events: none;
  white-space: nowrap;
}
.yr-slider {
  position: relative;
  width: 100%;
  height: 26px;
  touch-action: none; /* we drive the drag ourselves */
  /* no text selection / tap highlight while dragging (the blue flash) */
  user-select: none;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: transparent;
}
.yr-track,
.yr-fill {
  position: absolute;
  top: 50%;
  height: 4px;
  border-radius: 2px;
  transform: translateY(-50%);
}
.yr-track { left: 0; right: 0; }
/* The two range inputs render the thumbs (and keep keyboard support), but all
   pointer dragging is driven by a custom handler on .yr-slider (see app.js) so
   that overlapping thumbs can be pulled apart by drag direction — hence
   pointer-events:none on the inputs, thumbs included. */
.yr-track,
.yr-fill { pointer-events: none; }
.yr-slider input[type="range"] {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 26px;
  margin: 0;
  background: none;
  -webkit-appearance: none;
  appearance: none;
  pointer-events: none;
}
.yr-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 18px;
  height: 18px;
  border-radius: 50%;
}
.yr-slider input[type="range"]::-moz-range-thumb {
  width: 18px;
  height: 18px;
  border-radius: 50%;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
.yr-slider input[type="range"]::-moz-range-track { background: none; }
.yr-slider { cursor: pointer; }

/* "Filters" heading row: title on the left, reset control inline on the right */
.filter-head-row {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 12px;
}
.filter-title {
  margin: 0;
  font-size: 15px;
}
.filter-reset {
  font: inherit;
  font-size: 12px;
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
}

/* "Select race" heading row (or the active custom-range text) + multi toggle */
.select-race-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin: 14px 0 8px;
}
.select-race-row .year-grid-title { margin: 0; }
#select-race-label.custom-active {
  color: var(--ink);
  font-weight: 700;
}
/* A toggle button: greyed out when off, highlighted yellow with a tick when on */
.multi-toggle {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11.5px;
  white-space: nowrap;
  cursor: pointer;
  padding: 4px 10px;
  /* opt out of native button chrome so our background/border actually paint */
  -webkit-appearance: none;
  appearance: none;
}
.multi-toggle:active { transform: scale(0.96); }
.multi-toggle .mt-tick { display: none; font-size: 11px; line-height: 1; }
.multi-toggle.on .mt-tick { display: inline; }

/* Custom date-range popover — floats over the panel */
.year-note {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.4;
}

/* Build stamp, last line of the panel: which version this device is actually
   running and when it was published — the answer to "has my phone got the
   latest?" when the service worker has served an old shell. */
.app-version {
  margin: 14px 0 0;
  font-size: 10.5px;
  opacity: 0.75;
  text-align: right;
  letter-spacing: 0.02em;
}

/* ---------- Race × year grid ---------- */
.year-grid-title {
  margin: 14px 0 8px;
  font-size: 12.5px;
}
.year-grid {
  display: grid;
  gap: 4px;
  align-items: center;
}
.yg-head {
  font-size: 10px;
  font-weight: 700;
  text-align: center;
  padding-bottom: 2px;
  cursor: pointer;
  border-radius: 5px;
}
.yg-year {
  font-size: 12px;
  font-weight: 700;
  text-align: right;
  padding-right: 8px;
  cursor: pointer;
  border-radius: 5px;
}
.yg-cell {
  height: 22px;
  border-radius: 6px;
  border: 1.5px solid transparent;
  padding: 0;
}
button.yg-cell {
  cursor: pointer;
}
/* the women's-edition marker is a masked box, not the ♀ character: the glyph
   came from a different fallback font per platform and iOS's sat on the
   cell's floor (same story as the .sex-chip symbols) */
button.yg-cell.femmes::after,
button.yg-cell.donne::after,
button.yg-cell.femenina::after {
  content: "";
  display: block;
  width: 12px;
  height: 12px;
  margin: 3.5px auto 0;
  -webkit-mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'%3E%3Ccircle cx='12' cy='8.5' r='5.5'/%3E%3Cline x1='12' y1='14' x2='12' y2='20.5'/%3E%3Cline x1='8.75' y1='17.5' x2='15.25' y2='17.5'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2.6' stroke-linecap='round'%3E%3Ccircle cx='12' cy='8.5' r='5.5'/%3E%3Cline x1='12' y1='14' x2='12' y2='20.5'/%3E%3Cline x1='8.75' y1='17.5' x2='15.25' y2='17.5'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* Part-mapped edition: the share of its stages we can actually draw, in grey.
   It takes the cell's whole face, so the women's ♀ marker steps aside for it —
   the column heading already says which race it is. */
button.yg-cell.partial::after { content: none; }
/* The Worlds grid is one column a year, so the cell has room for the host
   city; a Grand Tour cell is a 1/6th-width square and never gets one. */
.yg-host {
  display: block;
  text-align: center;
  line-height: 19px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: -0.1px;
  padding: 0 6px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.yg-flag {
  width: 16px;
  height: 12px;
  margin-right: 6px;
  vertical-align: -1px;
  border-radius: 2px;
}
.yg-pct {
  display: block;
  text-align: center;
  line-height: 19px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: -0.2px;
}
/* A multi-selected edition moved off its race colour to avoid a clash with
   another pick (ALT_COLORS in app.js). Only the border carries the assigned
   colour (set inline) — the face stays the race colour — so it's thickened to
   read against the black active outline just outside it. */
button.yg-cell.alt-color { border-width: 2.5px; }
button.yg-cell:active { transform: scale(0.92); }
button.yg-cell.active {
  outline: 2px solid var(--ink);
  outline-offset: 1px;
}
/* a race excluded by the current category/sex filter: still shown and tappable,
   but dimmed so it reads as out of scope */
button.yg-cell.disabled {
  opacity: 0.32;
  filter: grayscale(0.8);
}
button.yg-cell.disabled.active { opacity: 0.6; }
/* Filter panel hover: a darkening of whatever colour the control wears.
   Mouse only — on touch a :hover sticks after the tap. The dimmed states keep
   their own filter alongside it. An ON race/sex chip fades instead, since a
   tap turns it off.
   .linked-flip is the grid's preview of a hovered filter (buildYearGrid in
   app.js): every cell the tap would switch, on to off or off to on, meets at
   one half-way face — plain dimming, no grey — and the rest stay as they are. */
@media (hover: hover) {
  .race-chip[data-race].on:not(.muted):not(.quiet):hover,
  body.rider-mode .race-chip.race-wc.on:not(.quiet):hover,
  .sex-chip.on:not(.quiet):hover { filter: none; }
  button.yg-cell.disabled:hover { filter: grayscale(0.8) brightness(0.86); }
  /* the preview fades rather than snaps as the pointer sweeps the grid.
     Mouse only: on iOS a filter transition hands each cell to the
     compositor and back, and the swap re-rasterised the masked ♀ markers
     of the cells around it - a visible flicker on every race-chip tap. */
  button.yg-cell { transition: opacity 0.12s ease, filter 0.12s ease; }
}
.yg-cell.empty::after {
  content: "·";
  display: block;
  text-align: center;
  line-height: 22px;
}
@media (min-width: 700px) {
  .year-panel { width: 360px; }
}
.topbar-title h1 {
  margin: 0;
}
/* the name is the way home: a fresh start, filters cleared, the opening
   draw again (the click handler in the app reloads the bare page) */
.home-link {
  color: inherit;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.home-link:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; border-radius: 3px; } /* (inside: the h1 clips its overflow) */
/* ---------- Search ---------- */
.topbar-search {
  position: relative;
  margin-top: 8px;
}
#search-input {
  width: 100%;
  box-sizing: border-box;
  padding: 9px 12px;
  font: inherit;
  font-size: 14px;
}
/* iOS Safari zooms the whole page in on a text box set smaller than 16px
   when it takes focus (the owner's iPhone: tapping search zoomed the app).
   On a touch screen the box is set at 16px, its padding trimmed so it
   stays the same 38px tall. Locking the viewport's scale would stop the
   zoom too, but takes pinch-zoom away from those who need it. */
@media (hover: none) and (pointer: coarse) {
  #search-input { font-size: 16px; padding-top: 7.5px; padding-bottom: 7.5px; }
}
.search-results {
  position: absolute;
  top: calc(100% + 5px);
  left: 0; right: 0;
  z-index: 1100;
  max-height: 60vh;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: 4px 0;
}
.search-group {
  font-size: 11px;
  text-transform: uppercase;
  padding: 8px 14px 4px;
}
.search-item {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  padding: 9px 14px;
  cursor: pointer;
}
.search-swatch {
  flex: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  border: 1px solid rgba(0,0,0,0.25);
}
.search-pin {
  flex: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  border: 2px solid #9aa0a6;
}
.search-name {
  display: block;
  font-size: 14.5px;
}
.search-sub {
  display: block;
  font-size: 12px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 78vw;
}
.search-empty {
  padding: 12px 14px;
  font-size: 13px;
}
/* the ↵ in "press ↵ to search map locations" — a key, so it looks like one */
.search-empty kbd {
  font: inherit;
  padding: 0 5px;
  border: 1px solid rgba(0,0,0,0.25);
  border-radius: 4px;
}
/* the point the tapped-point list is about — a map tap, a searched-for place
   or a #/here/ link (setPlacePin); tapping it removes it, so it takes a
   finger-sized hit area like the climb and stage labels */
.search-map-pin {
  width: 16px; height: 16px;
  border-radius: 50% 50% 50% 0;
  transform: rotate(-45deg);
  background: #2a7fff;
  border: 2px solid #fff;
  box-shadow: 0 1px 5px rgba(0,0,0,0.4);
  cursor: pointer;
}
.search-map-pin::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  /* rotate(45deg) cancels the pin's own -45°, so the hit box is a straight
     44×44 square rather than a diamond reaching 31px out to each side */
  transform: translate(-50%, -50%) rotate(45deg);
  width: 100%; height: 100%;
  min-width: 44px; min-height: 44px;
}

/* ---------- Race-day banner ---------- */
/* The column of banners is a child of the fixed topbar: top:100% keeps it
   just under the bar whatever its height is (safe area, open search row),
   floating over the map. The race day's and a new version's stack in it. */
.top-banners {
  position: absolute;
  top: calc(100% + 8px);
  left: 0;
  right: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  pointer-events: none; /* (the gaps and the column's empty end are map) */
}
.top-banners > * { pointer-events: auto; }
.race-day {
  display: flex;
  align-items: stretch;
  border-radius: 12px;
  overflow: hidden;
}
.race-day-rows {
  flex: 1 1 auto;
  min-width: 0;
}
.race-day-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 10px 10px 14px;
  text-decoration: none;
  color: inherit;
}
.race-day-row + .race-day-row { border-top: 1px solid #eee; }
.race-day-dot {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 1px solid rgba(0, 0, 0, 0.25);
}
.race-day-text { min-width: 0; }
.race-day-name {
  display: block;
  font-size: 14px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.race-day-stage {
  display: block;
  font-size: 12.5px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.race-day-chev {
  margin-left: auto;
  font-size: 18px;
  line-height: 1;
}
.race-day-close {
  flex: none;
  width: 40px;
  border: none;
  border-left: 1px solid #eee;
  background: none;
  font: inherit;
  font-size: 15px;
  cursor: pointer;
}
.race-day[hidden] { display: none; }
/* a new version: what it is, then Update (js/update.js) */
.update-row {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 10px 10px 14px;
}
/* (its two lines wrap: the whole message, not an ellipsis) */
.update-banner .race-day-name,
.update-banner .race-day-stage { white-space: normal; }
.update-btn {
  flex: none;
  margin-left: auto;
  font: inherit;
  font-size: 13px;
  font-weight: 600;
  padding: 7px 14px;
  cursor: pointer;
  white-space: nowrap;
}

/* ---------- Map ---------- */
/* The map's box holds the place on the page; the map fills it. They are two
   elements because of 3D mode (below): there the map is larger than its box
   and tilted, and the box is what stays where it is. */
.map-wrap {
  position: fixed;
  right: 0; left: 0;
  /* extend past the bottom so the map paints the home-indicator strip in an
     installed (standalone) PWA — whether the OS reports it as env() or, on
     iOS 26, silently shortens the viewport instead (--vp-deficit).
     0 in a normal browser tab. */
  bottom: calc(0px - var(--indicator));
  z-index: 1;
  background: #ddd;
}
/* (an empty shell in 2D; in 3D, what is tilted - see .is-3d below) */
.map-tilt {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
}
#map {
  position: absolute;
  top: 0; right: 0; bottom: 0; left: 0;
  /* A stacking context of its own, which it was when it held the page
     position itself. Without one, Leaflet's panes (z-index 400 and up) are
     ranked against the box's other children instead of inside the map, and
     bury the 3D button the moment the first tile paints - it showed for as
     long as the map was blank, and only came back in 3D, where the transform
     makes the context this makes always. */
  z-index: 0;
  background: #ddd;
}
/* The overshoot above pushes Leaflet's bottom corners (OSM attribution) that
   far below the screen: lift them back to just above the home indicator.
   env() is 0 exactly when the deficit already is the indicator zone. */
#map .leaflet-bottom { bottom: calc(var(--indicator) + var(--safe-env)); }

/* The whole trick: base tiles are rendered greyscale, routes are drawn
   in vector panes above and keep their colour. */
#map .leaflet-tile-pane {
  filter: grayscale(1) contrast(0.92) brightness(1.05);
}

.leaflet-container { font: inherit; }

/* ---------- The map in 3D ---------- */
/* The 3D/2D button and, in 3D, what-a-drag-does beside it: top right of the
   map, where Leaflet's +/- used to be. They live in the map's box, not in the
   map, so they never tilt with it.
   LINE ART, at the owner's asking: no box, no fill - grey strokes on the map.
   A line with no background behind it needs something to be read against, so
   each icon is drawn twice (index.html): a wide pale stroke, then the thin
   grey one over it; the 3D/2D letters get the same from a text-shadow. No CSS
   filter does it - not after what one filter did to one phone. With no fill
   to show which of the two drag modes is chosen, the chosen one is simply
   INK, and the other grey. */
.map-tools {
  position: absolute;
  right: 8px;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 2px;
  pointer-events: none; /* only the buttons take taps; the gaps are map */
}
.map3d-modes { display: flex; gap: 2px; }
/* (the camera, while its picture is being made) */
.map-tool.busy { opacity: 0.35; pointer-events: none; }
.map-tool {
  --tool-line: #737373;      /* a line at rest */
  --tool-line-on: #1a1a1a;   /* the chosen one; and any of them under the pointer */
  --tool-line-off: #b5b5b5;  /* not for pressing yet */
  --tool-halo: rgba(255, 255, 255, 0.9);
  pointer-events: auto;
  width: 38px; height: 38px; /* the finger's target: the drawing is smaller */
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: none;
  color: var(--tool-line);
  font: inherit;
  font-size: 15px;
  letter-spacing: 0.03em;
  line-height: 1;
  text-shadow: 0 0 2px var(--tool-halo), 0 0 2px var(--tool-halo),
               0 0 4px var(--tool-halo), 0 0 1px var(--tool-halo);
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
/* (hover only where there is a mouse: a phone keeps the last tapped button
   "hovered", and the 2D button stayed lit after leaving 3D) */
@media (hover: hover) { .map-tool:hover { color: var(--tool-line-on); } }
.map-tool:active { transform: scale(0.92); }
.map-tool[aria-pressed="true"] { color: var(--tool-line-on); }
/* (the 3D/2D button is an action, not a mode: pressed, it is still a grey line) */
.map3d-toggle[aria-pressed="true"] { color: var(--tool-line); }
@media (hover: hover) { .map3d-toggle[aria-pressed="true"]:hover { color: var(--tool-line-on); } }
/* nothing selected that could be lifted: there, but not yet for pressing */
.map-tool:disabled {
  color: var(--tool-line-off);
  cursor: default;
  transform: none;
}
/* EACH OF THE MAP'S CONTROLS IS A LAYER OF ITS OWN, AND SO IS THE COMPASS
   NEEDLE. They all float over the 3D canvas, which is composited, so the
   browser has to composite them too - and left to itself it lumps what
   overlaps a composited thing into ONE shared layer: here the compass in one
   corner, the buttons in the other and the attribution at the bottom, a layer
   the size of the map. The needle is turned every frame of a turn, and a
   transform on something that is not a layer is a REPAINT of the layer it is
   in: the whole lot rastered again, sixty times a second. On the Android
   phone that had already shown what re-rastering costs it (the flat labels,
   above), the last of the flicker was "largely on the 3D control buttons".
   With these, turning the needle is the compositor moving a layer, and
   nothing here is painted again until it actually changes. (The lab's
   `flatui` takes them away again, to compare.) */
.map-tools, .map3d-compass, .map3d-attrib,
.map3d-compass .line-icon { will-change: transform; }
.map-wrap.lab-flatui .map-tools, .map-wrap.lab-flatui .map3d-compass,
.map-wrap.lab-flatui .map3d-attrib,
.map-wrap.lab-flatui .map3d-compass .line-icon { will-change: auto; }
.line-icon { display: block; overflow: visible; fill: none; stroke-linecap: round; stroke-linejoin: round; }
.line-icon .halo { stroke: var(--tool-halo); stroke-width: 5; }
.line-icon .ink { stroke: currentColor; stroke-width: 1.7; }
.map-tool[aria-pressed="true"] .line-icon .ink { stroke-width: 2.1; }
/* The few solid shapes in the set (the compass needle's north half, half of
   the map-look disc, the locate button's centre). Styled where they are
   DEFINED: a selector cannot reach through <use> into the copy it draws, but
   what is inherited can, and currentColor is taken from each copy's own. */
.line-icon .solid { fill: currentColor; }
/* the locate pin crossed out while location is blocked for the site */
#locate-btn .locate-off { display: none; }
#locate-btn.denied .locate-off { display: inline; }

/* ---------- How the map looks ---------- */
/* The button beside 3D goes round three looks (mapLook in js/app.js puts the
   class on <html>): the skin's own grey - none of this - then THE OTHER grey,
   then the tiles in full colour. Here the other grey is the dark one; the
   dark theme, whose own is dark, turns these round in theme.css.
   A look is more than the tile filter. The box's background is what shows
   where tiles have not loaded, and in 3D what the far ground fades into, so
   it goes with the map; so do the tilted map's copy of the tiles, its haze
   and its ink; and the line-art buttons are drawn for the map they lie on,
   pale lines with a dark casing over a dark one. */
html.map-look-alt #map .leaflet-tile-pane { filter: grayscale(1) invert(1) brightness(1.3) contrast(0.78); }
html.map-look-colour #map .leaflet-tile-pane { filter: none; }
html.map-look-alt .map-wrap, html.map-look-alt #map { background: #0a0a0a; }
html.map-look-alt .map3d-canvas {
  --map3d-tile-filter: grayscale(1) invert(1) brightness(1.3) contrast(0.78);
  --map3d-haze: 10, 10, 10;
  --map3d-ink: 255, 255, 255;
  --map3d-wall-alpha: 0.6;
}
html.map-look-colour .map3d-canvas { --map3d-tile-filter: none; }
html.map-look-alt .map-tool {
  --tool-line: rgba(255, 255, 255, 0.62);
  --tool-line-on: #ffffff;
  --tool-line-off: rgba(255, 255, 255, 0.28);
  --tool-halo: rgba(10, 10, 10, 0.85);
}
html.map-look-alt .map3d-note {
  color: rgba(255, 255, 255, 0.75);
  text-shadow: 0 0 3px #000, 0 0 3px #000;
}
/* how far the heights are stretched, under the buttons: a long flat stage
   is lifted many times over or it would not lift at all, and it says so */
.map3d-note {
  flex-basis: 100%;
  text-align: right;
  font-size: 10px;
  font-weight: 700;
  color: #555;
  text-shadow: 0 0 3px #fff, 0 0 3px #fff, 0 0 2px #fff;
  pointer-events: none;
}

/* Which way is north, pinned to the map's top left corner while it is in 3D.
   The needle is turned inline; a tap turns the MAP back to north. */
.map3d-compass {
  position: absolute;
  left: 8px;
  z-index: 5;
}

/* In 3D the map is larger than its box and turned; the box clips it. Only
   then: a box that never clips is a box that cannot change how the bottom
   strip paints on iOS 26 (see the :root note). */
.map-wrap.is-3d { overflow: hidden; }
/* WHAT IS TILTED IS A WINDOW ONTO THE MAP, NOT THE MAP (#map-tilt; map3d sets
   its place, size and matrix inline), AND THE TILES IN IT ARE ONE PICTURE.

   How it got here, because the reasoning went wrong twice. On one Android
   phone (a PowerVR GPU under Chrome's Vulkan back end, dpr 3.3) the tilted map
   was fine at rest, set the WHOLE SCREEN flickering once it was turned, and
   left the top bar black; iPhones (Safari and Chrome, both WebKit) and
   desktop Chrome never showed it, so none of it could be seen here. It was
   put down first to the map's near edge lying behind the eye, then to too many
   small off-screen passes (a filter and Leaflet's blend mode on every tile),
   and "fixed" by greying the tiles as one picture with a filter ON THEIR
   WINDOW - which made one very large filtered pass under perspective, about
   2900x4000 px there, and was the worst of all. Then the phone was given
   instruments (the 3D lab, below) and said so itself: taking that filter off
   stopped the flicker; so did hiding the tiles. MEASURED ON THE DEVICE, which
   is the only authority this has.

   What came of it, at the owner's suggestion - "keep a copy of the map and
   treat it as one tile": tilted, the live tiles are hidden, and a COPY of
   them lies in their place, one canvas (map3d's compose()), greyed as it is
   drawn. The compositor is handed a finished picture: one quad, no filter, no
   blend mode, no pass at all, where it had sixty to a hundred tile layers
   with an effect or two each. Do not put a CSS filter back on anything that
   is tilted.

   What else stands:
   - the window is only as large as the ground the eye can see, its near edge
     held in front of the eye, and has no effect on it: no clip, no opacity;
   - the map inside it only TURNS (a flat, 2D turn) and clips nothing
     (Leaflet's container is overflow:hidden; on a turned plane that is a pass
     the size of the whole map). The box, above, clips in screen space. */
.map-wrap.is-3d .map-tilt { transform-origin: 0 0; }
.map-wrap.is-3d #map {
  transform-origin: 0 0;
  overflow: visible;
}
/* NOTHING UNDER THE TILT IS PAINTED: PICTURES ONLY. A canvas or a tile is a
   picture the compositor is handed as it is, however large it lies on the
   ground. Anything the browser has to PAINT there it paints at the screen's
   own density over the whole of its box - and the boxes under a tilt are
   several screens across. Two were being painted for nothing:
   - Leaflet's SVG of invisible hit-lines (26 px strokes at opacity 0), a
     fifth larger again than the map: about 2300x2650 css px on an iPhone,
     55 million pixels at its 3x. Nothing can be tapped through a tilted map
     anyway - map3d's canvas takes every gesture;
   - the map's own box: its background colour and Leaflet's attribution, lying
     flat in a far corner of the ground (the tilted map shows its own copy of
     that, upright: #map3d-attrib). 38 million more. The box behind the tilt
     is the same colour, so the map need not be any.
   What they cost grows as the tilt flattens and the far ground comes into
   view: an iPhone ended the page at a tilt of 66, 70 and 78 (the flattest
   there is) and never below - its hunt, which then tilted no further than
   64, lived through everything. (`layersback` in the lab puts both back.) */
.map-wrap.is-3d:not(.lab-layersback) #map { background: transparent; }
.map-wrap.is-3d:not(.lab-layersback) #map .leaflet-control-container,
.map-wrap.is-3d:not(.lab-layersback) #map svg.leaflet-zoom-animated { display: none; }
.map-wrap.is-3d #map .leaflet-tile-pane { visibility: hidden; }
/* WHERE THE COPY IS NOT MADE: WebKit - every browser on an iPhone. There the
   live tiles stay, greyed by the pane's own filter exactly as the flat map has
   them (so the map's look needs nothing said here). Not a preference: the copy
   ended the page on an iPhone, mid-turn, in every build that had it (v974,
   975, 978: with its filter, without, and with a tenth of the landings),
   and the live tiles are the only ground that phone has ever been happy
   with - as the copy is the only one the Android phone above is. map3d puts
   the class on (domGround); the lab's `domtiles` and `copytiles` force it
   either way, anywhere. */
.map-wrap.is-3d.dom-ground #map .leaflet-tile-pane { visibility: visible; }
.map-wrap.is-3d.dom-ground .map3d-ground { display: none; }
/* NO LABELS OR PINS ON THE TILTED MAP. There are a hundred of them, each a
   layer of its own that the browser rasters again whenever a turn changes the
   size it is seen at; on the Android phone above they were the last of the
   flicker (hiding them, or pinning their raster size, stopped it), and lying
   flat on the ground they said little. The famous roads ON THE ROUTE being
   shown are named instead, upright on poles over the wall, by map3d's
   drawPoles; start and finish are on the wall too. */
.map-wrap.is-3d #map .leaflet-marker-icon { visibility: hidden; }
.map3d-ground {
  position: absolute;
  top: 0; left: 0;
  transform-origin: 0 0;
  pointer-events: none;
}
/* THE 3D LAB'S SWITCHES (map3dLab in js/app.js; five taps on the title with a
   stage or road selected). Each changes ONE thing about the tilted map, so
   that what a phone's GPU cannot do can be found from the phone. (`domtiles`
   and `copytiles` choose the ground: see .dom-ground above.) */
.map-wrap.is-3d.lab-notiles .map3d-ground { display: none; }
.map-wrap.is-3d.lab-notiles #map .leaflet-tile-pane { visibility: hidden; }
.map-wrap.is-3d.lab-nolines #map .leaflet-pane canvas,
.map-wrap.is-3d.lab-bakelines #map .leaflet-pane canvas { visibility: hidden; }
.map-wrap.is-3d.lab-domlabels #map .leaflet-marker-icon { visibility: visible; }
body.lab-nobar .topbar { backdrop-filter: none; -webkit-backdrop-filter: none; }
/* (cpuwall: the raised route drawn by the CPU, on a canvas of its own laid
   over the one that takes the gestures) */
.map3d-canvas.map3d-canvas-cpu { pointer-events: none; }
/* The raised route. It also takes every gesture while the map is tilted:
   Leaflet reads a pointer's place from the map's bounding box, which means
   nothing once that box is a trapezoid, so map3d does the moving itself. */
.map3d-canvas {
  /* a canvas cannot be restyled from a stylesheet, so its ink is declared
     here and read back (as .road3d-canvas does for the road page's view) */
  --map3d-ink: 0, 0, 0;
  --map3d-wall-alpha: 0.5;
  /* what the far ground of a tilted map fades into, where the drawn map runs
     out: the box's own background, which is what lies beyond it */
  --map3d-haze: 221, 221, 221;
  /* how the tiles are greyed as they are copied into the tilted map's one
     picture: the flat map's own tile filter, word for word */
  --map3d-tile-filter: grayscale(1) contrast(0.92) brightness(1.05);
  /* the names on poles over the route: the flat map's label, in a canvas */
  --map3d-label-bg: 255, 255, 255;
  --map3d-label-ink: 26, 26, 26;
  --map3d-label-font: 700 11px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  --map3d-label-upper: 0;
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  z-index: 2;
  display: block;
  touch-action: none;
  cursor: move;
  -webkit-user-select: none;
  user-select: none;
  /* a finger resting on the tilted map is holding the camera still, not
     asking iOS for anything (see the touchstart listener in map3d) */
  -webkit-touch-callout: none;
  -webkit-user-drag: none;
  -webkit-tap-highlight-color: transparent;
}
/* THE CURSOR IS THE BUTTON THAT IS PRESSED. (It was the other way about: the
   turning mode wore `move`, the four-way arrow - which is the MOVE button's
   picture - and the move mode wore a hand, which is on no button.) Move mode:
   the system's four-way arrow (`cursor: move`, in the rule above). Turning -
   the orbit mode, or a right-button / ctrl / shift drag in move mode - the
   orbit button's own two arrows (#i-orbit in index.html) in the colours the
   system's own cursors wear on Windows, white with a black outline, so it
   sits beside the move arrow as one of the family; there is no system cursor
   for it. A PNG at 1x, 2x and 3x, MADE BY tools/make_cursor.py - do not edit
   the data by hand. (The first cut was an SVG, dark with a white halo: seen
   beside the white system cursors it read as inverted, and rasterised once
   at 24 px then scaled it had a rough 1 px rim.) */
.map3d-canvas.orbit,
.map3d-canvas.turning {
  cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACDklEQVR42r2V0W3jMBBEH4L7D90BS2AJvA5YAtWB0oGuAqcDugO6AyYV0B1IHTCpYO+HdGRHsuzgcAMQAhbULnZ2Zwj/ET2Q6rdBzeIjIPWMQAR8vbOd3BgjKSUxxggwAHullHjvJcYo4zhKwziOEkIQ770Apd6/iZRSEhGRlJIA0ve9lFJkC+M4inNOgAzo68S/rgNKKWKMOOfOsdPpxNvbG5+fnwA8Pz/jnENrjdaaGCOHw8F0XZeB38BptYM5cs5irW2c7ysVAxCAYq29oC6E0DpRmwVacq211OSLs1NKSc75/F+la7irAxGRUkrbnDWY3W5XWpFSiiilpM3jqV6a3t/fF/+epgng40aBUynlz8vLy3mGdX5ufklX7mThlLrvALaeJYyti7qJ6VEhRmNM00m8Q0v9I8mtMeY8l5rA3uMGTxtW8Sheqw5ef9LeFkX3WUXOuQlrkaoZNb4uwNJifLON3nt/5riqd4uqMhfYHMMwSFX715oqpc7mlnOW3W5XAHOjwKoZrq3p4Jy7sIqqyLVO9lprsdbKdSdrBRSQQwgXVlzpKrXlZnZ7YFxKfl1gbtcfQNd1XQKU9x6tNSklpmlSx+PRz+3aWosxXwwej0e01hexNWggO+curHgNpRTp+14AuXq0Nq1iAIr3XkII357MGKN479uc9sDwE6tQdd/jwqPfVK/+sRs8hr9LtnMqxWgXqAAAAABJRU5ErkJggg==") 12 12, grab;
  cursor: -webkit-image-set(url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAACDklEQVR42r2V0W3jMBBEH4L7D90BS2AJvA5YAtWB0oGuAqcDugO6AyYV0B1IHTCpYO+HdGRHsuzgcAMQAhbULnZ2Zwj/ET2Q6rdBzeIjIPWMQAR8vbOd3BgjKSUxxggwAHullHjvJcYo4zhKwziOEkIQ770Apd6/iZRSEhGRlJIA0ve9lFJkC+M4inNOgAzo68S/rgNKKWKMOOfOsdPpxNvbG5+fnwA8Pz/jnENrjdaaGCOHw8F0XZeB38BptYM5cs5irW2c7ysVAxCAYq29oC6E0DpRmwVacq211OSLs1NKSc75/F+la7irAxGRUkrbnDWY3W5XWpFSiiilpM3jqV6a3t/fF/+epgng40aBUynlz8vLy3mGdX5ufklX7mThlLrvALaeJYyti7qJ6VEhRmNM00m8Q0v9I8mtMeY8l5rA3uMGTxtW8Sheqw5ef9LeFkX3WUXOuQlrkaoZNb4uwNJifLON3nt/5riqd4uqMhfYHMMwSFX715oqpc7mlnOW3W5XAHOjwKoZrq3p4Jy7sIqqyLVO9lprsdbKdSdrBRSQQwgXVlzpKrXlZnZ7YFxKfl1gbtcfQNd1XQKU9x6tNSklpmlSx+PRz+3aWosxXwwej0e01hexNWggO+curHgNpRTp+14AuXq0Nq1iAIr3XkII357MGKN479uc9sDwE6tQdd/jwqPfVK/+sRs8hr9LtnMqxWgXqAAAAABJRU5ErkJggg==") 1x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAEW0lEQVR42u1a7XXjKhC92bMFUAJbgfUqWNKBUkFIB6QC8yrQdiB3IHeAUgF2BagD1AHvR8APK0hCHz67P3bO0Yljy4jLzFzuDAb+2u+1p5H3KQDmX7cAuplxiL+/APDTv8eiz3sAF//36sdsHwWqAuAGVzVyLwfQJO7PuSyA2oPezQMVAAEAZVkCAM7nc/jsF4D3aOJH76nP5WYMjDEcDgcQQsDY/w7o+x6XywVd1+F6veJ8PqPr7pzaAvh3q1doWB2ttQumtY5XjQMw4X9Kqavr2llr3VIzxjghhCOExOM3PhxXGQfgyrL88rCyLO/cTyl1Sim3h1lrnZQyBmIBlGsASABOSvnlIVLK2+RTn+8FZLBQMmfS33LREUKgtcbxeMy6v+97tG2Ltm2zx2+aBlV144ujT/LtHmiaZjbOtdZOCOGKokiyDiHEMcZcVVXOGDM5Vl3XOQyYD2DKmqYZm7QFoADoFCDO+SSQAQi+OwBjjGOMDSdcDTawIctxHxZZORWBsDn7RTYApdSQ/tzCeKUxkKIoRkOUcx7GV5uSOE7Oj48PCCEgpYQQAoSQQMMyc5gOwBuAfwB0l8sFz8/P6Pv+qyyoKlBKgzThu+fAYLOzK+ibhDwZ80QUSuYhAJxzcSKzLSA458nxKaWzCV2O7cQ5Fj2ArlQChfdgcpePvKCnVsEBcHVdL5p8lGhmoxKWABxjLPmciDjomBoVYeOglIbkmc7GrouV5fMOOt8AoEqpO0ULAC8vL0EdvwE4jQ0ggisXXCYR+4GVZO5GFHshlQuRVuK5MckyLpr4Xmr31ZnFS46sp3ig6SC5pZROShknuF5aFZZlOVSp1SMnz8PkYz631mbR4JrS9tvOACgAcM7D7nyTypzzu3sy7B3AD5+wb/71+/Cm7zt1JR5l3RTb7NGVeGQIbWupbEiePZL4YV0Jmkm/uTRKM6m62NSViKSCXMhIYxsZi9szCxphIjeJ7+z19RWn0wkAXheAGEs+FoqThXKFRGH8azSECCFJIRWVjxLbtc6obM6skclkAqbUqFJqUX06lWeU0lWSPSKVcpYGZ2SzXtkCZKHyWmNRg03OujjlBWttXHmtBWGHTLc3gORmNALCrAgnGXJNCHHbK6SUs82zXADwLDGaaAMQoaWyROLWw65dTrN4CYBbfTpVXsZN3wgInwDDPBXeCibG2GyrcQ2AWyjNgTDGxMmd2n1VqsIrisI1TfOQHEhqo7lC3xjjqqpyjLFU1+42aSHEbAJba5Pg1gC4i9eqqhatmFLKKaUWneBorR0hZO6cQq5qdwSttOZIaWGI7A4gNL5sYI0c2lviqUhyu4yjrtU1BYmPVQOX57LIMM7ruh5O3MTkkSPrn1YCYf4I6NYLopSiLEscDgdQSlEUxV1d3LYt+r7H9XpNHT11/pj1tPC4d7MVPsntysPuZiIcskrbJ+xnoXo6+FArBlopLPkHPn920PqfHsyp1z+hufDXRu0/OzhL/0I2FkoAAAAASUVORK5CYII=") 2x, url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAGE0lEQVR42u1c7ZGrNhQ9zuR/9CpYpYJVKlheB2wHuIKQCpZUQFyBSsAd4K1AbAViKxCpQPmxMoO1AsSHwJnxndE8D888eEf3+x4ZeMhDHhJQDjs8kwKIzJ/PAIhZrPOdBkBlPlcA/gVwMeuuJAEgAOjOEua6rxDzfQ5AWf/WnCUAZBagmwsBUI68aGm+1ydsRVCGwEpH3iOIiZXGDMbkAuCndS0C8NZ3PyEEjDFEUYSnpydQSttrrX01Darqy8KqqsLHxweqqmqvOaQBcALwj/kc3KzaXcqyTCultNZaK6V0lmX2LiYdjXFqHaVUZ1mmhRB6iSildFEUOkkSTQhxaZQy5hdURBccl1ggCQC5C5gkSRaDMgQW51xTSl1ASU8LmCXtg66a43q5Id+QJImWUuqtZACoLChAQ+ICJoqiTYGxJc9zl+mVazvxyQARQnSe5/oeREqpoyhymRzbBSDGWDA/s1SbHA6crRHmdftB6/4bDwfEcQzOOQiZrsF1XbchvK5r1HV9mytEXz725eWl/TxVzuczjscjmqbppgM/O1l6OA1KkmTyrgohdJIkfQ51cEVRpPM87w0cQ8+0/JIyZU5YgFaKMrPW1CjpAEkscdyrAVSW5RAwykSYzCSbkbUyU6qIPqDSNPXWKCGEfT/fDSCllE7TtA8UPiOJIz2FsyaE6LIsvTXZuj/eHCAhhGaM9WlNsUJOErlKmr6s3+U7rQ0jmwHksPW+CnwNie1OgU/gUErZZs93ddJlWdq7tmYJ8K0t4wNSWZb2+0S7RjFH4qZWLo/4VJDiOLbLkX0B0lrbqs1CgsQ5Hy1J5mpRMICsGilEO4J3o9tYrmSZPt8dICu6RYFaxaKbfU/UIq8MW15vWLNCdyRqofrIrPucoiimaHXq84Csq6ZjD5gZ/jnCStZt905IHoVPNX8Nna0TpZSC0nn1XbcB36mo/wBQB57KyKuWFkWBOI573+/Hjx+TZ4Wsa2orLrXhPKvtk8dxPKU7OmkX8hXnWrzHCRJj+6XZFGk+L5130Zn99Rv5dcg6APxl1tKIU/XMq5ipz2jPePpPAK8zG1y1uY8BwOl0wtvb27cvnU4n+z3vRtgE7WMbz/h2F9L1cYQQzTnXUkotpdScczvqyQXmVnpuQnlP2pN2wXH5B6WUDVK6I89gcyl9aiYrRynvgKmymbTmNVQvWaWA3ONFf8FDFgEUSi3bLPpyufR+yfq7+p6AC+3YtnTSYZ1ooNC4ZZjHvRCo7jFRxL0QqJZk00NF8RArgziGjVMXtiJQ0YX+bkqxSu3+88LuQj7VfL1artZUIN+wfgvBmu3V1EH6i1Kql9pyPp/x+vrarfx/D8wwpTbpgDE2i3pzpd9YlJsKX9SYxtsHjY1OrDFOFlh7biYWa5C2iqKwo2U2KUcZmwoURbFVtCHd56zJaLMGmnJyJ25stmRNBUSgfCVCh/K3tsxpuXLfMa6UcouJReSr1VsBFE3RIgfvhofqPo6NcbYC6KbcGJsKOMa4IUBSIQaaSwC60SIfFpcDpDU7dRksBocvsywUQDe+iFLqxQd0gKQwk+o2lIK41tzwvwQg0lVtX+qvo6DVE45Yjb1P4QJnCaF9CUAwu+/Nu+myuHroeNeGG5kRyZwH9KawXUMA9I2c5LtTA2zXLljc+Be70k7M9bKv/qKUruKH1gDohncz1dallC7fNHtRSr01eSuArhm2WuIQlVI6z3PXaRwvUOYe0BvznUMAHWYkbG3oJoSAc95LLRmTy+WC9/d3Z/P+Srl5fn4GY2wW/aZpGhyPR5zP59GDOWself/Wk7mXs2JDhPYtTGywVbr3acOxk4dbA+QcDe198rDntOFuADlLgBBRZo0oOZdAtWY7QvYBtSSJ8zmg1wNKvvGExlublKtGSpJEF0WxGCwhhM6ybOgcWpeAunjGdwjUHk3xRZ9zlhOMMTDG2hDuasBXVYWmaVDXNT4/P3G5XNprAz+T8bfjF2KW/MxG8Pl+OlaBrzDX4iO98P8FgYoZ8xMrgjK14E3mMFUOOwF2LUh/6+w+s/7DV2ZsA+DD0F8u90aDechDHrJI/gPK+6K4oGseGgAAAABJRU5ErkJggg==") 3x) 12 12, grab;
}
/* the last tilted frame, laid over the flat map for a moment as the tilt
   ends and faded out (map3d's afterImage): a picture, nothing under it moves */
.map3d-after {
  position: absolute;
  top: 0; left: 0;
  width: 100%; height: 100%;
  z-index: 2;
  pointer-events: none;
  opacity: 1;
}
/* a zoom glide's landing, faded (map3d's fadeLanding): a line canvas, or the
   ground copy, doubled as it stood before the redraw, fading out over it */
.map3d-ghost { pointer-events: none; }
/* Leaflet's attribution, where Leaflet puts it */
.map3d-attrib {
  position: absolute;
  right: 0;
  bottom: calc(var(--indicator) + var(--safe-env));
  z-index: 4;
  padding: 0 5px;
  font-size: 12px;
  line-height: 1.4;
}
.map3d-attrib a { text-decoration: none; }
.map3d-attrib a:hover { text-decoration: underline; }

/* user location marker */
/* you are here. Small: it marks a point on a road, and at the zooms this map
   is read at a fat dot covers the road it is meant to be on. The white collar
   and the glow around it are what make it findable, not its size - they are
   kept in proportion (the divIcon's box in app.js is the same 12px, or the
   dot sits off its own anchor). */
.you-marker {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: #2a7fff;
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 1.5px rgba(42, 127, 255, 0.4), 0 1px 5px rgba(0,0,0,0.4);
}

/* climb labels (shown when idle) — clickable to select the road */
/* The marker div is a zero-size point sitting exactly on the road (iconAnchor
   [0, 0]); the dot centres itself on that point and the name hangs off its
   right. Laying the two out in text flow instead would put the dot wherever
   the line box happened to fall — a few pixels below the road, invisible when
   zoomed in but plainly off the line when zoomed out. */
.climb-label {
  white-space: nowrap;
  cursor: pointer;
  width: 0; height: 0;
}
.climb-label:active { transform: scale(0.94); }
/* Finger-sized tap targets (iOS wants >=44×44): an invisible pseudo-element
   centred on what's drawn grows each label's hit area without changing it —
   hit-testing is geometric, so the unpainted box still catches taps. The
   climb halo hangs off the marker div, whose zero-size box is the road point
   itself, so 50%/50% lands on the dot; the stage halo hangs off the pill span,
   whose self-centring transform both offsets it from the marker div and makes
   it the pseudo's containing block, keeping the halo on the visible pill. */
.climb-label::before,
.stage-num-label .stage-num-pill::before {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: 100%; height: 100%;
  min-width: 44px; min-height: 44px;
}
/* While stage pills are up they are the primary target: the climb labels'
   panes hit-test above the pills' pane, so their invisible halo would steal
   taps aimed at a pill — drop it; the dot and name still catch their own
   taps and pass them up to the marker. */
body.stage-pills-on .climb-label::before { display: none; }
.climb-label .climb-dot {
  position: absolute;
  left: 0; top: 0;
  transform: translate(-50%, -50%); /* centre the dot on the road */
  /* sized by the zoom (app.js sets both on <body> - see roadDotAt): a
     borderless pinprick at the Europe view, a step bigger every level in */
  width: var(--road-dot, 9px); height: var(--road-dot, 9px);
  border-radius: 50%;
  border: var(--road-dot-border, 1.5px) solid #fff;
  box-sizing: border-box;
  box-shadow: 0 0 2px rgba(0,0,0,0.55);
}
/* The open stage's classified climbs (showKomDots): a famous road's dot,
   size and ring, in the category's colour; it grows under the mouse and
   puts up the climb's name as a road's dot does (.climb-name's chip).
   The marker is not interactive, so the dot opts back in to the pointer. */
.kom-dot { width: 0; height: 0; }
.kom-dot:hover { z-index: 10000 !important; }
.kom-dot .kom-pin {
  position: absolute;
  left: 0; top: 0;
  transform: translate(-50%, -50%);
  width: var(--road-dot, 9px); height: var(--road-dot, 9px);
  border-radius: 50%;
  border: var(--road-dot-border, 1.5px) solid #fff;
  box-sizing: border-box;
  box-shadow: 0 0 2px rgba(0,0,0,0.55);
  pointer-events: auto;
  transition: transform 0.12s ease-out;
}
.kom-dot .kom-pin:hover { transform: translate(-50%, -50%) scale(1.6); }
/* an HC climb: white in a black ring twice as heavy, the same size outside */
html .kom-dot .kom-pin.kom-hc,
html .climb-label .climb-dot.kom-hc {
  background: #fff !important;
  border-color: #0b0b0b;
  /* (never none: zoomed out the other dots lose their ring, and a white
     dot with none is lost on the map) */
  border-width: max(1px, calc(var(--road-dot-border, 1.5px) * 2));
}
/* its label is the profile's own (.elev-climb-tip; owner, 2026-10-07):
   name and summit altitude, the figures line under it, in the profile's
   chip, font and sizes - not a map name's pill. Its face named: the map's
   box sets its own. */
.kom-dot .kom-name {
  display: none;
  position: absolute;
  left: calc(var(--road-dot, 9px) * 0.8 + 4.5px); top: 0;
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--t-body, sans-serif);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.3;
  padding: 0 3px;
  border-radius: 3px;
  pointer-events: none;
}
.kom-dot .kom-name .elev-climb-stats {
  display: block;
  font-weight: 600;
  opacity: 0.8;
}
@media (max-width: 699px) {
  .kom-dot .kom-name { font-size: 11px; }
}
.kom-dot .kom-pin:hover + .kom-name,
.kom-dot.kom-hot .kom-name { display: inline-block; }
/* km 0 on the open stage (km0Icon): the start's disc with "km0" in it,
   which opts in to the pointer and puts up the profile's label for it - in
   the profile's own chip, word and look alike (.elev-climb-tip; owner,
   2026-10-05), not a map name's pill. Its face named: the map's box sets
   its own. */
.km0-dot:hover { z-index: 10000 !important; }
.km0-dot svg {
  display: block;
  pointer-events: auto;
  letter-spacing: -0.2px;
}
.km0-dot .km0-name {
  display: none;
  position: absolute;
  left: 22px; top: 9px;
  transform: translateY(-50%);
  white-space: nowrap;
  font-family: var(--t-body, sans-serif);
  font-size: 10px;
  font-weight: 700;
  line-height: 1.3;
  padding: 0 3px;
  border-radius: 3px;
  pointer-events: none;
}
.km0-dot svg:hover + .km0-name { display: block; }
@media (max-width: 699px) {
  .km0-dot .km0-name { font-size: 11px; }
}
/* km 0's label (km0LabelHtml), on the map and the profile: the neutral
   roll-out's length under the name, as a climb's figures are */
.km0-neutral {
  display: block;
  font-weight: 600;
  opacity: 0.8;
}
/* over the 3D terrain an open stage's famous roads and climbs are named on
   poles (drawPoles in js/app_gl.js): the flat map's names for them go */
html .m3d-poled .climb-label.on-pole .climb-name,
html .m3d-poled .kom-dot .kom-name { display: none !important; }
.climb-label .climb-name {
  position: absolute;
  /* clear of the dot whatever its size, centred on its line */
  left: calc(var(--road-dot, 9px) / 2 + 4.5px); top: 0;
  transform: translateY(-50%);
  padding: 1px 5px;
  border-radius: 8px;
}
/* Zoomed out the dot alone marks the road, and the name is dropped — except on
   the ten roads each race is famous for, which are always named, and on whichever
   dot the mouse is over. */
body.labels-dot-only .climb-label .climb-name { display: none; }
body.labels-dot-only .climb-label.iconic .climb-name,
body.labels-dot-only .climb-label:hover .climb-name,
body.labels-dot-only .climb-label.hot .climb-name { display: inline-block; }
/* At the country view and further out, even the iconic ten per race are too
   many: only the marquee handful (Champs-Élysées, Alpe d'Huez, Ventoux,
   Tourmalet, Stelvio, Blockhaus, Etna, Angliru, Sierra Nevada) stay
   named. Leaflet stacks the shared marker pane by latitude, so without the
   z-index boost an unnamed iconic dot to the south would sit on the marquee
   names — a shown name always beats a bare dot (the you/search pins sit higher
   still, and the hover reveal higher than other names). */
body.labels-marquee-only .climb-label.iconic .climb-name { display: none; }
body.labels-marquee-only .climb-label.marquee .climb-name,
body.labels-marquee-only .climb-label:hover .climb-name,
body.labels-marquee-only .climb-label.hot .climb-name { display: inline-block; }
body.labels-marquee-only .climb-label.marquee { z-index: 5000 !important; }
/* At marquee zoom the whole country is in the frame and the map is mostly
   route line. The handful of names that survive come down a size. (The dots
   are sized by the zoom itself now - see .climb-dot - and the 44px tap halo
   is unchanged: it is unpainted, so the target stays finger-sized.) */
body.labels-marquee-only .climb-label .climb-name {
  padding: 1px 4px;
}
/* ...and on a phone the marquee roads with a short name use it there
   (MARQUEE_SHORT in js/data.js): Angliru, Tourmalet, Ventoux, Stelvio, Etna.
   From the iconic zoom in, and on a wider screen at any zoom, the full name
   (owner's call: only the phone's map is short of room). */
.climb-name .nm-short { display: none; }
@media (max-width: 699px) {
  body.labels-marquee-only .climb-name .nm-full { display: none; }
  body.labels-marquee-only .climb-name .nm-short { display: inline; }
}
/* One edition on the map: a road it did not ride keeps its dot, not its
   name (unless hovered) - see syncRoadNames */
.climb-label.unraced:not(:hover):not(.hot) .climb-name { display: none !important; }
/* ...and its dot is greyed out (the race colour is inline, hence !important) */
.climb-label.unraced .climb-dot { background: #a2a2a2 !important; border-color: #d4d4d4; }
/* ...and a road it did ride is named at every zoom, over the bare dots */
.climb-label.raced .climb-name { display: inline-block !important; }
.climb-label.raced { z-index: 5000 !important; }
/* Much further out than Europe, no road is named - not the marquee few, not
   an edition's raced roads: the dots alone (a hover still names one). */
body.labels-none .climb-label:not(:hover):not(.hot) .climb-name { display: none !important; }
/* From the iconic zoom in, a road is named only where the ranked placement
   put its name (.placed - placeRoadNames in app_gl.js): best-ranked first,
   clear of the other names, and no more than the view has room for. */
body.labels-ranked .climb-label .climb-name { display: none; }
body.labels-ranked .climb-label.placed .climb-name,
body.labels-ranked .climb-label:hover .climb-name,
body.labels-ranked .climb-label.hot .climb-name { display: inline-block; }
/* ...on the dot's left where the right had no room */
.climb-label.name-left .climb-name { left: auto; right: calc(var(--road-dot, 9px) / 2 + 4.5px); }
/* One edition's raced road whose name had no room by its dot: moved to the
   nearest clear spot (--nx/--ny, its top-left from the dot), with a thin
   line from the dot's edge to it (black on a light map, white on a dark one) (--la its angle, --l0 where it
   starts, --ll how long) - see placeRoadNames. */
.climb-label.moved .climb-name {
  left: var(--nx); top: var(--ny); right: auto;
  transform: none;
}
.climb-label.moved::after {
  content: "";
  position: absolute;
  left: 0; top: -0.5px;
  width: var(--ll); height: 1px;
  background: #000;
  transform-origin: 0 50%;
  transform: rotate(var(--la)) translateX(var(--l0));
  pointer-events: none;
}
/* (white wherever the map is dark: the dark theme's own map, or the
   app's alt look - darkMap in app_gl.js - never the full-colour one) */
html.tr-dark:not(.map-look-alt):not(.map-look-colour) .climb-label.moved::after,
html.map-look-alt:not(.tr-dark):not(.map-look-colour) .climb-label.moved::after { background: #fff; }
/* ...and one whose dot is off the map, or under the top bar, is not named
   until it comes into view - nor, zoomed out past the edition's framing,
   one with no room beside its dot (placeRoadNames) */
.climb-label.raced.offmap:not(:hover):not(.hot) .climb-name { display: none !important; }
/* (no line where the name is not shown) */
body.labels-none .climb-label.moved:not(:hover):not(.hot)::after,
.climb-label.offmap::after,
html .m3d-poled .climb-label.on-pole::after { display: none; }
/* the hidden copy the placement measures names on */
.climb-label.label-probe { position: absolute; left: -9999px; top: 0; visibility: hidden; pointer-events: none; }
/* ?labels: the placement's tuning panel and its boxes */
.label-tune {
  /* (under the top bar, clear of the sheet; folded to its heading until
     tapped, and scrolling when open rather than covering the map) */
  position: fixed; z-index: 400000; right: 12px; top: calc(env(safe-area-inset-top, 0px) + 72px);
  width: 250px; max-width: calc(100vw - 24px); padding: 10px 12px; border-radius: 10px;
  max-height: 55vh; overflow-y: auto; box-sizing: border-box;
  background: rgba(255,255,255,0.96); color: #1a1a1a; box-shadow: 0 2px 12px rgba(0,0,0,0.3);
  font-size: 12px; display: flex; flex-direction: column; gap: 7px;
}
.label-tune .lt-head { font-weight: 700; font-size: 13px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.label-tune .lt-sum { font-weight: 400; color: #555; }
.label-tune.folded { width: auto; padding: 6px 10px; }
.label-tune.folded > :not(.lt-head) { display: none; }
.label-tune .lt-x { border: 0; background: none; font-size: 18px; line-height: 1; cursor: pointer; color: inherit; }
.label-tune .lt-row { display: flex; flex-direction: column; gap: 2px; }
.label-tune .lt-row select, .label-tune .lt-row input { width: 100%; }
.label-tune .lt-check { display: flex; gap: 6px; align-items: center; }
.label-tune .lt-stats { color: #555; font-variant-numeric: tabular-nums; }
.label-tune .lt-sub { font-weight: 700; margin-top: 4px; border-top: 1px solid #ddd; padding-top: 6px; }
.label-tune .lt-reset { align-self: flex-start; font: inherit; padding: 2px 10px; cursor: pointer; }
.label-debug { position: absolute; inset: 0; pointer-events: none; z-index: 300000; }
.label-debug-box { position: absolute; box-sizing: border-box; border: 1px solid; }
.label-debug-box.placed { border-color: #16a34a; }
.label-debug-box.collides { border-color: #dc2626; background: rgba(220,38,38,0.25); }
.label-debug-box.budget { border-color: #6b7280; background: rgba(107,114,128,0.25); }
/* The opening draw: the famous roads wait for the lines, then their dots
   fade in and their names after them (revealRoads in app.js) */
body.intro-roads .climb-label .climb-dot,
body.intro-roads .climb-label .climb-name,
body.intro-names .climb-label .climb-name { opacity: 0; }
body.intro-roads .climb-label { pointer-events: none; }
body.intro-fade .climb-label .climb-dot,
body.intro-fade .climb-label .climb-name { transition: opacity 600ms ease; }
/* a name revealed on hover would otherwise sit under the markers south of it
   (.hot: the mouse is on the road's line close to its dot - see the hit
   line's mousemove) */
.climb-label.hot { z-index: 10000 !important; }
/* ...and lit, as its dot grows: the pill solid, ringed in its race's colour
   (--road-c, set on the name - see the climb labels in app_gl.js); a road
   the edition did not ride is ringed in its dot's grey */
.climb-label .climb-name { transition: box-shadow 0.12s ease-out, background-color 0.12s ease-out; }
html .climb-label.hot .climb-name,
html.tr .climb-label.hot .climb-name {
  background: #fff;
  box-shadow: 0 0 0 2px var(--road-c, #1a1a1a), 0 2px 6px rgba(0, 0, 0, 0.4);
}
html.tr .climb-label.hot .climb-name { background: rgb(var(--t-paper-rgb)); }
html.tr .climb-label.unraced.hot .climb-name { box-shadow: 0 0 0 2px #a2a2a2, 0 2px 6px rgba(0, 0, 0, 0.4); }
/* A famous road the open stage climbs: the climb's figures under its name
   while it is lit (setRoadClimb in app_gl.js), in the profile's chip and
   sizes (.kom-name), hanging below the pill so the name stays put */
.climb-label .climb-name .road-climb {
  display: none;
  position: absolute;
  left: 3px; top: calc(100% + 3px);
  white-space: nowrap;
  font-family: var(--t-body, sans-serif);
  font-size: 10px;
  font-weight: 600;
  line-height: 1.3;
  padding: 0 3px;
  border-radius: 3px;
  background: rgba(255, 255, 255, 0.92);
  color: #1a1a1a;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
  pointer-events: none;
}
html.tr .climb-label .climb-name .road-climb {
  background: rgba(var(--t-paper-rgb), 0.92);
  color: rgba(var(--t-ink-rgb), 0.92);
}
.climb-label .road-climb .elev-climb-derived { opacity: 0.6; }
@media (max-width: 699px) {
  .climb-label .climb-name .road-climb { font-size: 11px; }
}
.climb-label.hot .climb-name .road-climb { display: block; }
html .m3d-poled .climb-label.on-pole .road-climb { display: none; }
/* ...and under a mouse. Only a mouse: a phone's browser keeps :hover on
   whatever was last tapped, and a road's name tapped open, then tapped shut,
   stayed lit on the map (owner, 2026-10-02) - the same test as the app's
   canHover */
@media (hover: hover) and (pointer: fine) {
  .climb-label:hover { z-index: 10000 !important; }
  .climb-label:hover .climb-name .road-climb { display: block; }
  html .climb-label:hover .climb-name,
  html.tr .climb-label:hover .climb-name {
    background: #fff;
    box-shadow: 0 0 0 2px var(--road-c, #1a1a1a), 0 2px 6px rgba(0, 0, 0, 0.4);
  }
  html.tr .climb-label:hover .climb-name { background: rgb(var(--t-paper-rgb)); }
  html.tr .climb-label.unraced:hover .climb-name { box-shadow: 0 0 0 2px #a2a2a2, 0 2px 6px rgba(0, 0, 0, 0.4); }
}
/* The road's name over its line, away from the dot: the dot's own name pill,
   not Leaflet's bubble */
.leaflet-tooltip.road-tip {
  font-size: 11px;
  border: 0;
  padding: 1px 5px;
  border-radius: 8px;
}
.leaflet-tooltip.road-tip::before { display: none; }

/* stage-number pills — shown when the filter isolates one race's one year */
.stage-num-label {
  white-space: nowrap;
  cursor: pointer;
}
.stage-num-label .stage-num-pill {
  display: inline-block;
  transform: translate(-50%, -50%); /* centre the pill on the stage midpoint */
  text-align: center;
}
.stage-num-label:hover { z-index: 10000 !important; }

/* ---------- Bottom sheet ---------- */
.sheet {
  position: fixed;
  /* anchored to the viewport, NOT stretched by --vp-deficit: painting is
     clipped at the short viewport's edge, so shifting the sheet down only
     hides its last rows behind the clip (seen on-phone 2026-08-29) */
  left: 0; right: 0; bottom: 0;
  z-index: 950;
  border-radius: var(--sheet-radius) var(--sheet-radius) 0 0;
  max-height: 72vh;
  display: flex;
  flex-direction: column;
  transition: transform 0.25s ease;
}
/* interior clearances use --safe-env: with the sheet's bottom edge on the
   physical screen bottom (both modes end up there), the raw env inset is the
   indicator clearance; --safe-bottom would read 0 in vp-clipped mode */
.sheet.collapsed { transform: translateY(calc(100% - 96px - var(--safe-env))); }
.sheet.hidden-sheet { transform: translateY(100%); }
/* a phone's opening draw: the sheet out of sight at its rest (holdSheet) */
.sheet.sheet-held { visibility: hidden; }

.sheet-handle {
  padding: 12px 0 8px;
  cursor: grab;
  flex: none;
  touch-action: none;      /* drag the sheet instead of scrolling the page */
  user-select: none;
  -webkit-user-select: none;
}
.sheet-handle:active { cursor: grabbing; }
.sheet-handle span {
  display: block;
  width: 42px; height: 4px;
  margin: 0 auto;
  border-radius: 2px;
}

.view {
  overflow-y: auto;
  padding: 0 16px calc(20px + var(--safe-env));
  overscroll-behavior: contain; /* never chain a sheet scroll into the document */
  -webkit-overflow-scrolling: touch;
}
.view.hidden { display: none; }

.sheet-title {
  margin: 4px 0 4px;
  font-size: 19px;
}
.sheet-hint {
  margin: 0 0 10px;
  font-size: 13px;
}

/* nearby list */
.route-list, .stage-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.route-list li, .stage-list li {
  border-bottom: 1px solid #eee;
}
.route-item, .stage-item {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 12px 4px;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  cursor: pointer;
}

/* The near-you and tapped-point lists are the same row as the timeline under
   a famous road (.stage-lines below): one column, the race on top in its own
   colour, the thing you will open under it, then what that thing is. The
   coloured swatch that used to lead the row is gone with the layout — it said
   which race the road belongs to, and the line naming the race says it. */
.route-item .route-title {
  line-height: 1.3;
}
.route-item .route-sub {
  margin-top: 2px;
  font-size: 12.5px;
}
/* the distance badge, worn by both kinds of row in these lists (see distPill
   in js/app.js): a road's and a stage's say the same thing the same way */
.route-item .dist,
.stage-item .dist {
  margin-left: auto;
  flex: none;
  font-size: 13px;
  border-radius: 12px;
  padding: 3px 9px;
}
.route-item .dist.far,
.stage-item .dist.far { font-weight: 600; }
/* the pill and chevron sit level with the row's top line (the race line of a
   stage, the name of a road), not centred on however many lines follow it */
.route-list .dist,
.route-list .chev { align-self: flex-start; }
.route-item .chev {
  font-size: 18px;
}

/* "Show more" at the foot of the near-you list — same face as .filter-reset */
.route-list li.show-more-row { border-bottom: none; }
.show-more {
  display: block;
  width: 100%;
  margin: 10px 0 4px;
  font: inherit;
  font-size: 13px;
  padding: 9px 10px;
  cursor: pointer;
}

/* the tapped point's own coordinates, foot of the tapped-point panel */
.tap-coords {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 12px 0 0;
  font-size: 13px;
}
.tap-coords[hidden] { display: none; }
.tap-coords-label { font-weight: 600; }
.tap-coords-val {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  border-radius: 8px;
  padding: 3px 7px;
  user-select: all;
  -webkit-user-select: all;
}
.copy-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: none;
  padding: 0;
  border: 1px solid #ddd;
  border-radius: 8px;
  cursor: pointer;
}
.copy-btn svg {
  width: 15px;
  height: 15px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.copy-btn.copied {
  background: var(--tdf-yellow);
  color: var(--ink);
  border-color: #d9c400;
}

.linklike {
  border: none;
  background: none;
  padding: 0;
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}

/* route view */
.back-btn {
  cursor: pointer;
  /* a touch higher than its row would set it, in every pane and both skins:
     lifted where it stands, so nothing beside or below it moves */
  position: relative;
  top: -3px;
}
/* the road's name shares the back button's row, right-aligned; it drops to
   its own line (still right-aligned) when the two don't fit */
.route-topbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0 12px;
}
.route-topbar .back-btn { flex: none; }
/* Back names where it goes (app.js, backLabel): a long road or rider name is
   cut short on its one line rather than wrapping the button. A stage or an
   edition rides on its race's colour, a small version of the race pill. */
.back-btn {
  max-width: 100%;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.back-race {
  display: inline-block;
  border-radius: 10px;
  line-height: 1.35;
}
/* the road page's own title (.route-title is the nearby list's row name) */
.route-head-title {
  margin: 4px 0 0 auto;
  text-align: right;
}
.route-figs,
.route-meta {
  margin: 0 0 2px;
  font-size: 13px;
  text-align: right;
}
.route-meta { margin-bottom: 8px; }
/* the region, right-aligned (the climb-from label used to hold the other end
   of this row; it went down to the picker it names) */
.route-where {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0 10px;
}
.route-where .route-meta { margin-left: auto; }
.route-blurb {
  margin: 0 0 14px;
}
/* ---------- Approach picker (multi-sided climbs) ---------- */
.approach-picker {
  margin: 0 0 12px;
}
.approach-label {
  display: block;
  margin-bottom: 4px;
  font-size: 11.5px;
  text-transform: uppercase;
}
.approach-select-wrap {
  position: relative;
}
.approach-select {
  display: flex;
  align-items: center;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  background: var(--paper);
  border: 1.5px solid rgba(26, 26, 26, 0.18);
  padding: 6px 12px;
  cursor: pointer;
  appearance: none;
  padding-right: 32px;
}
.approach-select:focus {
  outline: none;
}
/* the button face and each menu row share one layout: start town left in
   ink, the grey stage count right-justified */
.approach-face {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
}
.approach-picker .face-from {
  font-size: 15px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.approach-picker .face-count {
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;
}
.approach-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 30;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--paper);
  border: 1.5px solid rgba(26, 26, 26, 0.18);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.14);
}
.approach-menu li {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 8px;
  border-radius: 7px;
  cursor: pointer;
}
.approach-menu li:hover,
.approach-menu li:focus {
  outline: none;
}
.approach-note {
  margin: 6px 0 0;
  font-size: 12.5px;
}

/* ---------- Elevation profile ---------- */
.elevation {
  margin: 0 0 14px;
}
.elevation .elev-chart {
  position: relative;
  touch-action: pan-y; /* compact thumbnails let vertical scrolling through */
}
/* the full chart owns the whole gesture: a held slide scrubs the route
   left/right and rides the map zoom up/down, so nothing is left to scroll */
.elevation:not(.compact) .elev-chart { touch-action: none; }
/* space above the profile box for the summit labels (only when there are any);
   the actual height is set inline per chart — one 15px lane per label row */
.elevation .elev-chart.has-annot { padding-top: 34px; }
.elevation { --elev-h: 96px; }
.elevation .elev-svg {
  display: block;
  width: 100%;
  height: var(--elev-h);
  border: 1px solid #e6e6e6;
  border-radius: 8px;
  cursor: crosshair;
}
/* compact variant for the race-panel stage list: shorter box, no hover cursor */
.elevation.compact { --elev-h: 54px; margin: 5px 0 0; }
.elevation.compact .elev-svg { cursor: default; }
.elevation .elev-cursor {
  position: absolute;
  top: 0;
  height: var(--elev-h);
  width: 1px;
  margin-left: -0.5px;
  pointer-events: none;
}
/* the dot mirrored onto the map at the point under the profile cursor: red
   with a white centre */
.elev-map-dot {
  box-sizing: border-box;
  border-radius: 50%;
  background: #e8352e radial-gradient(circle closest-side, #fff 0 40%, transparent 46%);
  border: 2px solid #fff;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}

.elevation .elev-dot {
  position: absolute;
  width: 9px;
  height: 9px;
  margin: -4.5px 0 0 -4.5px;
  border-radius: 50%;
  border: 2px solid #1a1a1a;
  pointer-events: none;
}
/* (the elev-tip used to need its own [hidden] rule here; the global one at the
   top of this file now covers every element, so it has been removed) */
.elevation .elev-tip {
  position: absolute;
  pointer-events: none;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 3px 8px;
  border-radius: 6px;
  background: rgba(26, 26, 26, 0.94);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.28);
  white-space: nowrap;
  line-height: 1.2;
}
.elevation .elev-tip-grad {
  font-size: 13px;
  font-weight: 700;
}
.elevation .elev-tip-sub {
  font-size: 11px;
  color: #d0d0d0;
}
/* summit / town annotations */
.elevation .elev-annot {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 3;
}
/* A chart drawn narrower than its row (the race panel scales each stage by
   distance) has empty space beside it, so a summit name longer than the box
   runs out into it instead of being clipped. The label placement keeps it
   inside the ROW, which is what the scroll container clips against. */
.elevation.scaled .elev-annot { overflow: visible; }
.elevation .elev-poi-line {
  position: absolute;
  top: 0;
  width: 1px;
  margin-left: -0.5px;
}
.elevation .elev-poi-dot {
  position: absolute;
  width: 6px;
  height: 6px;
  margin: -3px 0 0 -3px;
  border-radius: 50%;
}
.elevation .elev-poi-label {
  position: absolute;
  transform: translateX(-50%);
  white-space: nowrap;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.3;
  padding: 0 3px;
  border-radius: 3px;
}
/* A classified climb: its category on a small square with a point below
   it, the point's tip on the summit, coloured by category as a published
   stage profile marks its KOM points (HC darkest, a 4th-category côte
   palest). Their layer does not clip, so a summit finish's marker can stand
   on the very end of the box. .on lifts it while the cursor is there. */
.elevation .elev-climbs {
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 3;
}
.elevation .elev-climb,
.elevation .elev-km0 {
  position: absolute;
  box-sizing: border-box;
  min-width: 10px;
  height: 14px;
  padding: 0 1px 4px;
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  clip-path: polygon(0 0, 100% 0, 100% 10px, 50% 100%, 0 10px);
  font-size: 8.5px;
  font-weight: 800;
  line-height: 10px;
  text-align: center;
  letter-spacing: -0.2px;
  color: #fff;
  transition: transform 0.12s ease-out;
}
/* a marker lifted clear of its neighbour, back down to its summit */
.elevation .elev-climb-stem {
  position: absolute;
  width: 1px;
  margin-left: -0.5px;
}
.elevation .elev-climb.cat-HC { background: #1f1f1f; }
.elevation .elev-climb.cat-1 { background: #b3221f; }
.elevation .elev-climb.cat-2 { background: #df5a1b; }
.elevation .elev-climb.cat-3 { background: #e39a12; }
.elevation .elev-climb.cat-4 { background: #3c8f4a; }
.elevation .elev-climb.cat-x { background: #7a7a7a; }
/* km 0, the départ réel after the neutral roll-out: the climbs' marker in
   the selected route's blue, as narrow as theirs and a little taller - a
   small "km" stood over the "0" where a climb has its category */
.elevation .elev-km0 {
  background: #1e6bff;
  width: 10px;
  height: 20px;
  padding: 0 0 4px;
  clip-path: polygon(0 0, 100% 0, 100% 16px, 50% 100%, 0 16px);
  line-height: 9px;
  letter-spacing: 0;
}
.elevation .elev-km0 .elev-km0-km {
  display: block;
  font-size: 5.5px;
  line-height: 7px;
  padding-top: 1px;
  margin-bottom: -1px;
}
.elevation .elev-climb.on,
.elevation .elev-km0.on {
  transform: translateX(-50%) scale(1.3);
  z-index: 1;
}
/* the climb's name and summit altitude beside its marker, while the cursor
   is at the top: over the profile, as it is never there for long. The famous
   roads' label, word for word (.elev-poi-label) */
.elevation .elev-climb-tip {
  position: absolute;
  z-index: 4;
  transform: translateY(-50%);
  pointer-events: none;
  white-space: nowrap;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.3;
  padding: 0 3px;
  border-radius: 3px;
}
.elevation .elev-climb-tip.over { transform: translateY(-100%); }
/* a famous road's climb: the figures alone, hung under the road's label */
.elevation .elev-climb-tip.under { transform: none; }
/* the rest of the profile, dimmed while a climb's label is up */
.elevation .elev-dim { pointer-events: none; }
/* ...and the famous roads' labels over it (dimAround): about as faint as
   the profile is under the shade */
.elevation .elev-faded { opacity: 0.35; }
/* the second line: length · height gain · average gradient */
.elevation .elev-climb-tip .elev-climb-stats {
  display: block;
  font-weight: 600;
  opacity: 0.8;
}
/* A named climb's label is a button into that climb's page. The annotation
   layer is pointer-events:none so the profile scrub reads through it; the link
   labels opt back in, and they sit in the lane ABOVE the box, clear of the
   graph itself. */
.elevation .elev-poi-label.is-link {
  pointer-events: auto;
  cursor: pointer;
  border: 0;
  margin: 0;
  font: inherit;
  font-size: 10px;
  font-weight: 700;
  line-height: 1.3;
  text-decoration: none;
  -webkit-tap-highlight-color: transparent;
}
/* a touch larger on a phone, where 10px is a squint. The lane packing's
   width guess (POI_CHAR_W in app.js / app_gl.js) follows this size */
@media (max-width: 699px) {
  .elevation .elev-poi-label,
  .elevation .elev-poi-label.is-link,
  .elevation .elev-climb-tip { font-size: 11px; }
}
/* A race ridden in laps: faint dashed lines either side of the lap section,
   and between them, at the top of the profile box, a toggle that draws the lap once
   ("collapse laps") or every time ("12 laps"). */
.elevation .elev-lap-mark {
  position: absolute;
  width: 0;
  border-left: 1px dashed rgba(26, 26, 26, 0.3);
  margin-left: -0.5px;
}
.elevation .elev-lap-toggle {
  position: absolute;
  transform: translateX(-50%);
  pointer-events: auto;
  cursor: pointer;
  white-space: nowrap;
  border: 0;
  margin: 0;
  padding: 0 3px;
  border-radius: 3px;
  font: inherit;
  font-size: 10px;
  font-weight: 600;
  line-height: 1.3;
  -webkit-tap-highlight-color: transparent;
}
/* the arrows from the dashed lines in towards "collapse laps": dashed like
   the lines they leave, with a small head at the text end */
.elevation .elev-lap-arrow {
  position: absolute;
  height: 0;
  border-top: 1px dashed rgba(26, 26, 26, 0.3);
}
.elevation .elev-lap-arrow::after {
  content: "";
  position: absolute;
  top: -3px;
  border: 2.5px solid transparent;
}
.elevation .elev-lap-arrow.to-right::after {
  right: -1px;
  border-left: 3.5px solid rgba(26, 26, 26, 0.4);
  border-right: 0;
}
.elevation .elev-lap-arrow.to-left::after {
  left: -1px;
  border-right: 3.5px solid rgba(26, 26, 26, 0.4);
  border-left: 0;
}
/* the one-lap chart's close button, top right inside its box */
.elevation .elev-close {
  position: absolute;
  right: 5px;
  width: 18px;
  height: 18px;
  padding: 0;
  margin: 0;
  border: 0;
  border-radius: 50%;
  pointer-events: auto;
  cursor: pointer;
  font: inherit;
  font-size: 15px;
  line-height: 18px;
  text-align: center;
  -webkit-tap-highlight-color: transparent;
}
.elevation .elev-close:hover,
.elevation .elev-close:focus-visible {
  color: var(--ink);
}
.elevation .elev-lap-toggle:hover,
.elevation .elev-lap-toggle:focus-visible {
  color: var(--ink);
  text-decoration: underline;
}
/* the one-lap chart drawn under a collapsed race: named top left in its box,
   level with the × opposite */
.elevation .elev-box-label {
  position: absolute;
  left: 5px;
  font-size: 10px;
  font-weight: 700;
  line-height: 18px;
  padding: 0 3px;
  border-radius: 3px;
  white-space: nowrap;
}
.elevation .elev-poi-label.anchor-left { transform: none; }
.elevation .elev-poi-label.anchor-right { transform: translateX(-100%); }
.elevation .elev-cap {
  display: flex;
  /* baseline, not the flex default (stretch): the ascent figure leads with a
     ▲, which SF Pro on iOS doesn't carry, so it falls back to a symbol face
     with taller metrics. A stretched item puts its text at the top of its own
     line box, and that taller box pushed the ascent a few px below the
     distance and the altitude range. Aligning on the baseline pins all three
     to the same line whatever font the glyph comes from. */
  align-items: baseline;
  gap: 14px;
  margin-top: 5px;
  font-size: 12px;
  font-weight: 600;
}
/* the race date shares the caption row, pushed to the right-hand end; if the
   row is too narrow for both it wraps onto its own line, still right-aligned */
.elevation .elev-cap { flex-wrap: wrap; }
.elevation .elev-cap-date { margin-left: auto; white-space: nowrap; }
.stages-heading {
  margin: 0 0 4px;
  font-size: 13px;
  text-transform: uppercase;
}
.stage-item .year {
  flex: none;
  width: 52px;
  font-weight: 800;
  font-size: 16px;
  /* fallback only — the real colour is set inline per race from
     RACE_META[race].textColor in js/app.js */
  color: var(--ink);
}
/* ---------- A famous road's timeline row ----------
   One column, four lines: the edition, the stage, who won it, the story. The
   year used to sit in a 52px column of its own with a route squiggle beside
   it; naming the race on the same line says more than the squiggle did, and
   the width that frees is what lets the stage line stay on one row.
   (.stage-item .year above still dresses the nearby and rider lists.) */
.stage-lines {
  display: flex;
  flex-direction: column;
  gap: 1px;
  min-width: 0;
  flex: 1;
}
.stage-edition {
  /* a point and a half over the lines below it: this is the loud line of the
     row, and at 13.5 it was not winning against them */
  font-size: 15px;
  /* fallback only — the race colour is set inline per row from
     RACE_META[race].textColor, and only when we hold the stage's route */
  color: var(--ink);
}
/* an appearance we can name but not draw: no race colour to spend on it */
.stage-edition.no-route { font-weight: 700; }
/* greyed because the stage climbed a side other than the one the approach
   picker is showing (dimApproachIcons) */
.stage-edition.dim { opacity: 0.45; }
/* a multi-sided climb's row: the side the stage climbed, set right on the
   edition line, quieter than the race's name */
.stage-edition.has-side {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 10px;
}
.stage-edition .stage-side {
  flex: none;
  max-width: 55%;
  text-align: right;
  font-size: 13px;
  font-weight: 400;
  text-transform: none;
  letter-spacing: 0;
  color: #888;
}
.stage-story {
  margin-top: 3px;
}
/* A stage's row is set the same in every list - near you, near a tapped
   point, a road's timeline and a race's stage list (the owner's call,
   2026-10-07): the towns at 14px, the grey "STAGE 6:" before them a size
   under (.race-stage-no). The smaller label buys back the width the towns
   took from 13px, so a road's rows still hold one line on a phone. */
.stage-item .stage-route,
.race-stage-name {
  font-size: 14px;
  line-height: 1.3;
}
.stage-item .stage-winner-mini {
  font-size: 12.5px;
  margin-top: 2px;
}
.stage-item .chev {
  margin-left: auto;
  font-size: 18px;
}



/* stage view */
/* stage view header: Back link left, race/stage/distance pill right.
   Wraps the pill onto its own (still right-aligned) line when it can't fit
   beside Back, so it stays a clean single-line pill on narrow screens. */
.stage-topbar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}
.stage-topbar .back-btn {
  flex: none;
  /* This row is as tall as Back itself, so Back already sits at the top of
     the panel and the lift above has nowhere to go: it took 3px off the top
     of the button, which the view's scroll edge cut away. */
  top: 0;
}
.stage-year-chip {
  margin-left: auto;
  font-size: 14px;
}
/* the chip's race name + year open the edition's race panel: nothing says so until a pointer
   is over it */
.stage-race.is-link { cursor: pointer; }
@media (hover: hover) {
  .stage-race.is-link:hover { opacity: 0.75; }
}
.stage-winner {
  margin: 2px 0 10px;
  font-size: 13px; /* match the GC-leader line beside it */
  font-weight: 700;
}
.stage-winner::before { content: "🏆 "; }
/* winner + GC leader share one line; leader is bold like the winner but a step
   smaller so long names still fit beside it, wrapping only when they must */
.stage-credits {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  column-gap: 14px;
  row-gap: 2px;
  margin: 2px 0 10px;
}
.stage-winner { margin: 0; }
.stage-leader {
  display: flex;
  align-items: center;
  gap: 5px;
  margin: 0;
  font-size: 13px;
  font-weight: 700;
}
.stage-leader .jersey-icon { flex: none; }

/* ---------- Race view (S4) ---------- */
.race-block { margin-bottom: 22px; }
.race-sub-title {
  margin: 0 0 6px;
  font-size: 16px;
}
/* race-panel heading pill — race-coloured, mirrors the stage-header chip.
   background + text colour are set inline per race (racePillHtml in app.js). */
.race-pill {
  display: inline-block;
  font-size: 15px;
  line-height: 1.35;
}
/* The edition panel's header row, built like the stage view's: Back on the
   left, the edition's name pushed to the right-hand end, wrapping onto its
   own (still right-aligned) line when the two don't fit. */
.race-topbar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 10px;
}
.race-topbar .back-btn { flex: none; }
.race-title {
  margin: 4px 0 10px auto;
  text-align: right;
}
.race-intro {
  margin: 0 0 12px;
}
.race-jerseys { margin: 0 0 14px; }
.jersey-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 3px 0;
  font-size: 14px;
}
.jersey-row .jersey-icon { flex: none; }
.jersey-cls {
  flex: none;
  width: 74px;
  font-weight: 700;
  text-transform: uppercase;
  color: var(--ink-soft);
}
/* rider + team on one line; the team is clipped with … rather than wrapping */
.jersey-rider {
  font-weight: 600;
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.jersey-rider .rteam,
.race-gc .rteam {
  margin-left: 6px;
  font-weight: 400;
  color: var(--ink-soft);
}
/* Final-GC list: one compact line per rider (team inline, not stacked), the
   time-from-the-winner pinned right, long rider+team clipped with … . The
   `ol.` prefix outweighs the later `.result-list .rteam { display:block }`. */
ol.race-gc { margin: 4px 0 10px; }
ol.race-gc li { padding: 2px 0; font-size: 13px; }
ol.race-gc .rider {
  flex: 1;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
ol.race-gc .rteam { display: inline; }
.race-gc-heading,
.race-stages-heading {
  margin: 4px 0 6px;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-soft);
}
.race-gc { margin: 0 0 8px; }
.race-stage-list { list-style: none; margin: 0; padding: 0; }
.race-stage-list li { border-bottom: 1px solid var(--line); }
.race-stage-item {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  padding: 9px 2px;
  cursor: pointer;
  font: inherit;
  color: inherit;
}
/* a stage we have no route for: still a full row (name, winner, GC leader) and
   still opens its summary, but it draws nothing on the map and has no profile.
   The dashed rule down the left says "there's a gap here" without shouting. */
.race-stage-item.no-route {
  border-left: 2px dashed var(--line);
  padding-left: 8px;
}
.race-stage-item.no-route .race-stage-name { font-weight: 600; }
.race-stage-item.no-route .race-stage-no,
.race-stage-item.no-route .race-stage-name { color: var(--ink-soft); }
/* the grey "STAGE 2:" label is set in capitals, a size under the towns,
   wherever it appears: the race view's stage list, the road page's stage
   lines and the near-you rows */
.race-stage-no {
  text-transform: uppercase;
  font-size: 0.86em;
}
/* the compact profile is a static thumbnail — let clicks (and the pointer
   cursor) fall through to the row so the whole item, chart included, is one
   clickable target down to the bottom of the graph */
.race-stage-item .elevation.compact { pointer-events: none; }
.race-stage-head {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  align-items: baseline;
}
.race-stage-km { flex: none; font-size: 12px; }
/* stage winner + GC leader share one line, like the single-stage header */
.race-stage-credits {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  column-gap: 12px;
  row-gap: 1px;
  margin-top: 1px;
}
.race-stage-win { font-size: 12px; font-weight: 400; } /* match the GC-leader beside it */
.race-stage-gc {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 12px;
  font-weight: 400;
}
/* the day the stage was raced — a quiet line between the profile and the
   summary, hidden on the stages whose date isn't known */
.stage-date {
  margin: 0 0 6px;
  font-size: 12px;
  font-weight: 600;
}
.stage-date[hidden] { display: none; }
.stage-summary {
  margin: 0;
}

/* stage result (top 10) */
#stage-results { margin-top: 16px; }
.result-list {
  margin: 6px 0 4px;
  padding: 0;
  list-style: none;
}
.result-list li {
  display: flex;
  gap: 10px;
  align-items: baseline;
  padding: 5px 0;
  border-bottom: 1px solid #f0f0f0;
  font-size: 14px;
}
.result-list li.hidden-row { display: none; }
.result-list .rank {
  flex: none;
  width: 22px;
  font-weight: 800;
}
.result-list .rider { font-weight: 600; }
.result-list .rteam {
  font-size: 12px;
  display: block;
}
.result-list .rtime {
  margin-left: auto;
  flex: none;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
.result-toggle { margin-top: 8px; font-size: 13.5px; }

/* non-clickable appearance rows in the road timeline */
.appearance-row {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 9px 4px;
  font-size: 13px;
}

@media (min-width: 700px) {
  .sheet {
    left: auto;
    right: 16px;
    width: 400px;
    bottom: 16px;
    border-radius: var(--sheet-radius);
    /* up to just under the map's buttons (app.js measures them into
       --sheet-max-desk); 70vh until it has */
    max-height: var(--sheet-max-desk, 70vh);
  }
  .sheet.collapsed { transform: translateY(calc(100% - 80px)); }
}

/* ---------- Rider view ---------- */
/* a rider's name leads the filter button, so it has to give way before the
   title and search button do */
.year-btn {
  max-width: 52vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the rider's name sits beside Back, like a stage's chip does - and when
   the two don't fit (Back names where it goes, and "Tour 2007 · Prologue"
   leaves a long name little room) the name drops to its own line, still
   right-aligned, the way a road's or an edition's does. Never cut short:
   it is the page's title. A name too long for a line of its own wraps. */
.rider-topbar { flex-wrap: wrap; gap: 4px 12px; }
.rider-title {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 0;
  margin: 0 0 0 auto; /* the right-hand end, where a stage keeps its chip */
  text-align: right;
}
/* the near-here panel's heading shares Back's row the same way, at the
   right-hand end, dropping to its own right-aligned line when it can't fit */
.here-topbar { gap: 4px 12px; }
.here-title {
  min-width: 0;
  margin: 4px 0 4px auto;
  text-align: right;
}
/* the row is only as tall as Back, so what comes under it needs air of its own */
.here-topbar + .sheet-hint,
.here-topbar + .fbar,
.here-topbar + .fbar[hidden] + .sheet-hint { margin-top: 10px; }
/* What the filters cut a near-you / near-here list down to, and the way out
   (renderFilterBar in js/app_gl.js): a rider's line, then everything else's. */
.fbar {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin: 0 0 10px;
}
.fbar[hidden] { display: none; }
.fbar-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 6px 10px;
}
.fbar-text {
  display: inline-flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 6px;
  min-width: 0;
  font-size: 13px;
}
.fbar-tag {
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 12px;
  font-weight: 600;
  background: rgba(127, 127, 127, 0.18);
}
.fbar-btn {
  margin-left: auto;
  font: inherit;
  font-size: 12px;
  padding: 6px 10px;
  cursor: pointer;
  white-space: nowrap;
}
.rider-flag, .search-flag, .rider-chip-flag, .result-flag {
  flex: none;
  border-radius: 2px;
  object-fit: cover;
}
.rider-flag { width: 24px; height: 18px; }
.search-flag { width: 16px; height: 12px; }
.rider-chip-flag { width: 14px; height: 10.5px; }
/* after a rider's name in a result list, in place of the "(SUI)" code the
   Tour's sheets carry and the other races' do not */
.result-flag {
  display: inline-block;
  width: 14px; height: 10.5px;
  margin-left: 5px;
  vertical-align: -1px;
}
/* wins / podiums / top 10s — what the map shows, one at a time */
.rider-tiers {
  display: flex;
  gap: 6px;
  margin: 12px 0 8px;
}
.rider-tier {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 12.5px;
  font-weight: 700;
  line-height: 1.2;
  padding: 3px 4px;
  border-radius: 12px;
  cursor: pointer;
}
.rider-tier:disabled { opacity: 0.45; cursor: default; }
.rider-tier-n {
  font-size: 11.5px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
/* a chip's classifications, after its stage results: "12, [jersey] 3" */
.rider-tier-n + .rider-tier-jn { margin-left: -3px; }
.rider-tier-jn {
  display: inline-flex;
  align-items: center;
  gap: 2px;
}
.rider-tier-jersey { flex: none; }
/* a rider's results: one line each — place, year, race, stage, route */
.rider-result {
  display: flex;
  align-items: baseline;
  gap: 10px;
  width: 100%;
  padding: 5px 4px;
  border: none;
  background: none;
  text-align: left;
  font: inherit;
  font-size: 14px;
  cursor: pointer;
}
.rr-place {
  flex: none;
  width: 30px;
  font-size: 12.5px;
  font-weight: 700;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}
.rr-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rr-race { font-weight: 700; }
.rider-result .chev { margin-left: auto; font-size: 16px; flex: none; }
.rider-filtered { margin-top: -4px; }
/* the rider in the filter pane, with the ✕ that ends their view */
/* the rider rides in the heading row, just left of Reset (which stays put
   at the right-hand end); a long name trims before it wraps */
.filter-head-row:has(.rider-filter:not([hidden])) { gap: 6px; }
.rider-filter { min-width: 0; margin-left: auto; display: flex; }
/* as tall as Reset beside it: the row is as tall as Reset, and the chip
   fills it */
.filter-head-row .rider-filter { align-self: stretch; }
.filter-head-row .rider-chip { box-sizing: border-box; height: 100%; }
.rider-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  max-width: 100%;
  border: 1.5px solid #e0b93a;
  border-radius: 12px;
  padding: 1px 2px 1px 7px;
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  cursor: pointer;
}
.rider-chip-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.rider-chip-x {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px; height: 18px;
  border-radius: 50%;
  font-size: 10px;
}
/* a rider races one sex's events: nothing for the men/women chips to pick */
body.rider-mode .sex-chip { display: none; }
/* the Worlds column squeezed in beside the Grand Tours in a rider's grid:
   the flag alone, centred */
body.rider-mode .yg-cell.race-wc .yg-flag { margin-right: 0; }
/* who the rider is, between their name and the results chips */
.rider-bio {
  margin: 10px 0 0;
  font-size: 13.5px;
  line-height: 1.45;
}
/* a final classification in a rider's list: its jersey, and a name that
   reads as the edition's result rather than a stage's */
.rider-class .jersey-icon { vertical-align: -2px; margin-right: 3px; }
.rider-class .rr-cls { font-weight: 600; }

/* ---------- A famous road in 3D (road page) ---------- */
.road3d { position: relative; margin: 0 0 10px; }
.road3d-canvas {
  /* The gridded floor, the road's track on it, the north arrow and the
     cursor's stalk are painted into a CANVAS, which no stylesheet can reach.
     So their ink is declared here instead and read back by road3d in
     js/app.js: channels, and a multiplier for the alphas it draws them at.
     Black on white paper; a skin over a dark panel moves both. */
  --road3d-ink: 0, 0, 0;
  --road3d-ink-alpha: 1;
  display: block;
  width: 100%;
  height: 250px;
  border: 1px solid #e6e6e6;
  border-radius: 8px;
  /* a sideways drag turns the road; up/down still scrolls the sheet */
  touch-action: pan-y;
  cursor: grab;
}
.road3d-canvas.dragging { cursor: grabbing; }
.road3d-note {
  position: absolute;
  right: 8px;
  bottom: 6px;
  font-size: 10px;
  font-weight: 600;
  pointer-events: none;
}
/* the 3D toggle, at the right-hand end of the road profile's caption */
.elevation .elev-3d-btn {
  margin-left: auto;
  padding: 2px 8px;
  border: 1px solid #ccc;
  border-radius: 5px;
  font: inherit;
  font-size: 11px;
  font-weight: 700;
  line-height: 1.4;
  cursor: pointer;
}

/* iOS data detectors: Safari wraps anything it reads as a place, a date or a
   phone number in its own <a>, so "Bourg-d'Oisans → Alpe d'Huez" in a stage
   list came out iOS blue and underlined where Android drew it black. The
   format-detection meta in index.html asks it not to; where it does anyway,
   the link it injects carries x-apple-data-detectors, so take the colour back.
   -webkit-text-fill-color as well as color: iOS paints the blue through the
   fill, which wins over color on its own. */
a[x-apple-data-detectors],
a[x-apple-data-detectors] * {
  color: inherit !important;
  -webkit-text-fill-color: inherit !important;
  text-decoration: none !important;
  font-size: inherit !important;
  font-weight: inherit !important;
}
