/* Self-hosted typefaces (2026-09-15): the latin subsets Google Fonts served,
   OFL 1.1, one variable file each. unicode-range keeps a page in another
   script from ever asking for them; swap because the figures on Stats are
   drawn on first paint and a fallback face beats an invisible number. */
@font-face {
  font-family: 'Instrument Sans';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/instrument-sans-latin.e35fad09c70b.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: 'JetBrains Mono';
  font-style: normal;
  font-weight: 100 800;
  font-display: swap;
  src: url("fonts/jetbrains-mono-latin.570751c5f8b4.woff2") format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* The Lines Project V2 — "Nightfall v2" design system (docs/V2-DESIGN.md).
 * Every value here is a token from the handoff; the classes are the
 * vocabulary the v2/ templates speak. One breakpoint, 720px.
 */
:root {
  --ground:#09090b; --map:#0c0c0f; --surface:#111114; --raised:#15151a; --sunken:#0f0f13;
  --hair:rgba(255,255,255,.08); --hair-strong:rgba(255,255,255,.14); --hair-faint:rgba(255,255,255,.06);
  --fg:#f4f4f5; --fg2:#a1a1aa; --muted:#71717a; --faint:#52525b;
  --red:#ff2d2d; --red-hover:#ff4444; --red-link:#ff5a52; --red-deep:#7a1010; --red-deep2:#5c1414;
  --ok:#3ddc84; --warn:#ffb020; --info:#4f9dff; --gold:#ffd700;
  --chrome:#3f3f46; --chrome2:#2a2a32; --chrome3:#26262c;
  --strava:#fc4c02; --strava-link:#fc5200; --rwgps:#d64227; --garmin:#0a7ec2;
  --sans:'Instrument Sans',ui-sans-serif,system-ui,sans-serif;
  --mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
  --r-tag:6px; --r-btn:10px; --r-lg:12px; --r-row:14px; --r-card:16px;
  --ease:cubic-bezier(.2,.7,.2,1);
}
*{box-sizing:border-box}
html,body{margin:0;background:var(--ground);color:var(--fg);font:15px/1.5 var(--sans);color-scheme:dark;-webkit-font-smoothing:antialiased}
a{color:var(--fg);text-decoration:none}a:hover{color:var(--red-link)}
button,input,select,textarea{font-family:inherit;font-size:inherit}
h1,h2,h3,p{margin:0}
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
@keyframes fade{from{opacity:0}to{opacity:1}}
@keyframes draw{from{stroke-dashoffset:1200}to{stroke-dashoffset:0}}
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:.4;transform:scale(.7)}}
@keyframes glow{0%,100%{opacity:.55}50%{opacity:1}}
@keyframes slide{from{transform:translateX(-30%)}to{transform:translateX(130%)}}
@keyframes ping{0%{transform:scale(.4);opacity:.55}100%{transform:scale(1);opacity:0}}
@keyframes spin{to{transform:rotate(360deg)}}
/* The line reveal. It pairs with pathLength="1" stroke-dasharray="1" on the
 * path, so one dash is exactly the whole line whatever its real length —
 * `draw` above (1200→0) is only honest against a path whose length has been
 * measured, and the handoff's mistake #2 was a 2000/6000 dash against paths
 * nobody had measured, which starts the reveal mostly drawn. */
@keyframes drawLine{from{stroke-dashoffset:1}to{stroke-dashoffset:0}}
@keyframes markPop{from{transform:scale(0)}to{transform:scale(1)}}
.rise{animation:rise .5s var(--ease) both}
/* Reduced motion (handoff README § Motion) — half of it. This neutralises
 * every keyframe and transition and draws every dashed reveal in full; the
 * other half is v2.js's count-up, which checks the same query and sets its
 * scalar straight to 1, because CSS cannot change a number a script writes.
 * ⚠️ The ONE @media in this file that is not the 720px breakpoint;
 * v2.tests.ResponsiveTests names it and refuses any other. */
@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{animation-duration:.001ms!important;animation-delay:0ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important}
  [stroke-dasharray]{stroke-dashoffset:0!important}
}
:focus-visible{outline:2px solid var(--red);outline-offset:2px}

/* ---- type ---- */
.title{font-size:32px;font-weight:600;letter-spacing:-.03em;line-height:1.1}
.sub{color:var(--fg2);margin-top:8px;max-width:640px;text-wrap:pretty}
.eyebrow{font-size:12px;letter-spacing:.14em;color:var(--red);font-weight:600;text-transform:uppercase}
.caps{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:500}
.mono{font-family:var(--mono)}
.muted{color:var(--muted)}.dim{color:var(--fg2)}.faint{color:var(--faint)}
.ok{color:var(--ok)}.warn{color:var(--warn)}.bad{color:var(--red-link)}
.fine{font-size:12px;color:var(--faint);line-height:1.5}
.check{display:flex;gap:10px;align-items:flex-start;font-size:13.5px;line-height:1.5;color:var(--fg2);cursor:pointer}
.check input{flex:none;width:18px;height:18px;margin:1px 0 0;accent-color:var(--red);cursor:pointer}
.check a{color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.check .sender{display:block;margin-top:4px;font-size:12.5px;color:var(--fg2)}
.consent-notice{margin-top:20px;padding:14px 16px;background:var(--surface);border:1px solid var(--hair);border-radius:12px;font-size:13.5px;line-height:1.55;color:var(--fg2)}
.consent-notice p+p{margin-top:8px}.consent-notice strong{color:var(--fg);font-weight:600}
.consent-notice a{color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.consent-banner{position:fixed;left:16px;right:16px;bottom:16px;z-index:60;max-width:560px;margin:0 auto;padding:16px 18px;background:var(--raised);border:1px solid var(--hair-strong);border-radius:var(--r-card);box-shadow:0 12px 40px rgba(0,0,0,.5);font-size:13.5px;line-height:1.55;color:var(--fg2)}
.consent-banner[hidden]{display:none}
.consent-banner a{color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.consent-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:12px}
.small{font-size:12.5px;color:var(--muted)}
h2.h{font-size:16px;font-weight:600;margin:24px 0 12px}
h2.hc{font-size:15px;font-weight:600;margin:0 0 12px}
.num{font-family:var(--mono);font-weight:500;letter-spacing:-.02em}
.num.xl{font-size:26px}.num.lg{font-size:22px}.num.md{font-size:18px}
.unit{font-size:12px;color:var(--muted);margin-left:4px;font-family:var(--sans);font-weight:400}

/* ---- surfaces ---- */
.card{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card)}
.card.pad{padding:22px}.card.pad-c{padding:16px 18px}
.card.ok-b{border-color:rgba(61,220,132,.25)}.card.warn-b{border-color:rgba(255,176,32,.25)}.card.red-b{border-color:rgba(255,45,45,.3)}
.well{background:var(--map);border:1px solid var(--hair-faint);border-radius:var(--r-lg);padding:12px 14px}
.tile{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);padding:18px 20px;min-width:0}
.tile .l{font-size:12px;color:var(--fg2)}
.tile .v{font-family:var(--mono);font-size:26px;font-weight:500;letter-spacing:-.02em;margin-top:6px}
.tile .n{font-size:12px;color:var(--muted);margin-top:2px}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:14px}
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.detail{display:grid;grid-template-columns:minmax(0,1fr) 300px;gap:20px;align-items:start}
/* ⚠️ A grid item's automatic minimum is its CONTENT width, so one
 * `white-space:nowrap` row in a sidebar card — an activity name under
 * "Recent activities here", the "View on Strava" link — makes the whole
 * column that wide, and on a phone the map and every card beside it run
 * off the right edge of the screen while `overflow-x:hidden` hides the
 * scroll. Measured 2026-09-15: one nowrap row took the activity map from
 * 343px to 647px on a 375px screen. The desktop never showed it because
 * the sidebar is a fixed 300px column there. `min-width:0` on every item,
 * not on the two that happened to carry it inline. */
.detail>*{min-width:0}
.statnum{font-family:var(--mono);font-size:54px;font-weight:500;letter-spacing:-.05em;line-height:1;margin:10px 0 8px;font-variant-numeric:tabular-nums}
.tile.stat{border-radius:14px;padding:16px 18px}
.tile.stat .v{margin-top:4px;font-variant-numeric:tabular-nums}
.tile.stat .unit{font-size:13px}
.stack{display:flex;flex-direction:column;gap:14px}
.between{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:16px 24px}
.hr{height:1px;background:var(--hair);margin:16px 0}
.hidden{display:none !important}

/* ---- controls ---- */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:var(--r-btn);padding:9px 14px;font-size:13px;cursor:pointer;border:1px solid var(--hair-strong);background:transparent;color:var(--fg);transition:border-color .18s,background .18s,color .18s;white-space:nowrap;text-decoration:none}
.btn:hover{border-color:rgba(255,255,255,.35);color:var(--fg)}
.btn.primary{background:var(--red);border-color:var(--red);color:#fff;font-weight:600}
.btn.primary:hover{background:var(--red-hover);border-color:var(--red-hover)}
.btn.light{background:var(--fg);border-color:var(--fg);color:var(--ground);font-weight:600}
.btn.light:hover{background:#fff}
.btn.danger{background:rgba(255,45,45,.12);border-color:rgba(255,45,45,.4);color:var(--red-link)}
.btn.danger:hover{background:rgba(255,45,45,.2)}
.btn.pill{border-radius:999px}
.btn.lg{padding:13px 22px;font-size:15px;border-radius:999px}
.btn.wide{width:100%;padding:13px 16px;font-size:14px;border-radius:var(--r-lg)}
.btn.glow{box-shadow:0 0 0 1px rgba(255,45,45,.4),0 12px 32px -8px rgba(255,45,45,.6)}
.btn.sm{padding:7px 11px;font-size:12.5px;border-radius:8px}
.btn.xs{padding:3px 8px;font-size:11.5px;border-radius:7px;vertical-align:baseline}
/* The Home base typeahead. The list is absolutely positioned over whatever
   follows, so adding it moved nothing on the page. */
.combo{position:relative}
.combo-list{position:absolute;z-index:40;left:0;right:0;top:calc(100% + 4px);margin:0;padding:4px;list-style:none;background:var(--raised);border:1px solid var(--hair-strong);border-radius:var(--r-lg);box-shadow:0 18px 40px rgba(0,0,0,.5);max-height:264px;overflow:auto;scrollbar-width:thin}
.combo-list li{display:flex;align-items:baseline;justify-content:space-between;gap:12px;padding:9px 11px;border-radius:8px;cursor:pointer;font-size:13.5px}
.combo-list li i{font-style:normal;font-size:11.5px;color:var(--faint);font-family:var(--mono);flex:0 0 auto}
.combo-list li.on,.combo-list li:hover{background:rgba(255,255,255,.07)}
.combo-list li.on{outline:1px solid rgba(255,45,45,.35);outline-offset:-1px}
/* A note that is a caution rather than a fact. */
.fine.warn{color:var(--warn)}
.btn.icon-only{padding:0;border:0;background:none;line-height:0;border-radius:8px}
.btn[disabled],.btn[aria-disabled="true"],.is-disabled{opacity:.45;cursor:not-allowed;pointer-events:none}
/* The submit button v2.js's one-submission guard is holding because it was
 * PRESSED: being acted on, which is a different fact from unavailable, so it
 * keeps its full colour (this rule ties `.btn[disabled]` on specificity and is
 * written after it) and gains a spinner. Set only by that guard. */
.btn[data-held="pressed"]{opacity:1}
[data-held="pressed"]::after{content:"";display:inline-block;flex:0 0 auto;box-sizing:border-box;width:12px;height:12px;vertical-align:-1px;border-radius:50%;border:2px solid currentColor;border-top-color:transparent;animation:spin .9s linear infinite}
.link[data-held="pressed"]::after{margin-left:6px}
.link{background:none;border:0;color:var(--fg2);font:inherit;font-size:13px;cursor:pointer;padding:0;text-decoration:underline;text-underline-offset:3px}
.link:hover{color:var(--fg)}
.input,.select{width:100%;background:var(--ground);border:1px solid rgba(255,255,255,.1);color:var(--fg);border-radius:var(--r-btn);padding:10px 12px;font-size:14px;outline:none}
.input.dark{background:var(--surface);padding:12px 14px}
.input:focus,.select:focus{border-color:var(--red);box-shadow:0 0 0 3px rgba(255,45,45,.2);outline:none}
.select{width:auto;padding:8px 10px;font-size:13px}
label.lab{display:block;font-size:12px;color:var(--fg2);margin-bottom:6px}
.seg{display:inline-flex;background:rgba(255,255,255,.04);border-radius:var(--r-btn);padding:3px;font-size:13px;white-space:nowrap;max-width:100%;overflow-x:auto;scrollbar-width:none}
.seg a,.seg button{padding:7px 14px;border-radius:8px;color:var(--fg2);border:0;background:transparent;cursor:pointer;text-decoration:none}
.seg .on{background:var(--fg);color:var(--ground);font-weight:600}
.chips{display:inline-flex;background:var(--ground);border:1px solid rgba(255,255,255,.1);border-radius:999px;padding:3px}
.chips button{border:0;padding:8px 16px;border-radius:999px;cursor:pointer;font-size:13px;font-weight:500;background:transparent;color:var(--fg2);transition:background .2s}
.chips .on{background:var(--red);color:#fff}
.tag{display:inline-block;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;border-radius:var(--r-tag);padding:2px 7px;background:rgba(255,255,255,.06);color:var(--fg2);white-space:nowrap}
.tag.ok{background:rgba(61,220,132,.14);color:var(--ok)}
.tag.warn{background:rgba(255,176,32,.14);color:var(--warn)}
.tag.bad{background:rgba(255,45,45,.14);color:var(--red-link)}
.tag.red-o{background:transparent;color:var(--red);border:1px solid rgba(255,45,45,.4)}
.tag.fill-warn{background:var(--warn);color:#141418;font-weight:600}
.dot{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--chrome);flex:0 0 auto}
.dot.ok{background:var(--ok)}.dot.warn{background:var(--warn)}.dot.red{background:var(--red)}.dot.info{background:var(--info)}
.dot.live{animation:pulse 1.6s ease-in-out infinite}
.dot.glow{box-shadow:0 0 8px currentColor}
.toggle{flex:0 0 auto;width:40px;height:22px;border-radius:999px;border:0;padding:0;position:relative;cursor:pointer;background:var(--hair-strong);transition:background .18s}
.toggle:focus-visible{outline-offset:3px}
.toggle .knob{position:absolute;top:2px;left:2px;width:18px;height:18px;border-radius:50%;background:var(--fg);transition:left .18s}
.toggle[aria-checked="true"]{background:var(--red)}
.toggle[aria-checked="true"] .knob{left:20px}
.notice{display:flex;gap:12px;align-items:flex-start;background:rgba(255,176,32,.06);border:1px solid rgba(255,176,32,.25);border-radius:var(--r-lg);padding:12px 16px;font-size:13px;color:var(--fg2);line-height:1.55}
.notice svg{flex:0 0 auto;margin-top:2px}
.notice.ok{background:rgba(61,220,132,.06);border-color:rgba(61,220,132,.25)}
.notice.bad{background:rgba(255,45,45,.04);border-color:rgba(255,45,45,.25)}
.notice.info{background:rgba(79,157,255,.06);border-color:rgba(79,157,255,.3)}
/* Neutral: a fact with nothing to act on (a web with no gap, 2026-10-03). */
.notice.plain{background:var(--surface);border-color:var(--hair)}
.notice b{color:var(--fg);font-weight:600}
/* A notice that folds to one line (the Next page's key, 2026-09-17). `.notice`
   is a flex row; a <details> cannot be, so the row moves onto the summary. */
.notice.fold{display:block;padding:0}
.notice.fold>summary{display:flex;gap:12px;align-items:center;padding:10px 16px;cursor:pointer;list-style:none;min-height:44px;box-sizing:border-box}
.notice.fold>summary::-webkit-details-marker{display:none}
.notice.fold>summary svg{margin-top:0}
.notice.fold .lbl{flex:1 1 auto;min-width:0}
.notice.fold .cnt{color:var(--muted)}
.notice.fold .routing{flex:0 0 auto;white-space:nowrap}
.notice.fold .chev{flex:0 0 auto;width:7px;height:7px;border-right:2px solid var(--fg2);border-bottom:2px solid var(--fg2);transform:rotate(45deg);margin:-3px 4px 0 0;transition:transform .18s}
.notice.fold[open] .chev{transform:rotate(-135deg);margin-top:3px}
.notice.fold>.body{padding:0 16px 13px 44px}
.notice.fold>.body p+p{margin-top:8px}
.empty{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);padding:28px;display:flex;flex-direction:column;align-items:flex-start;gap:10px;min-height:200px;justify-content:center}
.empty h2{font-size:17px;font-weight:600}
.empty p{font-size:13.5px;color:var(--fg2);line-height:1.6}
.empty.warn-b{border-color:rgba(255,176,32,.25);background:rgba(255,176,32,.04)}
.empty.red-b{border-color:rgba(255,45,45,.25);background:rgba(255,45,45,.04)}
.progress{height:6px;border-radius:3px;background:var(--hair-faint);overflow:hidden;position:relative}
.progress .bar{height:100%;background:var(--red);border-radius:3px;box-shadow:0 0 12px rgba(255,45,45,.6)}
.progress.busy{background:rgba(255,45,45,.16)}
.progress .sheen{position:absolute;top:0;bottom:0;width:30%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.35),transparent);animation:slide 1.6s linear infinite}
.bar-thin{height:4px;border-radius:2px;background:var(--hair-faint);overflow:hidden}
.bar-thin > span{display:block;height:100%;border-radius:2px;background:var(--red)}
.messages{list-style:none;margin:0 0 16px;padding:0;display:flex;flex-direction:column;gap:8px}
.messages li{border-radius:var(--r-lg);padding:10px 14px;font-size:13px;background:rgba(255,255,255,.04);border:1px solid var(--hair);color:var(--fg2)}
.messages li.success{border-color:rgba(61,220,132,.25);background:rgba(61,220,132,.06)}
.messages li.warning{border-color:rgba(255,176,32,.25);background:rgba(255,176,32,.06)}
.messages li.error{border-color:rgba(255,45,45,.25);background:rgba(255,45,45,.06)}

/* ---- tables ---- */
.tblwrap{overflow-x:auto;scrollbar-width:thin}
/* ⚠️ `.tscroll` is `auto` here and `hidden` under 720px. A table wide enough
   to need a sideways scrollbar on a phone is a table that has not been made
   to fit; `.fitm` below is what makes it fit, and this is what proves it —
   if a cell still overflows, it clips visibly instead of hiding behind a
   gesture nobody performs. */
.tscroll{overflow:auto;scrollbar-width:thin}
.tbl.fitm{min-width:900px}
.tbl{width:100%;border-collapse:collapse;font-size:13.5px}
.tbl th{text-align:left;padding:10px 16px;font-weight:500;color:var(--muted);font-size:11px;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap;background:var(--sunken)}
.tbl th.r,.tbl td.r{text-align:right}
.tbl td{padding:12px 16px;border-top:1px solid rgba(255,255,255,.05);vertical-align:middle}
.tbl.tight td{padding:9px 14px}
.tbl tr.go{cursor:pointer}.tbl tr.go:hover{background:rgba(255,255,255,.03)}
.tblfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 16px;padding:11px 18px;border-top:1px solid var(--hair-faint);font-size:12.5px;color:var(--muted);background:var(--sunken)}
.rows{display:flex;flex-direction:column}
.rows .row{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between;padding:12px 0;border-top:1px solid var(--hair-faint)}
.rows .row .t{font-size:13.5px}.rows .row .d{font-size:12.5px;color:var(--muted);line-height:1.5;margin-top:3px;text-wrap:pretty}
.kv{display:flex;flex-direction:column;gap:8px;font-size:13px;color:var(--fg2)}
.kv > div{display:flex;justify-content:space-between;gap:12px}
.kv .v{color:var(--fg)}

/* ---- public chrome ---- */
.pubhead{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:32px;padding:0 40px;height:64px;background:rgba(9,9,11,.72);backdrop-filter:blur(14px);border-bottom:1px solid var(--hair-faint)}
.pubhead .brand{display:flex;align-items:center;gap:12px}
.pubhead .brand .routemark{display:block;width:52px;height:auto;flex:0 0 auto}
/* The route symbol draws itself in on every page load wherever it appears — public header,
   page bar, rail tile (v2/_route_mark.html):
   the route left to right, the spurs, then the junction dots. Brand-handoff timings; the
   reduced-motion block at the top of this file ends it at its finished frame. */
.routemark .rm-line{stroke-dasharray:1;animation:drawLine 1.5s cubic-bezier(.4,0,.2,1) both}
.routemark .rm-s1{animation-duration:.55s;animation-delay:.8s}
.routemark .rm-s2{animation-duration:.7s;animation-delay:1s}
.routemark .rm-dot{transform-box:fill-box;transform-origin:center;animation:markPop .45s cubic-bezier(.2,.8,.3,1.3) .62s both}
.routemark .rm-dot2{animation-delay:1.02s}
.pubhead .wordmark{font-weight:600;letter-spacing:.14em;font-size:12px}
.pubhead nav{display:flex;gap:24px;font-size:13.5px;color:var(--fg2);margin-left:12px}
.pubhead nav a{color:var(--fg2)}.pubhead nav a:hover,.pubhead nav a.on{color:var(--fg)}
.pubhead .right{margin-left:auto;display:flex;align-items:center;gap:10px}
.pubfoot{border-top:1px solid var(--hair-faint);padding:32px 40px;display:flex;justify-content:space-between;align-items:center;color:var(--muted);font-size:13px;max-width:1280px;margin:0 auto}
.pubfoot .wordmark{letter-spacing:.14em;font-weight:600;font-size:11px;color:var(--fg2)}
.pubwrap{max-width:1200px;margin:0 auto;padding:40px 36px 80px}

/* ---- landing ---- */
.hero{position:relative;overflow:hidden;padding:32px 40px 40px;min-height:640px}
.hero .bg{position:absolute;inset:0;width:100%;height:100%;opacity:.55;mask-image:radial-gradient(ellipse 70% 80% at 65% 45%,#000 30%,transparent 100%);-webkit-mask-image:radial-gradient(ellipse 70% 80% at 65% 45%,#000 30%,transparent 100%)}
.hero .cols{position:relative;max-width:1200px;margin:0 auto;display:grid;grid-template-columns:minmax(0,1.15fr) minmax(300px,.85fr);gap:40px;align-items:start}
/* Top-aligned since the 2026-10-08 design pass: the copy starts level with
 * the top of the phone's screen rather than centred beside it. */
.hero .copy{padding-top:56px}
.hero .badge,.apppage .badge{display:inline-flex;align-items:center;gap:8px;border:1px solid rgba(255,255,255,.1);border-radius:999px;padding:5px 12px 5px 8px;font-size:12px;color:var(--fg2);background:rgba(17,17,20,.7)}
.hero h1{font-size:clamp(40px,4.6vw,64px);line-height:1.02;letter-spacing:-.035em;font-weight:600;margin:24px 0 20px;text-wrap:balance}
.hero h1 .red{color:var(--red)}
.hero p{font-size:18px;line-height:1.55;color:var(--fg2);max-width:520px;margin:0 0 32px;text-wrap:pretty}
.hero .ctas{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
.phone-wrap{display:flex;flex-direction:column;gap:26px;justify-content:center;align-items:center;position:relative;animation:rise .9s .15s var(--ease) both}
.phone-glow{position:absolute;width:min(420px,100%);height:420px;border-radius:50%;background:radial-gradient(circle,rgba(255,45,45,.16),rgba(255,45,45,.04) 45%,transparent 70%);filter:blur(10px);pointer-events:none}
.phone{position:relative;transform:rotate(-4deg)}
.phone .side{position:absolute;width:3px;border-radius:2px 0 0 2px;background:linear-gradient(90deg,#6f6f79,#2e2e35)}
.phone .side.r{right:-3px;border-radius:0 2px 2px 0;background:linear-gradient(270deg,#6f6f79,#2e2e35)}
.phone .body{padding:11px;border-radius:59px;background:linear-gradient(148deg,#8b8b95 0%,#3a3a42 18%,#6c6c76 38%,#26262c 62%,#5e5e68 82%,#33333a 100%);box-shadow:0 70px 100px -34px rgba(0,0,0,.95),0 0 0 1px rgba(0,0,0,.6),0 0 70px -14px rgba(255,45,45,.22),inset 0 1px 1px rgba(255,255,255,.45),inset 0 -1px 1px rgba(255,255,255,.18)}
.phone .screen{border-radius:48px;overflow:hidden;background:#000;box-shadow:inset 0 0 0 3px #050507,0 2px 6px rgba(0,0,0,.9);width:280px;height:580px;position:relative}
.phone .sheen{position:absolute;inset:0;background:linear-gradient(150deg,rgba(255,255,255,.09),transparent 42%);pointer-events:none}
/* The iOS device frame, from the handoff's ios-frame.jsx (IOSDevice): the
 * dynamic island, the 9:41 status bar and the home indicator. Without them the
 * hero is a picture in a rounded rectangle, not a phone. */
.phone .island{position:absolute;top:11px;left:50%;transform:translateX(-50%);width:126px;height:37px;border-radius:24px;background:#000;z-index:50}
/* The gap is the island: two flex:1 halves 154px apart, which on this 280px
 * screen pushes the clock and the radios out past their own boxes and clear
 * of the island above them. space-between instead parks the radios under it
 * and the signal bars disappear (the island is z-50). */
.phone .iosbar{position:absolute;top:0;left:0;right:0;bottom:auto;z-index:10;display:flex;align-items:center;justify-content:center;gap:154px;padding:21px 24px 19px;color:#fff;font-family:-apple-system,"SF Pro",system-ui,sans-serif}
.phone .iosbar>*{flex:1;height:22px;display:flex;align-items:center;justify-content:center}
.phone .iosbar .time{font-weight:590;font-size:17px;line-height:22px}
.phone .iosbar .icons{gap:7px}
.phone .home{position:absolute;left:0;right:0;bottom:0;z-index:60;height:34px;display:flex;justify-content:center;align-items:flex-end;padding-bottom:8px;pointer-events:none}
.phone .home i{display:block;width:139px;height:5px;border-radius:100px;background:rgba(255,255,255,.7)}
/* The two pieces of app chrome the shot is meant to show off. */
.phone .seg{position:absolute;top:76px;left:14px;right:14px;display:flex;background:rgba(17,17,20,.85);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:3px;backdrop-filter:blur(8px);font-size:12px;font-weight:500;z-index:5}
.phone .seg span{flex:1;text-align:center;padding:7px;color:var(--fg2)}
.phone .seg span.on{border-radius:7px;background:var(--red);color:#fff}
/* The screenshots (handoff 2026-10-05): Moab with its card, then Arizona,
 * then North Georgia. The island, status bar, toggle, home bar and sheen stay
 * fixed above a track that slides. herophone.js moves the track by transform
 * and pointer events; native overflow scrolling with scroll-snap was
 * unreliable inside the rotated phone. pan-y keeps a vertical swipe a page
 * scroll. With no script the track stays at 0, Moab shows, and the arrows and
 * dots stay hidden (they wait for `.js` on the wrap). */
.phone .shots{position:absolute;inset:0;overflow:hidden;touch-action:pan-y;user-select:none;-webkit-user-select:none}
.phone-wrap.js .shots{cursor:grab}
.phone .shots .track{position:absolute;inset:0;display:flex;transition:transform .35s var(--ease)}
.phone .shots .track.drag{transition:none}
.phone .slide{flex:0 0 100%;height:100%;position:relative}
.phone .slide img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;filter:brightness(1.15);-webkit-user-drag:none}
.phone .slide.moab img{object-position:50% 44%;filter:brightness(1.22) saturate(1.05)}
/* The map's selected-web card, compact: no buttons, no nearest-gap note, and
 * no rank — a rank moves on every build, so the card says SELECTED. */
.phone .shotcard{position:absolute;left:14px;right:14px;bottom:20px;background:rgba(17,17,20,.94);border:1px solid rgba(255,255,255,.1);border-radius:16px;padding:14px 16px 16px;backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px)}
.phone .shotcard .caps{font-family:var(--mono);font-size:10px;font-weight:400;letter-spacing:.08em;color:var(--muted)}
.phone .shotcard .name{margin:4px 0 12px;font-size:18px;font-weight:600;letter-spacing:-.02em;line-height:1.1}
.phone .shotcard .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.phone .shotcard .stats b{display:block;font-family:var(--mono);font-size:19px;font-weight:500;letter-spacing:-.03em;line-height:1}
.phone .shotcard .stats span{font-size:10px;color:var(--fg2)}
.phone .shotnav{position:absolute;top:50%;margin-top:-22px;width:44px;height:44px;border:0;background:transparent;padding:0;display:none;place-items:center;cursor:pointer;z-index:6;transition:opacity .2s}
.phone-wrap.js .shotnav{display:grid}
.phone .shotnav.prev{left:6px}
.phone .shotnav.next{right:6px}
.phone .shotnav[data-off]{opacity:0;pointer-events:none}
.phone .shotnav span{width:30px;height:30px;border-radius:50%;background:rgba(17,17,20,.8);border:1px solid rgba(255,255,255,.14);display:grid;place-items:center;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
.phone .shotnav svg{fill:none;stroke:#f4f4f5;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round}
/* Dots only: the design's caption line under the phone was dropped (Greg,
 * 2026-10-05). */
.shotcap{position:relative;display:flex;justify-content:center}
.shotcap .dots{display:none;gap:6px}
.phone-wrap.js .shotcap .dots{display:flex}
.shotcap .dots button{width:6px;height:6px;border-radius:999px;border:0;padding:0;background:rgba(255,255,255,.22);cursor:pointer;transition:width .25s,background .25s}
.shotcap .dots button.on{width:18px;background:var(--red)}
.features{border-top:1px solid var(--hair-faint);border-bottom:1px solid var(--hair-faint);background:var(--map)}
.features .in{max-width:1200px;margin:0 auto;padding:72px 36px;display:grid;grid-template-columns:repeat(3,1fr);gap:24px}
.feature{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);padding:28px;display:flex;flex-direction:column;gap:14px}
.feature .ic{width:36px;height:36px;border-radius:10px;background:rgba(255,45,45,.12);display:grid;place-items:center}
.feature h3{font-size:20px;font-weight:600;letter-spacing:-.02em}
.feature p{color:var(--fg2);line-height:1.6}
.pricing{max-width:1200px;margin:0 auto;padding:88px 40px 96px}
.pricing .lead{text-align:center;max-width:560px;margin:0 auto 48px}
.pricing h2{font-size:40px;letter-spacing:-.03em;font-weight:600;margin:12px 0}
.plans{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;align-items:stretch}
.plans.two{grid-template-columns:repeat(2,1fr);max-width:760px;margin-left:auto;margin-right:auto}
.plan{background:var(--surface);border:1px solid var(--hair);border-radius:20px;padding:30px;display:flex;flex-direction:column;gap:18px;position:relative}
.plan.hero-plan{background:#131316;border-color:rgba(255,45,45,.5);box-shadow:0 30px 60px -30px rgba(255,45,45,.35)}
.plan .pop{position:absolute;top:-12px;left:30px;background:var(--red);color:#fff;font-size:11px;font-weight:600;letter-spacing:.08em;border-radius:999px;padding:4px 10px}
.plan .price{font-size:44px;font-weight:600;letter-spacing:-.04em;font-family:var(--mono)}
.plan ul{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:10px;flex:1}
.plan li{display:flex;gap:10px;align-items:flex-start;font-size:14px}
/* Choose your plan (v2/plan.html, design handoff 2026-09-28): the /beta cards
 * in the app shell, the Pro card carrying the renewal consent. It FADES
 * in rather than rises (V2-DESIGN: an entrance page moves nothing). */
.page.planpage{animation-name:fade}
.planchoice{max-width:760px;margin:0 auto}
.planchoice .steps{margin-bottom:36px}
.planchoice .stepsmini{display:none;align-items:center;gap:10px;margin-bottom:20px;font-size:12px;color:var(--fg2)}
.stepsmini .bars{display:flex;gap:4px}
.stepsmini .bars i{width:18px;height:3px;border-radius:2px;background:var(--hair-strong)}
.stepsmini .bars i.on{background:var(--red)}
.stepsmini b{color:var(--fg);font-weight:400}
.planchoice .back{margin-bottom:20px}
.planlead{text-align:center;max-width:600px;margin:0 auto 44px;display:flex;flex-direction:column;gap:12px;align-items:center}
.planlead h1{margin:0;font-size:36px;font-weight:600;letter-spacing:-.03em;line-height:1.08;text-wrap:balance}
.planlead p{margin:0;font-size:16px;line-height:1.55;text-wrap:pretty}
.importpill{display:inline-flex;align-items:center;gap:8px;margin-top:4px;border:1px solid var(--hair);border-radius:999px;padding:5px 12px 5px 10px;background:var(--surface);font-size:12.5px;color:var(--fg2)}
.importpill .pulse{width:6px;height:6px;border-radius:50%;background:var(--red);animation:pulse 1.6s ease-in-out infinite}
.importpill .num{color:var(--fg)}
.planchoice .plan li svg{color:var(--fg2);flex:none;margin-top:3px}
.planchoice .plan.hero-plan li svg{color:var(--red)}
.planchoice .plan .btn{width:100%}
.plan .pop.mine{background:var(--raised);color:var(--fg2);border:1px solid var(--hair-strong);padding:3px 10px}
.plan .consent{display:flex;flex-direction:column;gap:12px;padding-top:18px;border-top:1px solid var(--hair)}
.renewbox{padding:12px 14px;border-radius:var(--r-lg);background:var(--sunken);border:1px solid var(--hair)}
.renewbox.is-error{border-color:rgba(255,45,45,.6)}
.renewerr{margin:0;display:flex;gap:8px;align-items:flex-start;font-size:13px;line-height:1.45;color:var(--red-link)}
.renewerr svg{flex:none;margin-top:1px}
.plan .consent .fine{color:var(--muted)}
.plan .consent .fine a{color:var(--fg2);text-decoration:underline;text-underline-offset:3px}
.plan .consent .fine a:hover{color:var(--fg)}
.signup{display:flex;gap:8px;max-width:460px}
.signup .input{flex:1}

/* ---- the mobile apps (/app/, design pass 2026-10-08) ----
 * One centred column. The store badges are Apple's and Google's files as
 * supplied: sized here and nowhere else, never recoloured. Google's PNG
 * carries transparent padding (168 of its 250px is artwork), hence 60px tall
 * with the padding pulled in; both fit one row on a phone at full size. */
.apppage{position:relative;overflow:hidden;padding:72px 40px 96px}
.apppage::before{content:"";position:absolute;left:50%;top:-120px;width:720px;height:520px;margin-left:-360px;background:radial-gradient(closest-side,rgba(255,45,45,.16),transparent);pointer-events:none}
.apppage .in{position:relative;max-width:640px;margin:0 auto;display:flex;flex-direction:column;align-items:center;text-align:center}
.apppage .appicon{display:block;width:112px;height:112px;border-radius:26px;box-shadow:0 0 0 1px rgba(255,255,255,.08),0 24px 60px -20px rgba(255,45,45,.45)}
.apppage .badge{margin-top:28px;background:rgba(17,17,20,.6)}
.apppage h1{font-size:clamp(36px,5vw,56px);line-height:1.04;letter-spacing:-.035em;font-weight:600;margin:18px 0 16px;text-wrap:balance}
.apppage .lede{font-size:17px;line-height:1.6;color:var(--fg2);margin:0 0 36px;max-width:520px;text-wrap:pretty}
.storebadges{display:flex;flex-wrap:nowrap;justify-content:center;align-items:center;gap:12px;padding:10px 0}
.storebadge{display:block;line-height:0;border-radius:8px}
.storebadge:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.storebadge img{display:block;width:auto}
.storebadge .apple{height:40px}
.storebadge .google{height:60px;margin:0 -10px}
.storecap{margin-top:14px;font-family:var(--mono);font-size:12px;letter-spacing:.04em;color:var(--muted)}
.notifycard{margin-top:56px;width:100%;max-width:520px;text-align:left}
.notifycard .hc{font-size:15px;margin-bottom:6px}
.notifycard>.small{font-size:13.5px;color:var(--fg2);margin-bottom:14px}
.notifycard .signup{max-width:none;flex-wrap:wrap}
.notifycard .signup .input{flex:1 1 220px;min-width:0}
.notifycard .signup[hidden]{display:none}
.notifycard #notify-msg{margin:10px 0 0}
.notifycard #notify-msg:empty{display:none}
.notifycard #notify-msg.ok{color:var(--ok)}.notifycard #notify-msg.bad{color:var(--red-link)}
.apppage .after{margin-top:28px;font-size:13.5px;color:var(--fg2)}
.apppage .after a{color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.apppage .after a:hover{color:var(--red-link)}

/* ---- app shell ---- */
.shell{display:grid;grid-template-columns:76px minmax(0,1fr);grid-template-rows:minmax(0,1fr) auto;height:100vh;height:100dvh;overflow:hidden}
.rail{grid-row:1 / span 2;height:100%;border-right:1px solid var(--hair-faint);background:var(--ground);display:flex;flex-direction:column;align-items:center;padding:16px 0;gap:6px}
.rail .logo{flex:0 0 44px;width:44px;height:44px;border-radius:14px;background:var(--red);display:grid;place-items:center;margin-bottom:14px;box-shadow:0 12px 30px -10px rgba(255,45,45,.7)}
.rail .logo .routemark{display:block;width:28px;height:auto}
.rail a.item{width:60px;padding:9px 0 7px;border-radius:12px;display:flex;flex-direction:column;align-items:center;gap:5px;color:var(--fg2);position:relative;font-size:10px;font-weight:500;letter-spacing:.01em}
.rail a.item:hover{color:var(--fg);background:rgba(255,255,255,.06)}
.rail a.item.on{color:var(--red);background:rgba(255,45,45,.12)}
.rail a.item.on::before{content:"";position:absolute;left:-8px;top:12px;bottom:12px;width:2px;border-radius:2px;background:var(--red)}
.rail .badge{position:absolute;top:6px;right:10px;background:var(--red);color:#fff;font-size:9px;font-weight:600;border-radius:999px;padding:1px 5px;font-family:var(--mono)}
.rail .tools{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:6px;padding-top:10px}
.rail .tools a.item{padding:8px 0 6px;font-size:9.5px}
.rail .sep{width:36px;height:1px;background:var(--hair);margin-bottom:6px}
.rail .plan{font-family:var(--mono);font-size:9px;margin-top:6px;letter-spacing:.08em;border:1px solid var(--hair-strong);border-radius:999px;padding:3px 7px;writing-mode:vertical-rl;transform:rotate(180deg);color:var(--fg2)}
.rail .plan.paid{color:var(--red);border-color:rgba(255,45,45,.5)}
.avatar{width:34px;height:34px;border-radius:50%;background:linear-gradient(135deg,var(--red),var(--red-deep));display:grid;place-items:center;font-size:13px;font-weight:600;color:#fff}
.avatar.lg{width:64px;height:64px;font-size:24px}
/* The rail's avatar IS the way to Settings (2026-09-28), and since 2026-10-02
 * it is an ITEM with the face for its icon and "Settings" under it, so it
 * lights like one — plus a ring, since a filled circle cannot change colour. */
.rail a.item.me .avatar{width:28px;height:28px;font-size:12px}
.rail a.item.me.on .avatar{box-shadow:0 0 0 2px var(--red)}
/* An uploaded photo fills the same circle the initial sat in (core.avatars). */
.avatar{overflow:hidden}
.avatar img{display:block;width:100%;height:100%;object-fit:cover;border-radius:50%}
.avatarpick{display:flex;flex-direction:column;align-items:center;gap:6px}
.avatarpick label.avatar{cursor:pointer}
.avatarpick .link{cursor:pointer;text-decoration:underline;text-underline-offset:3px}
/* The chooser is visually hidden, not display:none, so it stays focusable
   and a label still opens it in every browser. */
.filepick{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;clip:rect(0 0 0 0)}
.avatarpick:focus-within label.avatar{box-shadow:0 0 0 2px var(--fg)}
.main{min-width:0;min-height:0;overflow-y:auto;position:relative;scrollbar-width:thin}
.topline{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:12px;padding:0 36px;height:56px;background:rgba(9,9,11,.8);backdrop-filter:blur(14px);border-bottom:1px solid var(--hair-faint)}
.topline .brand{display:flex;align-items:center;gap:12px;min-width:0;border-radius:8px;-webkit-tap-highlight-color:transparent}
.topline .brand:hover .wordmark{color:var(--fg)}
.topline .brand:active{opacity:.6;transition-duration:0s}
.topline .routemark{display:block;width:40px;height:auto;flex:0 0 auto}
.topline .wordmark{font-weight:600;letter-spacing:.14em;font-size:11px;color:var(--fg2);white-space:nowrap}
.topline .right{margin-left:auto;display:flex;align-items:center;gap:12px;min-width:0;flex:1 1 auto;justify-content:flex-end}
.topline .search{display:flex;align-items:center;gap:8px;border:1px solid rgba(255,255,255,.1);border-radius:999px;padding:6px 12px 6px 10px;font-size:12px;color:var(--fg2);flex:0 1 260px;min-width:0;white-space:nowrap;overflow:hidden}
.topline .search input{background:none;border:0;color:var(--fg);outline:none;flex:1;min-width:0;font-size:12px}
.topline .kbd{margin-left:auto;font-family:var(--mono);font-size:10px;border:1px solid rgba(255,255,255,.1);border-radius:4px;padding:0 4px}
.planbadge{font-size:11px;font-weight:600;letter-spacing:.08em;border:1px solid var(--hair-strong);color:var(--fg2);border-radius:999px;padding:4px 10px}
.planbadge.paid{color:var(--red);border-color:rgba(255,45,45,.5)}
.page{max-width:1240px;padding:24px 36px 96px;animation:rise .5s var(--ease) both}
.page.narrow{max-width:900px}.page.thin{max-width:820px}.page.form{max-width:420px}
.back{font-size:13px;color:var(--fg2);display:inline-flex;align-items:center;gap:6px;margin-bottom:16px}
.back:hover{color:var(--fg)}
.tabbar{display:none}

/* ---- map screen ---- */
/* Fills the scroll area exactly. A percentage height chain through the
 * grid cell measured 0x0 in the browser pane (2026-09-08), which gives
 * MapLibre an 800x600 default canvas and a blank screen. */
.mapfull{position:absolute;inset:0}
.mapwrap{position:relative;overflow:hidden;background:var(--map);height:100%}
.mapwrap .ml{position:absolute;inset:0}
.firstrun{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none;padding:16px}
.firstrun .empty{pointer-events:auto;max-width:420px;width:100%}
.firstrun .frbtns{display:flex;gap:8px;flex-wrap:wrap}
.firstrun .progress{align-self:stretch;margin-top:4px}
.firstrun .frcount{font-size:12.5px;color:var(--muted)}
.firstrun .frcount .num{color:var(--fg)}
.glass{background:rgba(17,17,20,.88);border:1px solid rgba(255,255,255,.1);backdrop-filter:blur(12px)}
.ov{position:absolute;top:16px;left:16px;right:16px;bottom:16px;display:flex;justify-content:space-between;align-items:flex-start;gap:12px 16px;pointer-events:none;white-space:nowrap}
.ov > *{pointer-events:auto}
.ov-col{display:flex;flex-direction:column;gap:10px;min-width:0;flex:0 1 auto;animation:rise .5s var(--ease) both}
.modebar{display:flex;align-items:center;gap:2px;border-radius:999px;padding:5px;box-shadow:0 20px 40px -20px rgba(0,0,0,.8);align-self:flex-start;max-width:100%;flex-wrap:wrap}
.modebar button,.modebar a{border:0;padding:9px 16px;border-radius:999px;cursor:pointer;font-size:13.5px;font-weight:600;background:transparent;color:var(--fg2);transition:background .25s;text-decoration:none}
/* One line beside an open web card at ~1100px of map (Greg, 2026-09-27): the
 * Gaps link left the pill (the rail's Next covers it) and the gaps tightened. */
.modebar [data-mode]{padding:9px 14px}
.modebar .on{background:var(--red);color:#fff}
.modebar .sep{width:1px;height:20px;background:rgba(255,255,255,.12);margin:0 4px}
.modebar .ctl{font-weight:400;font-size:13px;display:flex;align-items:center;gap:6px;padding:0 6px}
.modebar .ctl:hover{color:var(--fg)}
.modebar select{background:transparent;border:0;color:var(--fg2);font-size:13px;padding:9px 10px;cursor:pointer}
.modebar .layerbtn{display:none}
.mapctl{display:flex;align-items:center;gap:2px;min-width:0}
.ctlgroup{display:flex;align-items:center;gap:2px}
/* One DOM order, two reading orders: the sheet reads Basemap / Filter top to
   bottom, the desktop pill reads Years · Type · Basemap left to right. `order`
   is ignored once the phone makes .mapctl a block. The space between the two
   groups goes on .g-base, the VISUALLY second: `.ctlgroup + .ctlgroup` would
   pick .g-filter by DOM order and put it before Years instead. */
.mapctl .g-filter{order:2}.mapctl .g-base{order:3;margin-left:4px}
.ctlcap,.sheetgrab,.sheethead,.bmgrid,.sheetscrim{display:none}
/* The captions used to be .hidden spans — a bare "Dark ⌄" named nothing. */
.modebar .ctl .cap{font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:600}
/* Each pill select is only as wide as the option showing (field-sizing; the
 * fallback is map.js's fitSelect) and draws its own chevron right after its
 * text (2026-09-27). A native select is as wide as its LONGEST option, so
 * "Dark" sat at the left of a box sized for "Satellite (Esri)" and its arrow
 * landed beside the next label. The 720px block gives the sheet rows back
 * their native select. */
.modebar .ctl select{appearance:none;-webkit-appearance:none;field-sizing:content;padding:9px 16px 9px 0;color:var(--fg);
  background:transparent url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23a1a1aa' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E") no-repeat right 1px center/10px 10px}
.modebar .ctl select:hover{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23f4f4f5' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")}
.modebar .ctl select option{background:#111114;color:var(--fg)}
/* Years and Type in Heat mode: disabled, at the site's 45% (map.js
   filterControls). Not `.is-disabled` — that takes pointer events away, and
   the label's title is the only place the reason is given. */
.ctl.off{opacity:.45;cursor:not-allowed}
.ctl.off select{cursor:not-allowed}
.legend{align-self:flex-start;display:flex;flex-wrap:wrap;gap:8px 16px;border-radius:999px;padding:8px 16px;font-size:12px;color:var(--fg2);animation:rise .4s both;max-width:100%}
.legend span{display:flex;align-items:center;gap:8px}
.legend i{display:inline-block;width:20px;height:3px;border-radius:2px}
/* The key: Webs mode's legend as a popover off the pill (2026-09-27, Greg:
 * "like a tool tip… only if the user needs it"). map.js's setKey opens it. */
.keywrap{position:relative;display:flex;align-items:center}
.modebar .keybtn{display:grid;place-items:center;width:36px;height:36px;padding:0;color:var(--fg2)}
.modebar .keybtn .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.modebar .keybtn:hover{color:var(--fg)}
.modebar .keybtn[aria-expanded="true"]{background:rgba(255,255,255,.08);color:var(--fg)}
/* max-width:none — the strip's `.legend{max-width:100%}` would size the
 * popover to .keywrap, i.e. the 36px button, and spill the entries out of it. */
.keywrap .legend{position:absolute;top:calc(100% + 10px);right:0;z-index:5;max-width:none;flex-direction:column;flex-wrap:nowrap;align-items:flex-start;gap:10px;border-radius:14px;padding:12px 14px;white-space:nowrap;box-shadow:0 20px 40px -20px rgba(0,0,0,.8);animation:rise .2s both}
.keywrap .legend.closed{display:none}
.rebuild{display:flex;align-items:center;gap:10px;border-radius:999px;padding:8px 14px 8px 12px;font-size:12px;color:var(--fg2);animation:rise .4s both;border-color:rgba(255,176,32,.3)}
.rebuild .x{background:none;border:0;color:var(--faint);font-size:14px;line-height:1;cursor:pointer;padding:0 0 0 2px}
.rebuild .x:hover{color:var(--fg2)}
/* Smaller: 10.5px on 5/10 padding instead of 11.5px on 8/14. It is a receipt,
   not a headline — it sat as tall as the zoom control. */
/* ⚠️ A BLOCK, not flex: `text-overflow` does nothing on a flex container (its
   text runs become anonymous flex items), so the flex version clipped
   mid-word with no ellipsis and cut off the mode label at the end. */
.status{position:absolute;left:16px;bottom:16px;display:block;max-width:calc(100% - 32px);border-radius:999px;padding:5px 10px 5px 8px;font-family:var(--mono);font-size:10.5px;line-height:1.35;color:var(--fg2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.status .v{color:var(--fg)}
.status .dot{vertical-align:middle;margin:-2px 7px 0 0}
.status .narrow{display:none}
/* The rebuild notice TAKES the status pill's slot rather than stacking above
   it (Greg, 2026-09-14: "only one notification showing"), at the pill's own
   size. The sibling rule is the whole swap — map.js only toggles `.hidden` on
   the notice, so the pill comes back the moment the job ends or is dismissed.
   ⚠️ #rebuild must stay BEFORE .status in the markup for `~` to reach it.
   Flex for the dismiss button, so the ellipsis lives on the .msg child. */
.rebuild.pos{position:absolute;left:16px;bottom:16px;max-width:calc(100% - 32px);gap:7px;padding:5px 8px 5px 8px;font-family:var(--mono);font-size:10.5px;line-height:1.35;white-space:nowrap}
.rebuild.pos .msg{min-width:0;overflow:hidden;text-overflow:ellipsis}
.rebuild.pos .x{font-size:12px}
.rebuild.pos:not(.hidden) ~ .status{display:none}
.brctl{position:absolute;right:16px;bottom:16px;display:flex;flex-direction:column;align-items:flex-end;gap:8px}
/* ONE SIZE FOR EVERY MAP BUTTON (Greg, 2026-09-17): this cluster wears the
   small maps' metrics (.ectl below) — 34px for a pointer, 38px for a thumb in
   the breakpoint — on the compass, the zoom pair and Locate alike. It was
   44 / 38 / 38 beside the embeds' 34; the smaller won. MapButtonsAreOneSizeTests
   compares the two clusters on both sides of the breakpoint. */
.compass{width:34px;height:34px;border-radius:50%;cursor:grab;display:grid;place-items:center;padding:0;touch-action:none;border:1px solid rgba(255,255,255,.1);background:rgba(17,17,20,.9);backdrop-filter:blur(12px)}
.compass:hover{border-color:rgba(255,255,255,.3)}
.compass svg{width:21px;height:21px;transition:transform .5s var(--ease)}
.compass.dragging svg{transition:none}
.zoomcluster{width:34px;display:flex;flex-direction:column;border-radius:12px;overflow:hidden}
.zoomcluster button{width:100%;height:34px;border:0;background:transparent;color:var(--fg);font-size:18px;cursor:pointer;border-bottom:1px solid var(--hair);display:grid;place-items:center;padding:0}
.zoomcluster button:last-child{border-bottom:0}
.zoomcluster button:hover{background:rgba(255,255,255,.06)}
.zoomcluster button:active{background:rgba(255,255,255,.14);transition-duration:0s}
.zoomcluster button:focus-visible{outline-offset:-2px}
.locate.on{color:var(--info)}.locate.blocked{color:var(--muted)}
.locate .spin{animation:spin .9s linear infinite}
.readout{position:absolute;left:50%;transform:translateX(-50%);bottom:58px;display:flex;align-items:center;gap:8px;border-radius:999px;padding:7px 14px;font-family:var(--mono);font-size:11.5px;color:var(--fg2);animation:rise .3s both;border-color:rgba(79,157,255,.35)}
.readout.warn{border-color:rgba(255,176,32,.3);font-family:var(--sans);font-size:12px}
/* position:relative moved here off the element's inline style — inline, it
   outranked the phone rule below and the card ran off the screen. */
/* A desktop card stops above the lower-right control stack (2026-09-27): with
 * no cap, a ~540px window ran the web card's foot — and the activity card's
 * stepper — under the compass and zoom. `.ov` has a bottom edge so the % can
 * resolve; 160px = compass 34 + gap 8 + zoom 3×34, borders, the 16px inset and
 * room to breathe. The breakpoint's bottom sheet sets its own max-height.
 * map.js measures the rendered (capped) box, so `isSheet` still reads a panel. */
.webcard{position:relative;width:300px;flex:0 0 300px;white-space:normal;border-radius:22px;padding:20px;box-shadow:0 20px 40px -20px rgba(0,0,0,.8);animation:rise .5s var(--ease) both;background:rgba(17,17,20,.94);max-height:calc(100% - 160px);overflow:auto;scrollbar-width:thin}
.webcard h2{margin:6px 0 16px;font-size:22px;font-weight:600;letter-spacing:-.02em;line-height:1.1;padding-right:26px}
.webcard .stats{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.webcard .stats span.n{display:block;font-family:var(--mono);font-size:24px;font-weight:500;letter-spacing:-.03em;line-height:1}
.webcard .stats span.l{font-size:12px;color:var(--fg2)}
.webcard p{font-size:12.5px;color:var(--fg2);line-height:1.6;margin:16px 0 14px;border-top:1px solid var(--hair);padding-top:14px;text-wrap:pretty}
.webcard .acts{display:flex;gap:8px}
.webcard .acts .btn{flex:1;border-radius:999px;padding:10px;min-height:44px}
/* `.btn` sets display, which outranks the UA's [hidden] rule: a web with no
 * gap hides "See the gaps" and Web details takes the row (2026-10-03). */
.webcard .acts .btn[hidden]{display:none}
.webcard .close{position:absolute;top:14px;right:14px;width:28px;height:28px;background:none;border:0;color:var(--faint);font-size:16px;cursor:pointer;line-height:1}
/* Lines mode's activity card (2026-09-16) wears the web card. A long
 * activity name stops at two lines; the stepper walks every activity a tap
 * landed on when several are drawn along the same stretch. */
.actcard h2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;overflow-wrap:anywhere}
/* The locked activity card (billing 6.3). It replaces the stats rather than
   greying them: the server sends no numbers for a locked ride, so there is
   nothing to grey out — the lock IS the content. */
/* The lock, wherever something is Pro-only (billing 6.3, 6.5). ⚠️ NOT
   scoped to `.actcard`: it started there, on the map's locked ride, and the
   upload page needs the same lock for the bulk uploader. A second copy under
   a second parent is two locks that drift — and `.actcard .lockbox` would
   beat `.lockbox` on specificity per PROPERTY, so the drift would show up as
   half this block applying and half not. */
.lockbox{display:flex;flex-direction:column;align-items:center;text-align:center;gap:.5rem;padding:.9rem .4rem .2rem}
.lockbox .lockicon{color:var(--muted);opacity:.85}
.lockbox .lockhead{margin:0;font-size:15px;font-weight:600;letter-spacing:.02em}
.lockbox .lockwhy{margin:0;font-size:12.5px;line-height:1.5;color:var(--muted);text-wrap:pretty;max-width:30ch}
.lockbox .lockcta{margin-top:.35rem;width:100%}
/* The card form is wider than the map's 260px panel, so the copy may breathe
   and the button need not span the card. */
.card.lockbox{gap:.7rem;max-width:none}
.card.lockbox .lockwhy{max-width:52ch;font-size:13.5px}
.card.lockbox .lockcta{width:auto;padding-left:1.4rem;padding-right:1.4rem}
.actcard.many h2{min-height:2.2em}
.actcard .eyebrow{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;padding-right:26px}
.actcard p:empty{display:none}
.actcard .pick{display:flex;align-items:center;justify-content:space-between;gap:8px;margin:14px 0 12px;padding:4px;border-radius:999px;background:rgba(255,255,255,.05);border:1px solid var(--hair)}
.actcard .pick.hidden{display:none}
.actcard .pick .of{font-size:12px;color:var(--fg2)}
.actcard .step{width:36px;height:36px;border-radius:50%;border:0;background:rgba(255,255,255,.07);color:var(--fg);font-size:20px;line-height:1;cursor:pointer}
.actcard .step:hover{background:rgba(255,255,255,.12)}
.actcard .step:active{background:rgba(255,255,255,.18);transition-duration:0s}
.actcard p:empty + .pick{margin-top:16px}
.actcard .close:active,.webcard .close:active{color:var(--fg);transition-duration:0s}
.maplibregl-ctrl-attrib,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-bottom-left{display:none}
.maplibregl-popup-content{background:var(--surface);color:var(--fg);border:1px solid var(--hair);border-radius:12px;font-family:var(--sans);font-size:13px;padding:12px 14px}
.maplibregl-popup-tip{border-top-color:var(--surface) !important;border-bottom-color:var(--surface) !important}
.maplibregl-popup-close-button{color:var(--muted)}
.mapbox{position:relative;border-radius:var(--r-card);overflow:hidden;border:1px solid var(--hair);background:var(--map);height:460px}
.mapbox .ml{position:absolute;inset:0}
.mapbox .key{position:absolute;left:14px;bottom:34px;display:flex;gap:14px;flex-wrap:wrap;border-radius:10px;padding:8px 12px;font-size:12px;color:var(--fg2);background:rgba(17,17,20,.86);border:1px solid rgba(255,255,255,.1);backdrop-filter:blur(8px)}
.mapbox .key span{display:flex;align-items:center;gap:7px}
.mapbox .key i{display:inline-block;width:20px;height:3px;border-radius:2px}
.mapbox .key i.dash{height:0;border-top:3px dashed var(--warn);background:none}
.mapbox .mapnote{position:absolute;left:14px;top:14px;border-radius:10px;padding:8px 12px;font-size:12px;color:var(--fg2);background:rgba(17,17,20,.86);border:1px solid rgba(255,255,255,.1)}
/* The small maps' control cluster (2026-09-16) — `embedchrome.js`, a MapLibre
 * control in `bottom-right` on all six embedded maps: compass, zoom pair,
 * basemap picker, the main map's `.brctl` corner and order one size down.
 * The rule above hides both bottom corners for the main map's sake; the two
 * the embeds use come back inside `.mapbox` only. ⚠️ The bottom-left one is
 * the OSM / OpenFreeMap attribution, which that rule had hidden on every
 * embed — a licence requirement, not a style. Scoped to `.mapbox`, so the
 * main map screen is untouched. */
/* the two corners the embeds actually use */
.mapbox .maplibregl-ctrl-bottom-right{display:flex;flex-direction:column;align-items:flex-end}
/* The left corner stops short of the cluster (12 + 34 + 12), so a long
 * attribution wraps instead of running under the basemap button. */
.mapbox .maplibregl-ctrl-bottom-left{display:block;right:58px}
/* The legend, moved into that corner by V2Embed, stacks above the credit. */
.mapbox .maplibregl-ctrl.key{position:static;float:left;margin:0 0 8px 12px;font-family:var(--sans);line-height:normal}
/* The credit on EVERY map (embedchrome.js credit()): small and quiet. The
 * global rule above hides MapLibre's attribution; `.ec-credit` is ours, and
 * three classes beat maplibre-gl.css's two. */
.maplibregl-ctrl.maplibregl-ctrl-attrib.ec-credit{
  display:block;padding:1px 5px;border-radius:4px;
  font:500 8.5px/12px var(--sans);letter-spacing:.01em;
  background:rgba(12,12,15,.32);color:rgba(255,255,255,.62);border:0}
.mapbox .maplibregl-ctrl-bottom-left .ec-credit{margin:0 0 8px 10px}
/* The main map: the credit sits left of the page's own compass cluster
 * (.brctl, 16px in, 34px wide — 38px on a phone) and wraps inside the right half, clear of the
 * status pill on the left. */
.mapwrap .maplibregl-ctrl-bottom-right{display:block;left:50%}
.mapwrap .maplibregl-ctrl-bottom-right .ec-credit{margin:0 58px 20px 0}
.maplibregl-ctrl-attrib.ec-credit a{color:inherit}
.maplibregl-ctrl-attrib.ec-credit a:hover{color:#fff;text-decoration:none}
/* Folded (embedchrome.js credit()): only the ⓘ shows. The button is 14px to
 * see and 34px to hit. */
.maplibregl-ctrl.maplibregl-ctrl-attrib.ec-credit.ec-folded{background:none;padding:0}
.ec-credit .maplibregl-ctrl-attrib-inner{display:inline}
.ec-credit.ec-folded .maplibregl-ctrl-attrib-inner{display:none}
.ec-credit-i{position:relative;display:inline-grid;place-items:center;width:14px;height:14px;margin:0 5px 0 -2px;padding:0;
  vertical-align:-3px;border:0;border-radius:50%;cursor:pointer;
  font:italic 700 9px/1 Georgia,serif;background:rgba(12,12,15,.45);color:rgba(255,255,255,.8)}
.ec-folded .ec-credit-i{margin:0}
.ec-credit-i::after{content:"";position:absolute;inset:-10px}
.ec-credit-i:hover{color:#fff;background:rgba(12,12,15,.7)}
.ec-credit-i:focus-visible{outline:2px solid var(--red);outline-offset:1px}

/* the cluster itself — compass, zoom pair, basemap; 12px off the corner */
.mapbox .maplibregl-ctrl.ectl{
  margin:0 12px 12px 0;display:flex;flex-direction:column;align-items:flex-end;gap:8px;background:none;box-shadow:none}
.ectl button{font-family:inherit;color:var(--fg);cursor:pointer;padding:0}
.ectl button:focus-visible{outline:2px solid var(--red);outline-offset:-2px}

.ectl .ec-compass{
  width:34px;height:34px;border-radius:50%;display:grid;place-items:center;touch-action:none;cursor:grab;
  border:1px solid rgba(255,255,255,.1);background:rgba(17,17,20,.9);backdrop-filter:blur(12px)}
.ectl .ec-compass:hover{border-color:rgba(255,255,255,.3)}
.ectl .ec-compass svg{width:21px;height:21px;transition:transform .5s var(--ease)}
.ectl .ec-compass.dragging{cursor:grabbing}
.ectl .ec-compass.dragging svg{transition:none}

.ectl .ec-zoom{
  width:34px;display:flex;flex-direction:column;border-radius:12px;overflow:hidden;
  background:rgba(17,17,20,.88);border:1px solid rgba(255,255,255,.1);backdrop-filter:blur(12px)}
.ectl .ec-zoom button{
  width:100%;height:34px;border:0;border-bottom:1px solid var(--hair);background:transparent;
  font-size:18px;line-height:1;display:grid;place-items:center}
.ectl .ec-zoom button:last-child{border-bottom:0}
.ectl .ec-zoom button:hover{background:rgba(255,255,255,.06)}
.ectl .ec-zoom button:active{background:rgba(255,255,255,.14);transition-duration:0s}

.ectl .ec-bm{position:relative;display:flex}
.ectl .ec-bmbtn{
  width:34px;height:34px;border-radius:12px;display:grid;place-items:center;
  border:1px solid rgba(255,255,255,.1);background:rgba(17,17,20,.88);backdrop-filter:blur(12px)}
.ectl .ec-bmbtn:hover{background:rgba(255,255,255,.06)}
/* the product's own `layers` glyph at the main map's weight: v2_tags.ICONS
 * ["layers"] at stroke-width 1.8 — the same icon the Layers button renders */
.ectl .ec-bmbtn svg{width:16px;height:16px}
.ectl .ec-bmbtn[aria-expanded="true"]{
  background:rgba(255,45,45,.16);border-color:rgba(255,45,45,.55);color:var(--red-link)}

/* the popover opens upward from the button and is inert until .open */
.ectl .ec-bmpop{
  position:absolute;right:0;bottom:calc(100% + 8px);width:196px;padding:10px;border-radius:14px;
  display:none;flex-direction:column;gap:9px;
  background:rgba(17,17,20,.94);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(12px);
  box-shadow:0 18px 40px -18px rgba(0,0,0,.9)}
.ectl .ec-bmpop.open{display:flex}
.ectl .ec-cap{font-size:10px;letter-spacing:.12em;color:var(--muted);font-weight:600;text-transform:uppercase}
.ectl .ec-bmgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}
.ectl .ec-bmgrid button{
  display:flex;flex-direction:column;gap:5px;padding:5px;border-radius:10px;
  border:1px solid var(--hair);background:var(--surface);color:var(--fg2);text-align:left}
.ectl .ec-bmgrid button:hover{border-color:rgba(255,255,255,.22);color:var(--fg)}
.ectl .ec-bmgrid .sw{height:24px;border-radius:6px}
.ectl .ec-bmgrid .nm{font-size:10px;font-weight:600}
.ectl .ec-bmgrid button[aria-pressed="true"]{
  border-color:rgba(255,45,45,.6);background:rgba(255,45,45,.12);color:var(--fg)}
/* Per-page map heights. These are TWO classes deliberately, and every one of
 * them is answered again inside the one breakpoint: a height declared here
 * beats the breakpoint's bare `.mapbox` on specificity, and a height declared
 * in a template's `style=` is not reachable by the breakpoint at all. A map
 * that stays 480px tall on a 375px phone is what an inline height looks like
 * from the outside. `.sharemap` is the exception that proves it — its height
 * is viewport-relative, so it needs no phone rule. */
.mapbox.webmap{height:420px}
.mapbox.spanmap{height:480px}
.mapbox.gapmap{height:500px}
.mapbox.sharemap{height:min(62vh,560px);border-radius:20px}

/* ---- misc components ---- */
.steps{display:flex;gap:8px;align-items:center;margin-bottom:40px}
.steps .st{display:flex;align-items:center;gap:8px;flex:1}
.steps .n{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;font-size:12px;font-weight:600;border:1px solid var(--hair-strong);color:var(--faint);font-family:var(--mono)}
.steps .l{font-size:13px;color:var(--faint)}
.steps .line{flex:1;height:1px;background:var(--hair);margin-left:8px}
.steps .st.done .n{background:var(--red);border-color:var(--red);color:var(--fg)}
.steps .st.done .l{color:var(--fg)}.steps .st.done .line{background:var(--red)}
.steps .st.now .n{background:rgba(255,45,45,.15);border-color:var(--red);color:var(--fg)}
.steps .st.now .l{color:var(--fg)}
.srcrow{background:var(--map);border:1px solid var(--hair);border-radius:var(--r-row);padding:16px;display:grid;grid-template-columns:160px minmax(0,1fr) auto;align-items:center;gap:16px}
.srcrow .logo{min-height:96px;display:grid;place-items:center}
.srcrow .name{font-size:15px;font-weight:600}
.srcrow .desc{font-size:12.5px;color:var(--muted);line-height:1.5;text-wrap:pretty}
.onboard{max-width:720px;margin:0 auto;padding:72px 40px 96px}
.srcrow .btn{width:200px;max-width:100%;height:48px;padding:0 22px;border-radius:var(--r-btn);font-size:13.5px;font-weight:600;justify-self:start}
/* Strava's own button, scaled to the column by width alone (474×96 → 200×~40);
 * the file is never altered. */
.srcrow .stravabtn{display:block;width:200px;max-width:100%;line-height:0;border-radius:8px;justify-self:start}
.srcrow .stravabtn img{display:block;width:100%;height:auto}
.srcrow .stravabtn:focus-visible{outline:2px solid var(--red);outline-offset:3px}
/* The permissions line under the dropzone, its tip opening upward. */
.infohead.perms{margin:0;gap:6px;font-size:12.5px;color:var(--muted)}
.infohead.perms .infobtn{width:28px;height:28px;margin:0}
.infohead.perms .infotip{font-size:12.5px}
.infohead.perms .infotip[data-js]{top:auto;bottom:calc(100% + 8px);max-width:none}
.infohead.perms code{color:var(--fg)}
.dropzone{border:1px dashed var(--hair-strong);border-radius:var(--r-lg);padding:22px;text-align:center;color:var(--fg2);font-size:13px;cursor:pointer;transition:border-color .2s,background .2s}
.dropzone:hover,.dropzone.over{border-color:var(--red);color:var(--fg);background:rgba(255,45,45,.04)}
.dropzone.big{padding:38px 20px;border-radius:14px}
.dropzone .t{font-size:16px;font-weight:600;color:var(--fg)}
/* ---- Sync (Claude Design handoff 2026-10-02) ----
 * A list of sources beside one panel. `.srcrow2` replaced the wrapping row
 * of `.srccard`s. Desktop is the base; the one breakpoint below turns the
 * list into the page and `?src=` into a page of its own (`.picked`). */
.synchead{margin-bottom:24px;max-width:640px}
.syncsum{margin-top:10px;font-size:12.5px;color:var(--muted);display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center}
.syncsum .n{color:var(--fg)}
.syncsum .attn{display:inline-flex;align-items:center;gap:6px;color:var(--warn)}
.syncsum .mono{color:var(--fg2)}
.syncgrid{display:grid;grid-template-columns:256px minmax(0,1fr);gap:20px;align-items:start}
.syncgrid>*{min-width:0}
.srclist{display:flex;flex-direction:column;gap:2px;min-width:0}
.srclist .cap{padding:16px 12px 6px}
.srclist>.cap:first-child,.srclist .srcov+.cap{padding-top:4px}
.srcrow2{display:flex;align-items:center;gap:12px;padding:10px 12px;border-radius:var(--r-lg);border:1px solid transparent;color:var(--fg);transition:background .18s,border-color .18s}
.srcrow2:hover{background:rgba(255,255,255,.03);color:var(--fg)}
.srcrow2.on{background:var(--raised);border-color:rgba(255,45,45,.45)}
.srcrow2.srcov{gap:10px;margin-bottom:10px;color:var(--fg2)}
.srcrow2 .body{min-width:0;flex:1;display:flex;flex-direction:column}
.srcrow2 .name{font-size:13.5px;font-weight:500;color:var(--fg)}
.srcrow2 .meta{font-size:12px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.srcrow2 .meta.warn{color:var(--warn)}
.srcrow2 .st,.hrow .st{display:inline-flex;align-items:center;gap:6px;font-size:11.5px;white-space:nowrap;color:var(--muted)}
.srcrow2 .st.ok,.hrow .st.ok{color:var(--ok)}
.srcrow2 .st.warn,.hrow .st.warn{color:var(--warn)}
.srcrow2 .count{font-size:11px;font-weight:600;color:#141418;background:var(--warn);border-radius:999px;padding:1px 7px}
.srcrow2 .caret{display:none;color:var(--faint)}
/* Not connected: a hollow dot. */
.dot.off{background:transparent;box-shadow:inset 0 0 0 1.5px var(--faint)}
/* The mark slot (`_srcmark.html`): each company's own file at the height the
 * design gives it per place, or its name in our type. The Ivory tile is the
 * brand package's Ivory; Polar's mark is for light grounds only. */
.srcmark{display:flex;align-items:center;gap:6px;line-height:0;color:var(--fg)}
.srcmark img{display:block;width:auto;max-width:100%}
.srcmark .nm{line-height:1.1;font-weight:600;white-space:nowrap}
.srcmark .tile{display:block;background:#f5f3ef;border-radius:6px;line-height:0}
/* Intervals.icu publishes its icon as a hard square (logo-512.png, served byte
 * for byte); the rounding is ours, in CSS, as intervals.icu rounds it on its
 * own pages — never cut into the file. */
.srcmark.m-intervals img{border-radius:22%}
.srcmark.row{height:22px}
.srcmark.row .nm{font-size:13.5px}
.srcmark.row.m-strava img{height:14px}.srcmark.row.m-rwgps img{height:18px}.srcmark.row.m-wahoo img{height:28px}
.srcmark.row.m-polar .tile{padding:5px 8px}.srcmark.row.m-polar img{height:11px}.srcmark.row.m-google img{height:24px}
.srcmark.row.m-intervals img{height:20px}.srcmark.row.m-coros img{height:15px}
/* Settings' Connections card draws the row mark inside `.rows .row`, which
   would otherwise lend it a row's padding, top rule and space-between. */
.rows .srcmark.row{padding:0;border-top:0;flex-wrap:nowrap;justify-content:flex-start;gap:6px}
.srcmark.panel{min-height:36px}
.srcmark.panel .nm{font-size:16px}
.srcmark.panel.m-strava img{height:18px}.srcmark.panel.m-rwgps img{height:24px}.srcmark.panel.m-wahoo img{height:36px}
.srcmark.panel.m-polar .tile{padding:7px 10px}.srcmark.panel.m-polar img{height:15px}.srcmark.panel.m-google img{height:30px}
.srcmark.panel.m-intervals img{height:26px}.srcmark.panel.m-coros img{height:20px}
.srcmark.connect{gap:8px}
.srcmark.connect .nm{font-size:15px;letter-spacing:-.01em}
.srcmark.connect.m-wahoo img{height:40px}
.srcmark.connect.m-polar .tile{padding:9px 14px;border-radius:8px}.srcmark.connect.m-polar img{height:18px}
.srcmark.connect.m-google img{height:26px}.srcmark.connect.m-intervals img{height:24px}
.srcmark.connect.m-coros img{height:34px}
.srcmark.card .nm{font-size:17px}
.srcmark.card.m-google,.srcmark.card.m-intervals{flex-direction:column;gap:2px}
.srcmark.card.m-strava img{height:16px}.srcmark.card.m-rwgps img{height:24px}.srcmark.card.m-wahoo img{height:44px}
.srcmark.card.m-polar .tile{padding:10px 14px}.srcmark.card.m-polar img{height:20px}.srcmark.card.m-google img{height:38px}
.srcmark.card.m-intervals img{height:34px}.srcmark.card.m-coros img{height:28px}
/* The panel. */
.srcmain{display:flex;flex-direction:column;gap:14px;min-width:0}
.syncback{display:none}
.srcpanel .phead{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:14px 20px}
.srcpanel .pwho{min-width:0}
.pname{margin:0;display:flex;align-items:center;min-height:36px;font-size:16px;font-weight:600}
.pstatus{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:4px;font-size:12.5px;color:var(--muted)}
.pstatus.ok{color:var(--ok)}.pstatus.warn{color:var(--warn)}
.pstatus .who{color:var(--muted)}
.pacts{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.btn.connect{min-height:48px;padding:0 18px;font-size:13.5px}
.stravabtn img{display:block;height:48px;width:auto}
.pnotice,.capnote{margin-top:18px}
.pnotice>svg,.ovattn>svg{color:var(--warn)}
.notice .nb{min-width:0;flex:1}
.notice .nh{color:var(--fg);font-weight:600}
.notice .nacts{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
.notice a.ul{color:var(--fg)}
.pimport{margin-top:18px;padding:16px}
.pimport .impn{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap}
.pimport .impn span+span{font-size:13px;color:var(--fg2)}
.pimport .progress{margin:12px 0}
.pimport p{font-size:13px;color:var(--fg2);line-height:1.55;text-wrap:pretty}
.pwhat{margin-top:16px;font-size:13px;color:var(--fg2);line-height:1.6;max-width:600px;text-wrap:pretty}
.pfigs{margin-top:20px;padding-top:18px;border-top:1px solid var(--hair-faint)}
.pfigs.top{margin-top:16px;padding-top:0;border-top:0}
.pfigs .fig{display:flex;flex-direction:column;gap:4px;min-width:0}
.pfigs .l{font-size:12px;color:var(--fg2)}
.pfigs .v{font-size:14px}
.pfigs .v.warn{color:var(--warn)}
.preads{display:flex;align-items:center;gap:8px 12px;flex-wrap:wrap;margin-top:16px}
.preads .l{font-size:12px;color:var(--fg2)}
/* COROS's Reads row carries an "i" (what COROS's own page asks for). Two
 * classes, so the row keeps its own margin and gap whatever .infohead says. */
.preads.infohead{margin:16px 0 0;gap:8px 12px}
.preads.infohead .infobtn{margin:-6px -8px -6px -10px}
.chip{display:inline-flex;align-items:center;gap:6px;font-family:var(--mono);font-size:11.5px;color:var(--fg2);background:rgba(255,255,255,.04);border:1px solid var(--hair-faint);border-radius:6px;padding:3px 8px}
.chip.ok{color:var(--ok)}.chip.warn{color:var(--warn)}.chip.muted{color:var(--muted)}
.chip .cd{width:6px;height:6px;border-radius:50%;background:currentColor}
.pcredit{display:flex;align-items:center;justify-content:space-between;gap:10px 20px;flex-wrap:wrap;margin-top:20px;padding-top:16px;border-top:1px solid var(--hair-faint);font-size:12px;color:var(--muted)}
.pcredit .fg2,.pcredit a.mono{color:var(--fg2)}
/* Activity notes, earlier activities, and the read-only card. */
.infohead.inner{margin:0}
.infohead h3{font-size:15px;font-weight:600;margin:0}
.infohead.inner h2{font-size:16px;font-weight:600;margin:0}
.notescard .nhead{display:flex;align-items:flex-start;gap:16px}
.notescard .nhead .infohead{flex:1;min-width:0}
.nbody{gap:18px;margin-top:20px;padding-top:18px;border-top:1px solid var(--hair-faint)}
.nslot{min-width:0}
.nslot .caps{margin-bottom:8px}
.nslot .fine{margin-top:10px}
.note.slot{border:1px dashed rgba(255,255,255,.16)}
.nside{gap:12px;min-width:0}
.kv .perm{display:flex;align-items:center;gap:7px}
.kv .perm.ok{color:var(--ok)}.kv .perm.warn{color:var(--warn)}
.kv .perm .mono{font-size:12px}
.nside .kv .v{font-size:12.5px}
.nperm{font-size:12.5px;color:var(--fg2);line-height:1.55}
.nside .nacts{display:flex;gap:8px;flex-wrap:wrap;margin-top:auto}
.noff{margin-top:20px;padding-top:18px;border-top:1px solid var(--hair-faint);font-size:12.5px;color:var(--muted);line-height:1.6;max-width:520px}
.runsh{margin:22px 0 4px}
.run{display:grid;grid-template-columns:150px 80px minmax(0,1fr);gap:4px 16px;padding:10px 0;border-top:1px solid rgba(255,255,255,.05);font-size:12.5px;align-items:baseline}
.run .when{font-size:12px;color:var(--fg2)}
.run .res{color:var(--muted)}
.run .ok{color:var(--ok)}.run .warn{color:var(--warn)}.run .dim{color:var(--muted)}
.ronote{margin-top:6px;font-size:13px;color:var(--fg2)}
.ehead{display:flex;flex-wrap:wrap;align-items:flex-start;justify-content:space-between;gap:14px 20px}
.ebody{min-width:0;flex:1 1 320px}
.eline{margin-top:6px;font-size:13px;color:var(--fg2)}
.pdone{margin-top:16px;align-items:center}
.pdone .fgc{color:var(--fg)}
/* Recent syncs, and Overview's health table. */
.sech{display:flex;align-items:center;gap:8px;margin:10px 0;font-size:15px;font-weight:600}
.sech .cnt{font-size:12px;color:var(--muted);font-weight:400}
.srows,.health{overflow:hidden}
.srow{display:grid;grid-template-columns:150px 110px minmax(0,1fr);padding:12px 16px;border-top:1px solid rgba(255,255,255,.05);font-size:12.5px;align-items:baseline}
.srows.all .srow{grid-template-columns:150px 130px 110px minmax(0,1fr)}
.srow.sh,.hrow.hh,.drow.dh{background:var(--sunken);font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);font-weight:500;border-top:0}
.srow.sh{padding:10px 16px}
.srow .when{font-size:12px;color:var(--fg2)}
.srow .res{color:var(--fg2)}
.jst{display:inline-flex;align-items:center;gap:7px}
.jst .jd{width:6px;height:6px;border-radius:50%;background:currentColor}
.jst.ok{color:var(--ok)}.jst.warn{color:var(--warn)}.jst.dim{color:var(--muted)}
.srempty{padding:12px 16px;border-top:1px solid rgba(255,255,255,.05)}
.ovattn .ovopen{white-space:nowrap;font-size:12.5px}
.hrow{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr) 140px 140px 110px;gap:12px;padding:13px 18px;border-top:1px solid rgba(255,255,255,.05);font-size:13px;align-items:center;color:var(--fg)}
.hrow.hh{padding:10px 18px}
a.hrow:hover{background:rgba(255,255,255,.03);color:var(--fg)}
.hrow .nm{font-weight:500}
.hrow .st{font-size:12.5px}
.hrow .mono{font-size:12px;color:var(--fg2)}
.hrow .r{text-align:right}
.hrow .mono.r{font-size:12.5px;color:var(--fg)}
.hfoot{padding:11px 18px;border-top:1px solid rgba(255,255,255,.06);background:var(--sunken);font-size:12.5px;color:var(--muted)}
/* Duplicate activities. */
.dupt{margin-top:18px;border:1px solid var(--hair-faint);border-radius:var(--r-lg);overflow:hidden}
.drow{display:grid;grid-template-columns:minmax(0,1fr) 150px 170px 120px;gap:4px 12px;padding:11px 14px;border-top:1px solid rgba(255,255,255,.05);font-size:13px;align-items:baseline}
.drow.dh{padding:9px 14px}
.drow .r{text-align:right}
.drow .nm{font-weight:500}
.drow .dv{display:flex;justify-content:flex-end;gap:8px}
.drow .dv i{display:none;font-style:normal;font-size:12px;color:var(--muted)}
.drow .dv b{font-weight:400;font-size:12.5px}
.drow .dv .dim{color:var(--fg2)}.drow .dv .warn{color:var(--warn)}
.dupq{padding:16px 18px;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr) auto;gap:14px 20px;align-items:center}
.dupq .qa{min-width:0}
.dupq .qb{min-width:0;padding-left:20px;border-left:1px solid var(--hair-faint)}
.dupq .qn{display:block;font-size:13.5px;margin-top:3px}
.dupq a.qn{color:var(--fg);text-decoration:underline;text-underline-offset:3px;text-decoration-color:rgba(255,255,255,.3)}
.dupq .qm{font-size:11.5px;color:var(--fg2)}
.dupq .qacts{display:flex;gap:6px;flex-wrap:wrap;justify-content:flex-end}
/* Nobody connected: the first-visit cards. */
.firstcards{display:grid;grid-template-columns:repeat(auto-fill,minmax(270px,1fr));gap:12px;max-width:960px}
.firstcard{position:relative;background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);padding:18px;display:flex;flex-direction:column;gap:10px;min-height:196px}
.firstcard.featured{background:var(--raised);border-color:rgba(255,45,45,.45)}
.starthere{position:absolute;top:-10px;left:16px;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;font-weight:600;border-radius:6px;padding:2px 8px;background:var(--red);color:#fff}
.firstcard .band{height:88px;display:grid;place-items:center;background:var(--map);border:1px solid var(--hair-faint);border-radius:var(--r-lg);padding:12px}
.firstcard .what{font-size:13px;color:var(--fg2);line-height:1.55;text-wrap:pretty;flex:1}
.firstcard>.btn{align-self:flex-start}
.uploadline{margin-top:20px;font-size:13px;color:var(--muted);line-height:1.6}
/* The Disconnect confirmation. The hold marks the form `aria-busy` while the
 * service answers (v2.js § one submission per form): Cancel greys and the
 * waiting line shows. */
.dlgdisc h2{font-size:17px;font-weight:600;margin-bottom:10px}
.dlgdisc .dwhat p{font-size:13.5px;color:var(--fg2);line-height:1.6;text-wrap:pretty}
.dlgdisc .dwhat p+p{margin-top:10px}
.dlgdisc .dwhat b{color:var(--fg);font-weight:600}
.choices{display:flex;flex-direction:column;gap:8px;margin-top:14px}
.choice{display:flex;gap:12px;align-items:flex-start;padding:12px 14px;border-radius:var(--r-lg);border:1px solid rgba(255,255,255,.1);cursor:pointer}
.choice:has(input:checked){border-color:rgba(255,45,45,.45);background:rgba(255,45,45,.05)}
.choice input{accent-color:var(--red);margin:3px 0 0;width:16px;height:16px;flex:0 0 auto}
.choice b{display:block;font-size:13.5px;font-weight:500;color:var(--fg)}
.choice b+span{display:block;font-size:12.5px;color:var(--fg2);line-height:1.5;margin-top:2px}
.dlgdisc .dlgwait{display:none;margin-top:12px;font-size:12px;color:var(--muted);text-align:right}
.dlgdisc form[aria-busy="true"] .dlgwait{display:block}
.dlgdisc form[aria-busy="true"] [data-dialog-close]{opacity:.45;pointer-events:none}
.note{background:var(--ground);border:1px solid var(--hair);border-radius:var(--r-btn);padding:12px 14px;font-family:var(--mono);font-size:12.5px;color:var(--fg2);line-height:1.85;white-space:pre-line}
.note .u{color:var(--fg)}
.gaprow{display:flex;flex-wrap:wrap;gap:14px 24px;align-items:center;background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-row);padding:16px 20px}
.gaprow:hover{border-color:rgba(255,255,255,.18)}
.gaprow .rank{font-family:var(--mono);font-size:20px;color:var(--faint);width:32px}
.gaprow .who{flex:1 1 260px;min-width:0}
.gaprow .who .n{font-weight:500;font-size:15px}
.gaprow .who .s{font-size:12px;color:var(--muted);margin-top:2px;font-family:var(--mono)}
.gaprow .m{font-family:var(--mono);font-size:18px;font-weight:500;letter-spacing:-.02em}
.gaprow .ml{font-size:11px;color:var(--muted)}
.gaprow .tier{display:flex;align-items:center;gap:10px;font-size:12.5px;color:var(--fg2);flex:1 1 220px;min-width:0}
.gaprow .tier i{width:26px;height:0;border-top:3px solid var(--ok);flex:0 0 auto}
.gaprow .tier i.dash{border-top-style:dashed;border-top-color:var(--warn)}
.gaprow .acts{display:flex;gap:6px;flex-wrap:wrap}
.gaprow .tier .why{color:var(--muted)}
.gaprow .tier b{font-weight:500}
.gaprow .tier .okb{color:var(--ok)}
.gaprow .tier .warnb{color:var(--warn)}

/* ---- one web's gaps (web_gaps.html, 2026-10-03) ----
 * Next's row, seen from one web: the partner is the other web, so the row
 * names one web with its ink, and span gain is a column because it is a
 * sort. The whole row opens the gap (`.go`); hovering lights it on the map,
 * and a gap hovered on the map lights its row (`.hot`, the gap's amber). */
.wghead{margin-bottom:20px;align-items:flex-end}
.wghead .sub .fig{font-family:var(--mono);font-size:13.5px;color:var(--fg)}
.mapbox.webgapmap{height:340px;margin-bottom:16px}
.mapbox .key i.route{height:4px;background:linear-gradient(#fff,#fff) center/calc(100% - 6px) 1px no-repeat,var(--warn)}
.wglist{display:flex;flex-direction:column;gap:10px}
.gaprow.go{cursor:pointer}
.gaprow.hot{border-color:rgba(255,176,32,.45)}
.gaprow .who .n{display:flex;align-items:center;gap:9px;min-width:0}
.gaprow .who .n span{min-width:0}
.gaprow .sw,.wgdis .sw{width:14px;height:3px;border-radius:2px;flex:0 0 auto;display:inline-block}
.gaprow .mets{display:flex;gap:24px;align-items:flex-start}
.gaprow .m .ml{margin-left:3px}
.gaprow .m.on{color:var(--red)}
.gaprow .tier{flex-wrap:wrap}
.gaprow .tier .pend{display:inline-flex;align-items:center;gap:7px}
.gaprow .tier .pend .spin{width:9px;height:9px;border-width:1.5px}
.gaprow .acts{align-items:center}
.gaprow .acts .tag{padding:4px 8px}
.gaprow .acts .link{font-size:12.5px;padding:0 6px}
.wgmore{text-align:center;margin-top:10px}
.wgdis{margin-top:24px}
.wgdis>summary{display:flex;align-items:center;gap:8px;cursor:pointer;list-style:none;font-size:13.5px;color:var(--fg2);margin-bottom:10px}
.wgdis>summary::-webkit-details-marker{display:none}
.wgdis>summary:hover{color:var(--fg)}
.wgdis .chev{width:6px;height:6px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(-45deg);transition:transform .18s;margin-right:2px}
.wgdis[open] .chev{transform:rotate(45deg)}
.wgdis .card{overflow:hidden}
.wgdis .row{display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px;padding:12px 16px;font-size:13.5px}
.wgdis .row+.row{border-top:1px solid var(--hair-faint)}
.wgdis .nm{display:flex;align-items:center;gap:9px;flex:1 1 160px;min-width:0}
.wgdis .nm .sw{opacity:.6}
.wgdis .mono{font-size:12.5px}
.wgdis .link{font-size:12.5px}
.wgdis .note{margin-top:10px;max-width:600px;line-height:1.6}
/* A partner's name on the web's gaps map: DOM, so it can be the page's own
 * type, haloed in the map's ground. Shown for the top few by the current
 * sort, and for any other while its gap is hovered. */
.wglabel{display:none;font-family:var(--sans);font-size:11.5px;color:var(--fg2);white-space:nowrap;pointer-events:none;
  paint-order:stroke fill;-webkit-text-stroke:3px var(--map);text-shadow:0 0 3px var(--map)}
.wglabel.on{display:block}
.routing{display:inline-flex;align-items:center;gap:6px}

/* ---- a web's name, edited in place (web.html, 2026-09-28) ----
 * The pencil swaps the heading for the form on the `hidden` attribute alone.
 * `.view` sets display, which outranks the UA's [hidden] rule, so it carries
 * its own [hidden] rule; the form sets none. The column's basis is a width,
 * not 0, or it never wraps under "Open in the main map" on a phone. */
.webhead{flex:1 1 320px;min-width:0}
.webname{margin-top:8px}
.webname .view{display:flex;align-items:center;gap:8px}
.webname .view[hidden]{display:none}
.webname .view .title{min-width:0;overflow-wrap:anywhere}
.webname .view .btn{flex:0 0 auto;color:var(--muted);padding:6px}
.webname .view .btn:hover{color:var(--fg)}
.webname .fields{display:flex;flex-wrap:wrap;align-items:center;gap:8px;max-width:640px}
.webname .fields .input{flex:1 1 260px;width:auto;min-width:0;font-size:22px;font-weight:600;letter-spacing:-.02em;padding:8px 12px}
.webname .hint{margin-top:8px;line-height:1.6;max-width:560px}

/* ---- one gap and its route (Where next, 2026-09-16) ---- */
.gapacts{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
.gapcard{padding:18px 20px}
.tile .v .u{font-size:13px;font-weight:400;color:var(--fg2)}
.cxkey i.gapkey{height:6px;border-radius:3px;background:linear-gradient(var(--warn) 0 30%,#fff 30% 70%,var(--warn) 70%)}
.cxkey i.gapkey.dash{height:0;border-top:3px dashed var(--warn);background:none}
.mapbox .mapstate{position:absolute;left:50%;top:14px;transform:translateX(-50%);z-index:3;display:flex;align-items:center;gap:8px;white-space:nowrap;border-radius:999px;padding:7px 14px;font-size:12.5px;color:var(--fg);background:rgba(17,17,20,.9);border:1px solid rgba(255,255,255,.12)}
.mapbox .mapstate[hidden]{display:none}
.spin{display:inline-block;flex:0 0 auto;width:12px;height:12px;border-radius:50%;border:2px solid rgba(255,255,255,.18);border-top-color:var(--warn);animation:spin .9s linear infinite}
.spin.lg{width:24px;height:24px;border-width:3px}
.cues{list-style:none;margin:0;padding:0}
.cue{display:grid;grid-template-columns:28px minmax(0,1fr) auto;gap:12px;align-items:center;padding:11px 0;border-top:1px solid var(--hair)}
.cue:first-child{border-top:0}
.cue b{font-weight:500}
.cue .n{font-size:12px;color:var(--faint);text-align:center}
.cue .dot{width:12px;height:12px;border-radius:50%;justify-self:center;box-shadow:0 0 0 2.5px var(--ground),0 0 0 3.5px rgba(255,255,255,.25)}
.cue .s{font-size:12px;color:var(--muted);margin-top:2px}
.cue .d{font-size:13px;color:var(--fg2);white-space:nowrap}
.cue.end .d{font-size:11px;color:var(--muted)}
.surfbar{display:flex;height:10px;border-radius:5px;overflow:hidden;background:var(--sunken)}
.surfbar i,.surfkey i{display:block}
.surfbar .unpaved,.surfkey .unpaved{background:#c98a4b}
.surfbar .paved,.surfkey .paved{background:#8d929c}
.surfbar .unknown,.surfkey .unknown{background:repeating-linear-gradient(135deg,#3f3f46 0 3px,#26262c 3px 6px)}
.surfkey{margin:12px 0 0;display:flex;flex-direction:column;gap:6px;font-size:13px}
.surfkey div{display:flex;justify-content:space-between;align-items:center}
.surfkey dt{display:flex;align-items:center;gap:8px;color:var(--fg2)}
.surfkey dd{margin:0}
.surfkey i{width:10px;height:10px;border-radius:3px}
.locker{position:absolute;left:0;right:0;bottom:0;height:62%;display:grid;place-items:center;background:linear-gradient(rgba(9,9,11,0),rgba(9,9,11,.85) 40%)}
.nmcard{background:var(--surface);border:1px solid var(--hair);border-radius:var(--r-card);overflow:hidden;display:flex;flex-direction:column}
.nmcard:hover{border-color:rgba(255,255,255,.18)}
.nmcard img,.nmcard svg.th{display:block;width:100%;background:var(--map);border-bottom:1px solid var(--hair-faint);aspect-ratio:300/170;object-fit:cover}
/* The thumbnail is wrapped in a link to the editor. Blockified anyway as a
 * flex item, but stated so the card does not depend on that. */
.nmcard>a{display:block;line-height:0}
.nmcard .body{padding:16px 18px;display:flex;flex-direction:column;gap:10px;flex:1}
/* ---- the connect-the-lines editor (V2-FEATURE-GAP D3; design handoff
 * 2026-10-09) ----
 * ⚠️⚠️ THE EDITOR FILLS `.main`, LIKE THE MAP PAGE. `connect.html` overrides
 * `block main`, so nothing here is inside `.page`. The page itself does not
 * scroll; the map takes whatever height is left and the status panel is
 * always on screen.
 *
 * ⚠️⚠️ NOTHING IN THE EDITOR IS `position:fixed`, and a test holds that. The
 * panel used to be a fixed bottom sheet on a phone, and on an iPhone Save was
 * a long scroll away: `.page{animation:rise … both}` keeps the animation in
 * effect after it ends, an element with a transform animation in effect is
 * the containing block for its fixed descendants, and so the sheet pinned
 * itself to the bottom of the PAGE — under every sidebar card — and never to
 * the screen. The same trap `.ov-col` documents inside the breakpoint. Docked
 * in the normal flow there is no containing block to get wrong. */
.cxfull{position:absolute;inset:0;display:flex;flex-direction:column;overflow:auto}
.cxfull>.topline{flex:none}
.cxhead{flex:none;padding:16px 28px 14px;display:flex;flex-direction:column;gap:6px}
.cxcrumb{display:flex;align-items:center;gap:10px;font-size:13px;color:var(--fg2)}
.cxback{display:inline-flex;align-items:center;gap:6px;color:var(--fg2)}
.cxback:hover{color:var(--fg)}
/* The chevron is the phone's back button; a pointer gets the words. */
.cxback svg{display:none}
.cxsep{color:var(--chrome)}
.cxhead .eyebrow{font-size:11.5px}
.cxtitle{display:flex;align-items:baseline;gap:6px 16px;flex-wrap:wrap;min-width:0}
.cxtitle h1{font-size:26px;font-weight:600;letter-spacing:-.025em;line-height:1.15}
.cxlede{font-size:13.5px;color:var(--fg2)}
.cxshort,.cxkeybtn{display:none}
.messages.cxnotes{flex:none;margin:0 28px 14px}
/* ⚠️ `flex-basis:0`, not `auto`. With `auto` the sidebar's lists set the
 * row's height and the whole page scrolls again. 544px is the map's old 520px
 * plus this row's bottom padding: on a window too short for it `.cxfull`
 * scrolls, and the map is never shorter than it was. */
.cxwork{flex:1 1 0;min-height:544px;display:flex;gap:16px;padding:0 28px 24px}
/* ⚠️ A CLASS, never an inline height: an inline declaration cannot be
 * overridden by a media query without `!important` (measured once: 520px
 * survived onto a 375px phone). `auto` because the row's height is the map's
 * height now — and it is said again inside the breakpoint, where the bare
 * `.mapbox{height:320px}` would otherwise be the only answer on file.
 * ⚠️ `z-index:0` makes the map its own stacking context, and the phone's
 * sheet depends on it: the scrim inside the map is z 4 and `.cxside` beside
 * it is z 2, so without this the scrim painted OVER the sheet it is there to
 * set off (measured 2026-10-09 — the sheet opened greyed out). */
.mapbox.cxmap{height:auto;flex:1;min-width:0;z-index:0}
/* The two views, ON the map and next to what they change: a view only moves
 * the camera. The strip itself takes no clicks, so the map under its empty
 * right-hand side still pans. The pill is the height of every other map
 * button (34px, 38px in the breakpoint — .ectl's metrics, 2026-10-24) and sits
 * the same distance off the map's edge as that cluster. There is no Frame
 * control any more: + and − are how you see further. */
.cxtools{position:absolute;top:12px;left:12px;right:12px;z-index:3;display:flex;flex-wrap:wrap;gap:6px;pointer-events:none}
.cxtools>*{pointer-events:auto}
.cxseg{display:flex;gap:2px;height:34px;padding:2px;border-radius:999px;background:rgba(17,17,20,.9);border:1px solid rgba(255,255,255,.12);box-shadow:0 12px 28px -14px rgba(0,0,0,.8)}
.cxseg button{height:100%;padding:0 11px;border:0;border-radius:999px;background:transparent;color:var(--fg2);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
.cxseg button:hover{color:var(--fg)}
.cxseg button[aria-pressed="true"]{background:rgba(255,255,255,.14);color:var(--fg)}
/* A map that could not draw says so on the map, under the controls. */
.mapbox .cxerr{position:absolute;left:12px;right:12px;top:62px;z-index:3;border-radius:10px;padding:8px 12px;font-size:12.5px;line-height:1.5;background:rgba(17,17,20,.92);border:1px solid rgba(255,45,45,.4)}
.cxside{flex:none;width:clamp(280px,30%,360px);display:flex;flex-direction:column;gap:12px;min-height:0}
/* The status panel: always visible, FIRST in the column. It says what to do
 * before the drag, reports the distance and the snap during it, and offers
 * Save the moment the end is let go — in V1 the verdict began below the map
 * and two paragraphs of prose, so the answer to "did that drag work?" was to
 * stop and scroll (Greg, 2026-08-31).
 * The tone is three custom properties set from `data-tone`: the border (the
 * top border on a phone), the icon's tint and the ink of the icon and of the
 * figure in the title. */
.cxpanel{flex:none;padding:0 18px 16px;box-shadow:0 18px 40px -18px rgba(0,0,0,.8);--tone:var(--fg2);--tint:rgba(255,255,255,.06);--edge:var(--hair);border-color:var(--edge)}
.cxpanel[data-tone="ok"]{--tone:var(--ok);--tint:rgba(61,220,132,.14);--edge:rgba(61,220,132,.35)}
.cxpanel[data-tone="warn"]{--tone:var(--warn);--tint:rgba(255,176,32,.14);--edge:rgba(255,176,32,.35)}
.cxpanel[data-tone="bad"]{--tone:var(--red-link);--tint:rgba(255,45,45,.14);--edge:rgba(255,45,45,.4)}
/* ⚠️⚠️ ONE HEIGHT IN EVERY STATE, BY CONSTRUCTION. Under the map (a phone) a
 * panel that grew when a drag was let go would shrink the map, and
 * `map.resize()` keeps the centre, so both ends of the lines would shift
 * under a finger. `min-height` alone does not hold it: measured 2026-10-09,
 * the design's own "ready" copy is 2px taller than its 90px at 390px, and a
 * pair of long activity names adds a whole line.
 * So `.cxsay` is a one-cell grid. `.cxnow` is the state on screen; connect.js
 * adds invisible `.cxsize` blocks holding the words of every other state
 * this gap can reach, and they all sit in that one cell — the cell, and so
 * the panel, is as tall as the tallest, at every width, with nothing
 * measured in script. `min-height` is the design's floor for a gap whose
 * words are all short. */
.cxstatus{display:flex;gap:12px;align-items:flex-start;padding-top:16px;min-height:100px}
.cxico{flex:none;width:28px;height:28px;border-radius:50%;background:var(--tint);color:var(--tone);display:grid;place-items:center}
.cxico i,.cxico svg{display:none}
.cxico i{border-radius:50%}
.cxico svg{width:16px;height:16px}
/* The loose end as the map draws it: the handle's green inside its ring. */
.cxpanel[data-ico="loose"] .cxico i{display:block;width:11px;height:11px;background:var(--ok);box-shadow:0 0 0 2px #141418,0 0 0 3px rgba(61,220,132,.5)}
.cxpanel[data-ico="dot"] .cxico i{display:block;width:10px;height:10px;background:var(--ok)}
.cxpanel[data-ico="ring"] .cxico i{display:block;width:10px;height:10px;border:2px solid var(--warn)}
.cxpanel[data-ico="check"] .i-check,.cxpanel[data-ico="alert"] .i-alert,.cxpanel[data-ico="undo"] .i-undo{display:block}
.cxsay{flex:1;min-width:0;display:grid}
.cxsay>*{grid-area:1/1;min-width:0}
.cxsize{visibility:hidden;pointer-events:none}
.cxsay strong{display:block;font-size:16px;font-weight:600;line-height:1.35;text-wrap:pretty}
.cxsay strong .mono{font-size:.92em;line-height:1;color:var(--tone);white-space:nowrap}
.cxsay p{margin-top:4px;font-size:13px;line-height:1.5;color:var(--fg2);text-wrap:pretty}
/* Save takes the row and may wrap in a 280px column; the button beside it
 * keeps its words on one line. Both are thumb-tall at every width — this is
 * the one control on the page that is reached for mid-gesture. */
.cxacts{display:flex;gap:8px;margin-top:14px}
.cxacts .btn{flex:none;min-height:44px;padding:0 18px;font-size:14px;white-space:nowrap}
.cxacts .btn.primary{flex:1;min-width:0;padding:0 12px;white-space:normal;line-height:1.2}
.cxacts form{display:flex;flex:none}
.cxacts form.grow,.cxacts form.grow .btn{flex:1}
/* `.btn` and the form rule above set display, which outranks the UA's
 * [hidden] rule. */
.cxacts [hidden]{display:none}
/* Everything that is not the map or the panel: the key, the connectors, the
 * lists, the way out. It scrolls under the panel on a desktop and is a sheet
 * on a phone (inside the breakpoint). */
.cxmore{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;display:flex;flex-direction:column;gap:12px;scrollbar-width:thin}
.cxmore>*{flex:none}
.cxgrab,.cxmorehead,.cxscrim{display:none}
/* The editor's legend. Same swatch shapes as `.mapbox .key` so the two read
 * as one system; `.stack` is one ink to a row, for a column 280px wide — as
 * an overlay on the map six inks wrapped to three lines and covered the
 * bottom fifth of a 375px map. */
.cxkey{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:10px;font-size:12px;color:var(--fg2)}
.cxkey span{display:flex;align-items:center;gap:7px}
.cxkey i{display:inline-block;width:20px;height:3px;border-radius:2px}
.cxkey i.dash{height:0;border-top:3px dashed var(--warn);background:none;border-radius:0}
.cxkey.stack{flex-direction:column;flex-wrap:nowrap;gap:7px;margin-top:0;font-size:12.5px}
.cxkey.stack span{gap:10px}
.cxkey.stack i{flex:0 0 20px}
/* The two lines the gap is about wear their casing here as on the map. */
.cxkey.stack i.cased{height:4px;box-shadow:0 0 0 1px #141418}
.cxkey.stack i.dot{flex-basis:10px;margin:0 5px;box-shadow:0 0 0 2px #141418}
.cxkeynote{margin-top:12px;padding-top:10px;border-top:1px solid var(--hair-faint);font-size:12px;line-height:1.55;color:var(--muted);text-wrap:pretty}
.cxkeynote #framecount{color:var(--fg2)}
/* "How saving works": closed until asked for. The row is the summary's, as
 * `.notice.fold` does it — a <details> cannot be a flex row itself. */
.cxhow>summary{display:flex;align-items:center;gap:8px;min-height:44px;padding:0 18px;font-size:13.5px;font-weight:600;cursor:pointer;list-style:none}
.cxhow>summary::-webkit-details-marker{display:none}
.cxhow>summary svg{margin-left:auto;color:var(--muted);transition:transform .15s}
.cxhow[open]>summary svg{transform:rotate(180deg)}
.cxhowbody{padding:0 18px 14px;display:flex;flex-direction:column;gap:8px;font-size:12.5px;line-height:1.6;color:var(--fg2);text-wrap:pretty}
/* One activity in the column's list, its swatch painted from the map's own ink by
 * connect.js so "which line is that?" is answerable without hovering every
 * line on a 400 m frame. */
.cxrow{display:flex;gap:10px;align-items:baseline;padding:9px 0;border-top:1px solid var(--hair-faint);font-size:13px}
.cxrow:first-child{border-top:0;padding-top:0}
.cxsw{flex:0 0 auto;width:12px;height:3px;border-radius:2px;background:var(--chrome);margin-top:6px}
/* The Crop page (2026-09-15). The map's height is a class, declared here and
 * again inside the one 720px block, for the reason `.cxmap` gives above. */
.mapbox.cropmap{height:460px}
.susth{display:block;width:120px;height:80px;border-radius:10px;background:var(--map);border:1px solid var(--hair-faint)}
.cropslider.cut .track{background:var(--red)}
.cropslider.cut .fill{background:rgba(77,163,255,.85)}
.cropmodes{display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap}
.cropwhy{border-left:3px solid var(--warn);padding:2px 0 2px 12px;margin-bottom:14px}
.cxkey i.dot{width:10px;height:10px;border-radius:50%}
/* The dual-handle slider: two native ranges stacked on one track, each
 * transparent and click-through except at its thumb, with the kept stretch
 * painted red on a blue track — Strava's kept/removed colours, and the map's.
 * Native inputs rather than a drawn widget so the keyboard, a screen reader
 * and a coarse pointer all get the browser's own handling for free. */
.cropslider{position:relative;height:36px;margin:4px 0 2px}
.cropslider .track{position:absolute;left:0;right:0;top:15px;height:6px;border-radius:3px;background:rgba(77,163,255,.45)}
.cropslider .fill{position:absolute;top:0;bottom:0;left:0;right:0;border-radius:3px;background:var(--red)}
.cropslider input[type=range]{position:absolute;left:0;top:0;width:100%;height:36px;margin:0;padding:0;background:none;-webkit-appearance:none;appearance:none;pointer-events:none;outline:none;z-index:2}
.cropslider input[type=range].top{z-index:3}
.cropslider input[type=range]::-webkit-slider-runnable-track{background:none;height:36px}
.cropslider input[type=range]::-moz-range-track{background:none;height:36px}
.cropslider input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;pointer-events:auto;width:22px;height:22px;margin-top:7px;border-radius:50%;border:2px solid #0d0d0f;background:#3ddc84;cursor:grab;box-shadow:0 2px 8px rgba(0,0,0,.5)}
.cropslider input[type=range]::-moz-range-thumb{pointer-events:auto;width:18px;height:18px;border-radius:50%;border:2px solid #0d0d0f;background:#3ddc84;cursor:grab;box-shadow:0 2px 8px rgba(0,0,0,.5)}
.cropslider input#cropend::-webkit-slider-thumb{background:#ffb020}
.cropslider input#cropend::-moz-range-thumb{background:#ffb020}
.cropslider input[type=range]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px rgba(255,45,45,.4)}
.cropslider input[type=range]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px rgba(255,45,45,.4)}
.cropslider input[type=range]:disabled::-webkit-slider-thumb{opacity:.45;cursor:not-allowed}
/* A finger, not a cursor: crop.js adds `.coarse` on a coarse pointer, so the
 * thumbs grow to a real hit target without a second @media in this file. */
.cropslider.coarse input[type=range]::-webkit-slider-thumb{width:28px;height:28px;margin-top:4px}
.cropslider.coarse input[type=range]::-moz-range-thumb{width:24px;height:24px}
.cropends{display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-top:10px;align-items:start}
.cropend.keep{text-align:center}
.cropend.last{text-align:right}
.cropend .num{white-space:nowrap}
.nudge{display:inline-flex;gap:4px;margin-top:6px}
.nudge .btn{min-width:38px;padding:5px 0;font-size:18px;line-height:1}
/* Photo pins on an activity map (2026-09-13). A MapLibre Marker positions its
   element with `position:absolute` and a `transform`, so NOTHING here may set
   either on .photopin itself — this sheet loads before maplibre-gl.css on some
   pages and after it on others, and a transform here would pin every photo to
   the map's corner. Hover scales the IMAGE, not the button. */
.photopin{width:44px;height:44px;box-sizing:border-box;border-radius:50%;overflow:hidden;padding:0;margin:0;background:#1a1a1f;cursor:pointer;box-shadow:0 2px 10px rgba(0,0,0,.5);display:block}
.photopin.fix{border:2px solid #fff}
.photopin.guess{border:2px dashed #4dd2ff}
.photopin img{width:100%;height:100%;object-fit:cover;display:block;transition:transform .15s;pointer-events:none}
.photopin:hover img,.photopin:focus-visible img{transform:scale(1.1)}
.photopin:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.photopin.bare{width:16px;height:16px;background:#4dd2ff}
.photopin.guess.bare{background:#0c0c0f}
.photopin .more{position:absolute;left:0;right:0;bottom:0;font:600 10px/14px var(--mono);color:#fff;background:rgba(0,0,0,.62);text-align:center;display:none}
.photopin.stacked .more{display:block}
.photopin.gone{display:none}
/* The activity page's sidebar (2026-09-16 handoff). The four figures read as
 * number-then-label on one baseline, two columns at EVERY width — this is not
 * `.grid2`, which the breakpoint folds to one column and which made the card
 * push the meta list off a phone's first screen. */
.actstats{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:9px 14px}
.actstats>div{display:flex;align-items:baseline;gap:6px;min-width:0}
.actstats .n{font-family:var(--mono);font-weight:500;letter-spacing:-.02em;font-size:21px;white-space:nowrap}
.actstats .n.sm{font-size:15px}
.actstats .l{font-size:11px;color:var(--fg2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.actstats+.hr{margin:14px 0}
/* Every source that delivered the activity, arrival order. The one the map
 * draws is tagged; the rest say they were dropped as duplicates. */
.srcs{margin-top:14px;padding-top:14px;border-top:1px solid var(--hair-faint)}
.srcs .head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:9px;font-size:13px;color:var(--fg2)}
.srcs .head .mono{font-size:11px;color:var(--faint)}
.srcs .list{display:flex;flex-direction:column;gap:9px}
.srcs .src{display:flex;align-items:center;justify-content:space-between;gap:10px;min-width:0}
.srcs .who{display:flex;flex-direction:column;min-width:0}
.srcs .nm{font-size:12.5px;color:var(--fg);display:flex;align-items:center;gap:6px}
.srcs .id{font-family:var(--mono);font-size:11px;color:var(--faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.srcs .drawn{font-size:9.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--red);border:1px solid rgba(255,45,45,.35);border-radius:5px;padding:1px 5px}
.srcs .view{color:var(--fg2);font-size:12px;text-decoration:underline;text-underline-offset:3px;white-space:nowrap;flex:0 0 auto}
.srcs .view:hover{color:var(--fg)}
.srcs .view.strava,.srcs .view.strava:hover{color:#fc5200;font-weight:600}
.photostrip{display:flex;gap:10px;overflow-x:auto;padding:16px 0 4px;scrollbar-width:thin}
.photostrip a{flex:0 0 auto;width:180px;height:130px;border-radius:var(--r-lg);border:1px solid var(--hair);overflow:hidden;background:var(--map);transition:transform .15s;position:relative}
.photostrip a:hover{transform:translateY(-3px)}
.photostrip img{width:100%;height:100%;object-fit:cover;display:block}
.photostrip .cap{position:absolute;left:10px;bottom:10px;font-size:11px;color:rgba(255,255,255,.7);font-family:var(--mono)}
/* A photo Strava is still processing: the strip's box, dashed and empty, with
   the words — never Strava's own grey placeholder graphic dressed as a photo. */
.photostrip .pending{flex:0 0 auto;width:180px;height:130px;border-radius:var(--r-lg);border:1px dashed var(--hair-strong);display:grid;place-items:center;padding:0 18px;text-align:center;font-family:var(--mono);font-size:11px;letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
.milestone{display:flex;align-items:center;gap:14px}
.milestone .ck{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;flex:0 0 auto;border:1px solid var(--hair-strong)}
.milestone.done .ck{background:var(--red);border-color:var(--red)}
.bar-thin.grow>span{transition:width 1s}
/* ---- Stats: the unique-miles card (figure + curve, ONE card) ----
   The number IS the right-hand end of the line, so they share a surface. */
.uniquecard{padding:26px 24px 18px;margin-bottom:16px}
.uhead{display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:16px}
.statnum{font-family:var(--mono);font-size:96px;font-weight:500;letter-spacing:-.05em;line-height:1;margin:10px 0 8px;font-variant-numeric:tabular-nums}
.uyear{text-align:right;flex:0 0 auto}
.uyear .l{font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
.uyear .v{font-family:var(--mono);font-size:20px;font-weight:500;letter-spacing:-.02em;margin-top:4px;color:var(--link);white-space:nowrap;font-variant-numeric:tabular-nums}
.uyear .n{font-size:11.5px;color:var(--faint);margin-top:2px;white-space:nowrap}
.tiles{margin-bottom:20px}
/* The growth chart's axis labels: HTML over the SVG, so they stay 11px at
   every card width. See _growth.html and views._growth_chart. The SVG ends at
   its baseline and the year labels hang BELOW it, in a strip of fixed height
   reserved by the margin — a strip inside the viewBox scaled with the card. */
.gwrap{position:relative;margin:12px 0 18px}
.gwrap .gy,.gwrap .gx{position:absolute;font-size:11px;line-height:1;color:var(--faint);font-family:var(--mono);pointer-events:none;white-space:nowrap}
.gwrap .gy{left:0;transform:translateY(-50%)}
.gwrap .gx{top:calc(100% + 5px);transform:translateX(-50%)}
/* Records: four figures, 2×2. Static — they take no part in the count-up. */
.records{gap:16px}
.records .num{font-size:24px}
.records .rl{font-size:12px;color:var(--fg2);margin-top:2px}
.records .rd{font-size:12px;color:var(--muted)}
.records .rd a{color:inherit}
.records .rd a:hover{color:var(--link)}
/* The web swatch before a name: the map's ink for that web's size rank. */
.swatch{display:inline-block;width:10px;height:3px;border-radius:2px;margin-right:10px;vertical-align:middle}

/* The streak card (Stats). Rest days are training, not failures: there is no
 * alarm state here and there must not be one (handoff README § 12). */
.streakcard{padding:22px 24px;margin-bottom:20px}
/* ⚠️ THE LEFT TRACK IS A FLOOR, NOT A WIDTH. A hard `190px` here is narrower
 * than the odometer the moment the streak reaches three digits — four slabs
 * plus DAYS measures 274px — and a fixed track does not grow, so the counter
 * ran 56px into the rail (Greg, 2026-09-20, on a 2,978-day streak). `minmax`
 * keeps 190px as the base so a two-digit streak looks exactly as designed,
 * and lets the track take its content's width past that; the rail's
 * `minmax(0,1fr)` absorbs the difference. */
.streak{display:grid;grid-template-columns:minmax(190px,max-content) minmax(0,1fr);gap:28px;align-items:center;margin-top:16px}
.streak .left{display:flex;flex-direction:column;gap:10px;align-items:flex-start}
.streak .fig{display:flex;align-items:baseline;gap:8px}
.streak .fig b{font-family:var(--mono);font-size:54px;font-weight:500;letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums}
/* `>span`, never ` span`: the count-up's own <span data-count> sits inside the
 * <b>, and a descendant selector shrank the 54px figure to the suffix's 14px. */
.streak .fig>span{font-size:14px;color:var(--fg2)}
.streak .when{font-size:12.5px;color:var(--muted);font-family:var(--mono)}
.streak .pill{display:flex;align-items:center;gap:8px;border-radius:999px;padding:6px 12px;font-size:12.5px;background:rgba(61,220,132,.08);border:1px solid rgba(61,220,132,.28);color:var(--ok)}
.streak .pill.warn{background:rgba(255,176,32,.06);border-color:rgba(255,176,32,.28);color:var(--warn)}
.streak .pill i{width:7px;height:7px;border-radius:50%;background:currentColor;flex:0 0 auto;animation:pulse 1.6s ease-in-out infinite}
/* The odometer. Digit slabs, minimum two — the number is what people care
   about, and a flame icon adds nothing a figure does not already say. */
.odo{display:flex;gap:6px;align-items:stretch}
.odo>span{background:var(--well);border:1px solid rgba(255,255,255,.1);border-radius:10px;padding:10px 4px;min-width:52px;display:grid;place-items:center;font-family:var(--mono);font-size:54px;font-weight:500;letter-spacing:-.04em;line-height:1;color:var(--fg);font-variant-numeric:tabular-nums}
.odo>b{align-self:flex-end;font-size:13px;font-weight:400;color:var(--muted);padding:0 0 12px 6px;letter-spacing:.06em;text-transform:uppercase}
.streak .when{margin-top:14px}
/* The rail, up to RAIL_CELL_MAX days: real elements, one per day, each with
   its own title. Height encodes the new line found that day. */
.streak .cells{display:flex;gap:2px;align-items:flex-end;height:44px}
.streak .cells span{flex:1 1 0;min-width:0;border-radius:1px;background:rgba(255,255,255,.07)}
.streak .cells span.live{background:var(--red)}
.streak .cells span.active{background:var(--red-deep)}
.streak .axis{display:flex;justify-content:space-between;margin-top:6px;font-size:11px;color:var(--faint);font-family:var(--mono)}
/* Past it, an SVG stretched to the box: 90 to 365 bars cannot be a flex row
   without each one collapsing sub-pixel. preserveAspectRatio="none", so the
   pitch is a ratio and holds at every window on the ladder. */
.streak .yearrail{display:block;width:100%;height:44px}
.streak .yearrail rect{fill:rgba(255,255,255,.11)}
.streak .yearrail rect.live{fill:var(--red)}
.streak .yearrail rect.active{fill:var(--red-deep)}
.streak .runmark{position:relative;height:15px}
.streak .runmark .rule{position:absolute;top:0;min-width:2px;height:2px;background:var(--red);border-radius:2px}
/* ⚠️ `line-height:1` is load-bearing: the row is 15px and the label sits
 * 3px down it, so an inherited 1.5 line box is 15.8px tall and hangs
 * 3.8px below its own row — 1.75px of that landed on the month label
 * under it, which is the crowding around 'Sep' in Greg's screenshot. */
.streak .runmark .lbl{position:absolute;right:0;top:3px;line-height:1;font-size:10.5px;color:var(--link);font-family:var(--mono);white-space:nowrap}
.streak .months{position:relative;height:14px;margin-top:2px}
.streak .months span{position:absolute;font-size:10px;color:var(--faint);font-family:var(--mono)}
.streak .winnote{font-size:11px;color:var(--faint);font-family:var(--mono)}
.streak .winfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:baseline;gap:6px 12px;margin-top:10px}
.winfoot .link{font-size:12px;color:var(--fg2)}
.winfoot .link:hover{color:var(--fg)}
/* A chart that opens a dialog (v2.js § dialogs gives it role=button). The
 * ring is the keyboard's; a pointer gets the faint wash. */
.chartbtn[role=button]{cursor:pointer;border-radius:10px;outline:none}
.chartbtn[role=button]:hover{background:rgba(255,255,255,.015)}
.chartbtn[role=button]:focus-visible{box-shadow:0 0 0 2px var(--red)}
/* The two Stats tiles that are links to /webs/. */
a.tile.go{display:block;color:inherit;transition:border-color .18s}
a.tile.go:hover{border-color:rgba(255,255,255,.2);color:inherit}
a.tile.go:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.tile .l.arr{display:flex;justify-content:space-between;gap:8px}

/* ---- Webs (design handoff 2026-09-28 § 3) ---- */
.h16{font-size:16px;font-weight:600}
.webshead{margin-bottom:22px;min-width:0}
.webshead .title{margin-top:8px}
.webshead .sub{margin-top:6px;max-width:640px}
/* The page's two tabs (design handoff 2026-09-30). The 2px underline sits ON
   the bar's 1px rule (`margin-bottom:-1px`). Content-wide on the left here;
   halves of the width on a phone. Focus is the app's own ring. */
.wtabs{display:flex;gap:4px;border-bottom:1px solid var(--hair);margin:0 0 22px}
.wtab{flex:0 0 auto;display:flex;align-items:baseline;justify-content:center;gap:8px;padding:12px 14px;min-height:44px;margin-bottom:-1px;border-bottom:2px solid transparent;color:var(--muted);font-size:14px;font-weight:600}
.wtab .n{font-family:var(--mono);font-size:11.5px;font-weight:500;color:var(--faint)}
.wtab:hover{color:var(--fg)}
.wtab[aria-selected=true]{border-bottom-color:var(--red);color:var(--fg)}
.wtab[aria-selected=true] .n{color:var(--fg2)}
/* Neither table scrolls inside its card: every row is on the page and the
   page is the only thing that scrolls, as on Activities (Greg, 2026-10-02).
   No sticky header either — inside `.tscroll` it would stick to a box that
   no longer scrolls. */
.webtools{display:flex;flex-wrap:wrap;gap:10px;align-items:center;padding:12px 14px;border-bottom:1px solid var(--hair-faint)}
.websearch{flex:1 1 220px;min-width:0;display:flex;align-items:center;gap:8px;background:var(--ground);border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:0 10px}
.websearch input{flex:1;min-width:0;background:transparent;border:0;outline:0;color:var(--fg);font-size:13px;padding:8px 0}
.websearch:focus-within{border-color:rgba(255,255,255,.3)}
.webtools .seg{font-size:12.5px;border-radius:10px}
.webtools .seg a{padding:6px 11px;display:flex;gap:6px;align-items:baseline}
.webtools .seg .n{font-family:var(--mono);font-size:11px;color:var(--faint)}
.webtools .seg .cap{align-self:center;padding:0 8px;color:var(--faint);font-size:11px;letter-spacing:.08em;text-transform:uppercase}
.wstat{margin-left:8px;font-size:10.5px;font-weight:400;letter-spacing:.04em;border-radius:5px;padding:1px 6px;white-space:nowrap;border:1px solid var(--hair-strong);color:var(--muted)}
.wstat.growing{color:var(--ok);border-color:rgba(61,220,132,.35)}
.webnone{padding:40px 20px;text-align:center;color:var(--muted);font-size:13px}
.tblfoot .pager{display:flex;gap:6px}
.overtime{padding:22px 24px 16px}
.otnow{text-align:right}
.otnow .v{font-family:var(--mono);font-size:24px;font-weight:500;letter-spacing:-.02em}
.otnow .unit{font-size:12px;color:var(--muted);margin-left:4px}
.otnow .n{font-size:12px;color:var(--muted)}
.otchart{position:relative}
.otchart svg{display:block;width:100%;height:170px;overflow:visible}
.otchart line{stroke:rgba(255,255,255,.05)}
.otchart .area{fill:rgba(255,45,45,.1)}
.otchart .step{fill:none;stroke:var(--red);stroke-width:2;stroke-linejoin:round}
.otchart .gl{position:absolute;left:0;transform:translateY(-120%);font-size:10.5px;color:var(--faint);font-family:var(--mono);pointer-events:none}
.otdot{position:absolute;width:9px;height:9px;margin:-4.5px 0 0 -4.5px;border-radius:50%;background:var(--ground);border:2px solid var(--red)}
.otdot:hover{background:var(--red)}
.otyears{position:relative;height:16px;margin-top:6px}
.otyears span{position:absolute;transform:translateX(-50%);font-size:10.5px;color:var(--faint);font-family:var(--mono)}
.legend2{display:flex;align-items:center;gap:16px;font-size:12px;color:var(--muted);flex-wrap:wrap}
.legend2 span{display:flex;align-items:center;gap:6px}
.legend2 i{width:14px;height:3px;border-radius:2px;background:rgba(255,255,255,.22);display:inline-block}
.legend2 i.red{background:var(--red)}
.evrow.open,.evrow:hover{background:rgba(255,255,255,.03)}
.evrow[data-js]{cursor:pointer}
.evtog{background:none;border:0;padding:0 8px 0 0;color:var(--faint);font:inherit;cursor:pointer;display:inline-block;transition:transform .15s}
.evtog[aria-expanded="true"]{transform:rotate(90deg)}
.evtog:focus-visible{outline:2px solid var(--red);outline-offset:2px;border-radius:3px}
/* An "i" beside a heading that opens its explanation (the Sync page's
   Duplicate activities, 2026-10-02) — the map key's button and popover,
   for a page. The tip hangs from the whole heading row, never the button,
   so it cannot run off a phone. With no script the button stays hidden and
   the tip is a plain line under the heading (v2.js § info tips). */
.infohead{position:relative;display:flex;flex-wrap:wrap;align-items:center;gap:2px;margin:24px 0 12px}
.infohead h2.h{margin:0}
.infobtn{display:grid;place-items:center;width:32px;height:32px;margin:-6px 0;padding:0;border:0;border-radius:999px;background:none;color:var(--muted);cursor:pointer}
.infobtn:hover{color:var(--fg)}
.infobtn[aria-expanded="true"]{background:rgba(255,255,255,.08);color:var(--fg)}
.infobtn:focus-visible{outline:2px solid var(--red);outline-offset:2px}
.infotip{flex-basis:100%;margin-top:8px;max-width:640px;font-size:13px;line-height:1.6;color:var(--fg2);text-wrap:pretty}
.infotip[data-js]{position:absolute;top:calc(100% + 8px);left:0;z-index:20;width:100%;max-width:440px;margin:0;padding:12px 14px;border-radius:14px;background:var(--raised);border:1px solid var(--hair-strong);box-shadow:0 20px 40px -20px rgba(0,0,0,.8);animation:rise .2s both}
.infotip[data-js][hidden]{display:none}
.evdetail td{background:var(--sunken);padding:14px 18px 18px}
.evbody{display:flex;flex-wrap:wrap;gap:14px 28px;align-items:flex-end;justify-content:space-between}
.evtext{flex:1 1 320px;min-width:0}
.evtext p{font-size:12.5px;color:var(--fg2);text-wrap:pretty;white-space:normal}
.evtext p b{color:var(--fg)}
.evtext p b.red{color:var(--red-link)}
.evbar{display:flex;height:8px;border-radius:4px;overflow:hidden;gap:2px;margin-top:10px;background:rgba(255,255,255,.04)}
.evbar span{display:block}
.evbar .a{background:rgba(255,255,255,.34)}
.evbar .b{background:rgba(255,255,255,.18)}
.evbar .new{background:var(--red)}
.evkey{display:flex;gap:14px;margin-top:6px;font-size:11px;color:var(--faint)}
.evkey span{display:flex;align-items:center;gap:5px}
.evkey i{width:8px;height:8px;border-radius:2px;background:rgba(255,255,255,.3)}
.evkey i.new{background:var(--red)}
.evacts{display:flex;gap:8px;flex-wrap:wrap}

/* ---- Settings, one page (design handoff 2026-09-28 § 4) ----
 * A sticky section column beside the cards on a desktop; a scrolling row
 * under the top bar on a phone. Every section card is an anchor whose
 * scroll-margin clears the top bar (and the row, on a phone). */
.page.settingspage{max-width:1040px}
.setwrap{display:grid;grid-template-columns:180px minmax(0,1fr);gap:28px;align-items:start}
.setnav{position:sticky;top:72px;z-index:5;display:flex;flex-direction:column;gap:2px}
.setnav a{flex:0 0 auto;display:block;padding:7px 10px;border-radius:8px;font-size:13px;white-space:nowrap;color:var(--muted)}
.setnav a:hover{color:var(--fg);background:rgba(255,255,255,.05)}
.setnav a.on{color:var(--fg);background:rgba(255,255,255,.07)}
.setmain{min-width:0}
.setsec{margin-bottom:14px;scroll-margin-top:72px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}

/* ---- Settings' profile card (design 2026-10-28) ----
 * One card: who you are, the home base, three figures. The name and the home
 * base are `.pf` forms that edit in place: `.open` swaps the view row for the
 * input row (the server renders it open for `?edit=`, v2.js toggles it).
 * The design's own palette, scoped to the card: its accent for the ring, the
 * pin and the focus rings, a deeper red for Save so white passes AA on it. */
.card.prof{--pf-accent:#f0503c;--pf-save:#d9362a;--pf-line:#222226;--pf-field:#2a2a2f;--pf-text:#f2f2f3;--pf-mute:#9a9aa2;
  background:#131315;border:1px solid var(--pf-line);border-radius:22px;padding:20px;display:flex;flex-direction:column;gap:20px;color:var(--pf-text)}
.prof :is(a,button):focus-visible{outline:2px solid var(--pf-accent);outline-offset:2px}
.pf-id{display:flex;align-items:center;gap:16px}
.pf-face{position:relative;flex:none;width:76px;height:76px}
.pf-face .avatar.lg{width:76px;height:76px;font-size:28px;box-shadow:0 0 0 2px #131315,0 0 0 3px var(--pf-accent)}
.pf-cam{position:absolute;right:-4px;bottom:-4px;width:30px;height:30px;border-radius:50%;border:2px solid #131315;background:var(--pf-text);color:#0a0a0b;display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer}
.pf-cam::before{content:"";position:absolute;inset:-7px;border-radius:50%}
.pf-cam:hover{background:#fff}
.pf-who{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
.pf-name .pf-view{display:flex;align-items:center;gap:2px;min-width:0}
.pf-name .pf-value{font-size:20px;font-weight:600;letter-spacing:-.01em;line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
.pf .pf-value.unset{color:var(--pf-mute);font-weight:500}
.pf-pencil{flex:none;width:40px;height:40px;margin:-4px 0;border-radius:10px;color:var(--pf-mute);display:flex;align-items:center;justify-content:center}
.pf-pencil:hover{color:var(--pf-text);background:rgba(255,255,255,.05)}
.pf-email{font-size:14px;color:var(--pf-mute);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
/* The photo's hidden chooser and its <noscript> fallback: `contents`, so the
 * empty box takes no row (and no gap) in the column when the script runs. */
.pf-photo{display:contents}
.pf-cam[hidden]{display:none}
.pf-face [data-photo-face].js{cursor:pointer}
/* The photo menu (v2.js § profile fields): a modal <dialog>. On a desktop a
 * dropdown under the photo, its top and left set by the script, over a clear
 * backdrop; on a phone a bottom sheet with a Cancel row (the 720px block). */
.pf-menu{position:fixed;inset:auto;margin:0;padding:6px;border:1px solid #2a2a2f;border-radius:14px;background:#18181b;color:#f2f2f3;min-width:220px;box-shadow:0 18px 40px rgba(0,0,0,.55)}
.pf-menu::backdrop{background:transparent}
.pf-menuin{display:flex;flex-direction:column}
.pf-menu [role=menuitem],.pf-menucancel{display:flex;align-items:center;gap:12px;width:100%;min-height:44px;padding:0 12px;border:0;border-radius:10px;background:transparent;color:inherit;font:inherit;font-size:15px;font-weight:500;text-align:left;cursor:pointer}
.pf-menu [role=menuitem] svg{flex:none;color:#9a9aa2}
.pf-menu [role=menuitem]:hover,.pf-menu [role=menuitem]:focus-visible{background:rgba(255,255,255,.07);outline:none}
.pf-menu [role=menuitem].danger{color:#ff6b5c}
.pf-menu [role=menuitem].danger svg{color:currentColor}
.pf-menucancel{display:none}
.pf-home{display:flex;flex-direction:column;gap:8px;border-top:1px solid var(--pf-line);padding-top:16px}
.pf-cap{font-size:12px;font-weight:600;letter-spacing:.12em;text-transform:uppercase;color:var(--pf-mute)}
.pf-homef .pf-view{display:flex;align-items:center;gap:2px;min-width:0}
.pf-place{display:flex;align-items:center;gap:10px;min-width:0}
.pf-pin{flex:none;color:var(--pf-accent)}
.pf-homef .pf-value{font-size:16px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.pf-help{font-size:13px;line-height:1.5;color:var(--pf-mute);max-width:620px}
.pf-help a,.pf-help b{color:var(--pf-text);font-weight:500}
.pf-help a:hover{color:var(--pf-accent)}
.pf-help.warn{color:var(--warn)}
.pf-alt .pf-help{margin-top:4px}
.pf-edit{display:none;align-items:center;gap:6px}
.pf.open .pf-view{display:none}
.pf.open .pf-edit{display:flex}
.pf-edit .combo{flex:1;min-width:0}
.pf-homef .pf-edit{max-width:560px}
.pf-name .pf-edit{max-width:440px}
.pf-input{flex:1;min-width:0;width:100%;height:44px;background:#0f0f11;border:1px solid var(--pf-field);border-radius:12px;padding:0 12px;font:inherit;font-size:16px;color:var(--pf-text)}
.pf-name .pf-input{height:40px;border-radius:10px;padding:0 10px;font-weight:600}
.pf-input::placeholder{color:#85858d}
.pf-input:focus{outline:none;border-color:var(--pf-accent);box-shadow:0 0 0 3px rgba(240,80,60,.18)}
.pf-save,.pf-cancel{flex:none;width:44px;height:44px;border-radius:12px;display:flex;align-items:center;justify-content:center;padding:0;cursor:pointer}
.pf-name .pf-save,.pf-name .pf-cancel{width:40px;height:40px;border-radius:10px}
.pf-save{border:0;background:var(--pf-save);color:#fff}
.pf-save:hover:not(:disabled){background:#e5402f}
.pf-save:disabled{opacity:.4;cursor:default}
.pf-cancel{border:1px solid var(--pf-field);color:#d4d4d8}
.pf-cancel:hover{color:#fff;border-color:#3f3f46}
.pf-spin{display:none;width:16px;height:16px;border:2px solid rgba(255,255,255,.35);border-top-color:#fff;border-radius:50%;animation:spin .7s linear infinite}
.pf.busy .pf-save svg{display:none}
.pf.busy .pf-spin{display:block}
.pf.busy .pf-save:disabled{opacity:1}
.pf-err{display:none;margin-top:8px;font-size:13px;line-height:1.45;color:#ff8a7a}
.pf-err.on{display:block}
.pf-stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));margin:0;border-top:1px solid var(--pf-line);padding-top:16px}
.pf-stats>div{display:flex;flex-direction:column-reverse;justify-content:flex-end;gap:4px;min-width:0}
.pf-stats>div+div{border-left:1px solid var(--pf-line);padding-left:14px}
.pf-stats dt{font-size:12px;line-height:1.35;color:var(--pf-mute)}
.pf-stats dd{margin:0;font-family:var(--mono);font-size:17px;font-weight:500;line-height:1.3;white-space:nowrap}
.pf-stats .dec{color:var(--pf-mute)}
.appscard{display:flex;flex-wrap:wrap;gap:16px 18px;align-items:center}
.appscard img{display:block;width:52px;height:52px;border-radius:12px;flex:0 0 auto;box-shadow:0 0 0 1px rgba(255,255,255,.08)}
.appscard .tx{flex:1 1 260px;min-width:0}
.appscard .hc{margin-bottom:4px}
.appscard .small{line-height:1.6;text-wrap:pretty}
#motorized{scroll-margin-top:72px}
.setfoot{display:flex;flex-wrap:wrap;gap:8px 16px;align-items:center;justify-content:space-between;padding-top:14px;border-top:1px solid var(--hair-faint)}
.compare{margin-top:6px;border-top:1px solid var(--hair-faint);padding-top:12px}
.compare summary{cursor:pointer;font-size:13px;color:var(--fg2);list-style:none;display:flex;align-items:center;gap:6px}
.compare summary::-webkit-details-marker{display:none}
.compare summary span{color:var(--faint);transition:transform .15s}
.compare[open] summary span{transform:rotate(180deg)}
.compare summary:hover{color:var(--fg)}
.delsched{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between;background:rgba(255,45,45,.06);border:1px solid rgba(255,45,45,.3);border-radius:10px;padding:12px 14px;font-size:13px}
.card.signout{display:flex;flex-wrap:wrap;gap:10px 16px;align-items:center;justify-content:space-between;padding:16px 22px;margin-top:28px}
.card.signout .t{font-size:14px;font-weight:500}
.card.signout .d{font-size:12.5px;color:var(--muted)}
a.ul{color:var(--fg);text-decoration:underline;text-underline-offset:3px}
.dlg.danger{max-width:460px;border-color:rgba(255,45,45,.35)}
.dlg.danger .dlgin{padding:24px}
/* Review's suspect multi-select (2026-09-30): the bar over the table, and a
 * tick box beside each name. */
/* Sticky under the 56px top line, so a long list is answered without scrolling
 * back up — which is why its card clips (`overflow:clip`) rather than hides:
 * `hidden` makes the card a scroll container and pins the bar to nothing. */
.suspick{display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:12px 16px;border-bottom:1px solid var(--hair-faint);position:sticky;top:56px;z-index:3;background:var(--surface)}
.suspick .link:disabled{opacity:.45;cursor:default;text-decoration:none}
.suspick-all{display:inline-flex;gap:6px;align-items:center;cursor:pointer}
.suspick-all[hidden]{display:none}
.suspick-n{flex:1 1 180px}
.suspick-row{display:flex;gap:10px;align-items:flex-start}
.suspick input[type=checkbox],.suspick-row input[type=checkbox]{width:16px;height:16px;margin:2px 0 0;flex:0 0 auto;accent-color:var(--red);cursor:pointer}
/* A plain yes/no confirmation (Review's "Stitch all", 2026-09-30): the delete
 * dialog's width and padding, without its red. */
.dlg.confirm{max-width:460px}
.dlg.confirm .dlgin{padding:24px}
.dellist{margin:8px 0 0;padding-left:18px;font-size:13.5px;line-height:1.7;color:var(--fg)}
.dlg .lab{display:block}
.dlg .input{margin-top:6px;letter-spacing:.08em}
.dlgacts{display:flex;gap:8px;justify-content:flex-end;flex-wrap:wrap;margin-top:18px}
.btn.danger.solid{background:var(--red);border-color:var(--red);color:#fff;font-weight:600}
.btn.danger.solid:hover{background:var(--red-hover)}
.btn.danger.solid[disabled]{background:rgba(255,45,45,.12);border-color:rgba(255,45,45,.4);color:var(--red-link)}

/* ---- dialogs (v2.js § dialogs) ----
 * Native <dialog>, shown modal: the browser makes the page behind it inert.
 * The padding is on `.dlgin`, never the dialog, so a click that lands on the
 * dialog element itself is a click on the backdrop. */
.dlg{padding:0;border:1px solid rgba(255,255,255,.1);border-radius:var(--r-card);background:var(--surface);color:var(--fg);width:calc(100% - 32px);max-width:720px;max-height:calc(100% - 32px);box-shadow:0 30px 80px -20px rgba(0,0,0,.8)}
.dlg::backdrop{background:rgba(5,5,6,.72)}
.dlg[open]{display:flex;flex-direction:column}
.dlgin{padding:22px 24px;display:flex;flex-direction:column;min-height:0;flex:1 1 auto;overflow-y:auto;scrollbar-width:thin}
.dlghead{display:flex;justify-content:space-between;align-items:flex-start;gap:16px}
.dlghead h2{font-size:16px;font-weight:600}
.dlghead p{margin-top:4px;font-size:12.5px;color:var(--muted);text-wrap:pretty}
.dlgx{flex:0 0 auto;background:transparent;border:1px solid rgba(255,255,255,.12);color:var(--fg2);border-radius:8px;width:30px;height:30px;cursor:pointer;display:grid;place-items:center}
.dlgx:hover{border-color:rgba(255,255,255,.35);color:var(--fg)}
.red{color:var(--red-link)}
/* The page behind a dialog holds still. The shell scrolls `.main`, not the
 * body, so that is what is held. */
.dlg-open .main{overflow:hidden}
/* The yearly breakdown (§ 2b): one bar per year, value above it. */
.dlg.yearly{height:460px}
.ybars{flex:1;display:flex;align-items:flex-end;gap:6px;margin-top:22px;min-height:120px}
.ycol{flex:1 1 0;min-width:0;height:100%;display:flex;flex-direction:column;justify-content:flex-end;align-items:center;gap:5px}
.ycol .yv{font-family:var(--mono);font-size:9.5px;color:var(--muted);white-space:nowrap}
.ycol i{display:block;width:100%;min-height:1px;background:rgba(255,255,255,.16);border-radius:4px 4px 1px 1px}
.ycol.best .yv,.ycol.now .yv{color:var(--red-link)}
.ycol.best i{background:var(--red)}
.ycol.now i{background:rgba(255,45,45,.14);border:1px dashed var(--red-link)}
.yaxis{display:flex;gap:6px;margin-top:6px;padding-top:6px;border-top:1px solid var(--hair)}
.yaxis span{flex:1 1 0;min-width:0;text-align:center;font-family:var(--mono);font-size:10px;color:var(--faint);overflow:hidden}
.ysum,.shsum{display:grid;gap:14px;margin-top:16px;padding-top:14px;border-top:1px solid var(--hair-faint)}
.ysum{grid-template-columns:repeat(3,minmax(0,1fr))}
.ysum .l,.shsum .l{font-size:12px;color:var(--fg2)}
.ysum .v,.shsum .v{font-family:var(--mono);font-size:17px;margin-top:3px}
.ysum .unit,.shsum .unit{font-size:11px;color:var(--muted);margin-left:3px}
.ysum .v.red{color:var(--red-link)}
/* Every day, all time (§ 2c): a row per year, newest first. Each tick is
 * one active day at x = day-of-year + 0.5 in a 366-wide viewBox. */
.dlg.streakhist{max-width:860px}
.shgrid{display:grid;grid-template-columns:34px minmax(0,1fr) 36px;gap:4px 10px;margin-top:16px;align-items:center}
.shgrid .yr{font-family:var(--mono);font-size:11px;color:var(--muted)}
.shgrid .n{font-family:var(--mono);font-size:11px;color:var(--fg2);text-align:right}
.shgrid .mo{position:relative;height:12px;overflow:hidden}
.shgrid .mo span{position:absolute;font-size:10px;color:var(--faint);font-family:var(--mono)}
.shgrid .hd{font-size:10px;color:var(--faint);text-align:right;font-family:var(--mono)}
.shgrid svg{display:block;width:100%;height:18px;background:rgba(255,255,255,.025);border-radius:2px}
.shgrid svg path{stroke-width:.8;fill:none}
.shgrid svg path.d{stroke:rgba(255,255,255,.34)}
.shgrid svg path.r{stroke:var(--red)}
.shgrid svg rect{fill:none;stroke:var(--fg);stroke-width:1}
.shsum{grid-template-columns:repeat(4,minmax(0,1fr))}
.shsum .v{font-size:16px}
.shsum .s{font-size:11px;color:var(--faint);margin-top:1px}
.streak .kpis{gap:14px;margin-top:18px;padding-top:16px;border-top:1px solid var(--hair-faint)}
.streak .kpi .l{font-size:12px;color:var(--fg2)}
.streak .kpi .v{font-family:var(--mono);font-size:15px;margin-top:4px}
.streak .kpi .v>span{font-size:12px;color:var(--muted);margin-left:4px;font-family:var(--sans)}
.streak .kpi .n{font-size:11.5px;color:var(--faint);margin-top:2px}
/* The count-up's first frame. stats.html's head script sets `cu-wait` before
 * the body parses and v2.js removes it on the frame it paints t=0, so the
 * first thing on screen is a zero and an undrawn line — never the final
 * number flashing before the roll. The script also removes it after 2.5 s on
 * its own, so a page whose v2.js never ran still shows its real figures. */
.cu-wait [data-count]:not([data-count-odo]){visibility:hidden}
/* The odometer hides its DIGITS, not its slabs: the boxes are the design and
   an empty row of them is what the first frame should show. */
.cu-wait [data-count-odo]>span{visibility:hidden}
.cu-wait [data-count-line]{stroke-dashoffset:1}
.cu-wait .bar-thin.grow>span{width:0!important}
.spark{width:60px;height:18px;display:block}
.spark polyline{fill:none;stroke:var(--fg2);stroke-width:1.5}
/* The photo lightbox: a modal <dialog class="dlg">, in the top layer, the
 * whole screen. `.lbstage` is inset by the same distance at the top and the
 * bottom (and at both sides), so the photo is centred on the SCREEN at any
 * size and is as large as the stage allows. The controls lie over the dark
 * ground. The stage lets taps through, so a tap anywhere but the photo or a
 * button lands on the <dialog> itself, which § dialogs reads as "close". */
.dlg.lightbox{--lbx:clamp(56px,6vw,72px);--lby:clamp(24px,8vh,64px);position:fixed;inset:0;width:100%;height:100%;max-width:none;max-height:none;margin:0;border:0;border-radius:0;background:#09090b;box-shadow:none;overflow:hidden;touch-action:pinch-zoom;overscroll-behavior:contain}
.dlg.lightbox[open]{display:block}
.dlg.lightbox::backdrop{background:#09090b}
.lightbox .lbstage{position:absolute;inset:var(--lby) var(--lbx);display:flex;align-items:center;justify-content:center;pointer-events:none}
.lightbox img{display:block;max-width:100%;max-height:100%;width:auto;height:auto;object-fit:contain;border-radius:6px;pointer-events:auto;user-select:none;-webkit-user-select:none;-webkit-touch-callout:none}
.lightbox .x,.lightbox .nav{position:absolute;z-index:1;display:grid;place-items:center;background:rgba(17,17,20,.8);border:1px solid var(--hair-strong);color:var(--fg);border-radius:999px;width:44px;height:44px;cursor:pointer;font-size:18px;line-height:1}
.lightbox .x{top:calc(env(safe-area-inset-top) + 12px);right:calc(env(safe-area-inset-right) + 12px)}
.lightbox .nav{top:50%;transform:translateY(-50%)}
.lightbox .nav.l{left:calc(env(safe-area-inset-left) + 14px)}.lightbox .nav.r{right:calc(env(safe-area-inset-right) + 14px)}
.lightbox .count{position:absolute;bottom:calc(env(safe-area-inset-bottom) + 20px);left:50%;transform:translateX(-50%);font-family:var(--mono);font-size:12px;color:var(--fg2);pointer-events:none}
.lightbox.one .nav,.lightbox.one .count{display:none}
.signin-grid{display:grid;grid-template-columns:1fr 1fr;min-height:calc(100vh - 64px)}
.signin-art{position:relative;overflow:hidden;background:var(--map);border-right:1px solid var(--hair-faint)}
.signin-art .quote{position:absolute;left:48px;bottom:48px;right:48px;background:linear-gradient(transparent,rgba(9,9,11,.9));padding-top:80px}
.signin-art .quote p{font-size:26px;line-height:1.3;letter-spacing:-.02em;font-weight:500;margin:0 0 12px;max-width:440px;text-wrap:balance}
.signin-form{display:grid;place-items:center;padding:48px}
/* ⚠️ The entrance form FADES; it does not rise (2026-09-23). `rise` slides the
 * fields 10px while a password manager is measuring them (1Password, once its
 * button is on a field, re-reads that field's box every frame for as long as
 * it keeps moving), and under `both` its end state is a computed
 * `matrix(1,0,0,1,0,0)`, not `none` — measured in Chrome 153 — so `.in` stays
 * the containing block for anything fixed inside it for the life of the page:
 * the trap MapOverlayPhoneCssTests already records for `.ov-col`. `fade` has
 * no transform at all. v2.tests.EntranceDoesNoFocusWorkTests holds it. */
.signin-form .in{width:min(380px,100%);animation:fade .6s var(--ease) both}
.strength{display:flex;gap:4px;margin-top:8px}.strength span{flex:1;height:3px;border-radius:2px;background:rgba(255,255,255,.1)}.strength span.on{background:var(--ok)}
.sheet{position:fixed;inset:auto 0 0 0;background:rgba(9,9,11,.97);border-top:1px solid var(--hair);border-radius:16px 16px 0 0;padding:16px 16px 28px;display:none;z-index:50}
.sheet.open{display:block}
.sheet a{display:flex;align-items:center;gap:12px;padding:14px 8px;border-radius:12px;color:var(--fg);font-size:14px;min-height:44px}
.sheet a:hover{background:rgba(255,255,255,.06)}
/* Without v2.js the More button is a link to #more-sheet, and this opens it. */
.sheet:target{display:block}
.sheet a{min-height:52px}
.sheet a svg{flex:0 0 auto}
.sheet a .tx{flex:1;min-width:0}
.sheet a .tx b{display:block;font-size:15px;font-weight:500}
.sheet a .tx small{display:block;font-size:12px;color:var(--muted)}
.sheet a.on{color:var(--red)}
/* Settings' row wears the rail's avatar, not a gear (2026-10-02). It is 28px
 * against the 20px icons, so a -4px margin keeps every row's text on one
 * column; it rings on Settings as the rail's does. */
.sheet a .avatar{flex:0 0 auto;width:28px;height:28px;margin:-4px;font-size:12px}
.sheet a.on .avatar{box-shadow:0 0 0 2px var(--red)}
.sheet a .badge{background:rgba(255,45,45,.14);color:var(--red-link);font-size:11px;font-weight:600;border-radius:999px;padding:2px 8px;font-family:var(--mono);flex:0 0 auto}
/* A row whose tab fits in the tab bar (v2.js § tab bar overflow). */
.sheet a.fit{display:none}

/* ---- press feedback ----
 *
 * ⚠️ Every affordance above acknowledges the pointer with :hover, and a phone
 * has no hover. Greg, 2026-09-10: *"it doesn't even look like i've tapped, and
 * something will randomly load."* The server half of that is fixed
 * (core.views.review_open_count — the Review badge was running a geometry scan
 * on every page), but a navigation is never instant, and a tap that produces
 * no mark at all until the next page paints reads as a tap that missed.
 *
 * So: an :active state on everything that navigates or submits. It costs no
 * round trip and no script — it is painted by the browser while the request is
 * still in flight, which is exactly the window that was silent.
 *
 * `touch-action:manipulation` goes with it. It is not the old 300 ms
 * double-tap delay (the viewport meta in _base.html already retired that);
 * it stops a quick second tap on the same control being read as a zoom
 * gesture, which on a tab bar is a real thing to do by accident.
 *
 * ⚠️ **`-webkit-tap-highlight-color` is set to transparent ONLY where a real
 * :active rule replaces it.** Clearing it globally would take the one piece of
 * feedback iOS gives for free and hand back nothing.
 */
a,button,summary,.tbl tr.go{touch-action:manipulation}
a:active,button:active{-webkit-tap-highlight-color:transparent}

.rail a.item:active,.rail .logo:active{background:rgba(255,255,255,.14)}
.rail a.item.on:active{background:rgba(255,45,45,.22)}
.btn:active{border-color:rgba(255,255,255,.45);background:rgba(255,255,255,.07)}
.btn.primary:active{background:var(--red-deep);border-color:var(--red-deep)}
/* ⚠️ `.btn.light` and `.btn:active` are both one class plus one simple
 * selector, so they tie on specificity and the :active rule — written lower
 * in this file — wins the `background`. Without this line the pressed state
 * of the one solid-white button on the site (Sign in, Continue) is 7% white
 * on a near-black panel: the button does not look pressed, it looks gone,
 * which is a reason to press it again. Specificity picks a winner per
 * PROPERTY, so `.btn.light` keeps its own colour and only loses this one. */
.btn.light:active{background:#d4d4d8;border-color:#d4d4d8}
.btn:active,.srcrow2:active,.dropzone:active{transform:translateY(1px)}
.chips button:active,.modebar button:active,.modebar a:active{background:rgba(255,255,255,.14)}
.srcrow2:active,.dropzone:active{border-color:rgba(255,255,255,.35);background:rgba(255,255,255,.04)}
.photostrip a:active{transform:scale(.97)}
.back:active{color:var(--fg)}
/* The row is not a link, so iOS paints nothing for it at all. */
.tbl tr.go:active{background:rgba(255,255,255,.09)}

/* A press must not be swallowed by a transition that fades it in over 180 ms —
 * the point is that it lands on the first frame. */
.btn,.chips button,.modebar button,.modebar a,.srcrow2,.dropzone,.photostrip a{
  transition-duration:.18s}
.btn:active,.chips button:active,.modebar button:active,.modebar a:active,
.srcrow2:active,.dropzone:active,.photostrip a:active,.tbl tr.go:active{
  transition-duration:0s}

/* ---- responsive: one breakpoint ---- */
@media (max-width:720px){
  /* The lightbox on a phone: the photo runs edge to edge across the screen,
   * centred between equal bands top and bottom that clear the notch and the
   * home indicator (the larger of the two, both ends, so it stays centred).
   * Arrows and the count sit over the photo's edge; a swipe steps too. */
  .dlg.lightbox{--lbx:0px;--lby:max(env(safe-area-inset-top),env(safe-area-inset-bottom),56px)}
  .lightbox img{border-radius:0}
  .lightbox .nav{width:40px;height:40px;background:rgba(17,17,20,.6)}
  .lightbox .nav.l{left:calc(env(safe-area-inset-left) + 8px)}.lightbox .nav.r{right:calc(env(safe-area-inset-right) + 8px)}
  .lightbox .count{bottom:calc(env(safe-area-inset-bottom) + 18px)}
  /* The rename pencil is a thumb target: 42px, with the 30px footprint kept. */
  .webname .view .btn{padding:12px;margin:-6px}
  /* The folded key is ONE line on a phone: the spinner says "routing" alone. */
  .notice.fold .rt{display:none}
  .shell{grid-template-columns:minmax(0,1fr)}
  .rail{display:none}
  .tabbar{grid-column:1 / -1;display:flex;justify-content:space-around;align-items:stretch;background:rgba(9,9,11,.92);border-top:1px solid var(--hair);padding:6px 4px 18px;backdrop-filter:blur(14px)}
  .tabbar a{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 0;border-radius:10px;color:var(--fg2);position:relative;font-size:10px;font-weight:500;min-height:44px;justify-content:center}
  .tabbar a.on{color:var(--red)}
  /* A tab that does not fit this width; its row in the More sheet shows. */
  .tabbar a.ovf{display:none}
  .tabbar .badge{position:absolute;top:2px;left:calc(50% + 6px);background:var(--red);color:#fff;font-size:9px;font-weight:600;border-radius:999px;padding:1px 5px;font-family:var(--mono)}
  .page{padding:20px 16px 40px}
  .topline{padding:0 16px}.topline .search{display:none}
  .pubhead{padding:0 16px;gap:16px}.pubhead nav{display:none}.pubhead .brand .routemark{width:40px}
  .pubfoot{padding:24px 16px;flex-direction:column;gap:8px;align-items:flex-start}
  .pubwrap{padding:24px 16px 48px}
  .hero{padding:56px 20px 24px;min-height:0}
  .hero .copy{padding-top:0}
  .apppage{padding:48px 16px 64px}
  .onboard{padding:40px 16px 64px}
  .hero .cols{grid-template-columns:1fr}
  .features .in{grid-template-columns:1fr;padding:48px 16px}
  .pricing{padding:56px 20px 64px}.plans,.plans.two{grid-template-columns:1fr}
  /* Choose your plan on a phone: Pro first, the compact step line, the
   * lead left-aligned (the base rules above are the desktop). */
  .planchoice .plans.two .hero-plan{order:-1}
  .planchoice .plan{padding:24px}
  .planchoice .steps{display:none}
  .planchoice .stepsmini{display:flex}
  .planlead{text-align:left;align-items:flex-start;margin-bottom:32px}
  .planlead h1{font-size:28px}
  .pricing h2{font-size:30px}
  .grid2,.grid3{grid-template-columns:1fr}.grid4{grid-template-columns:repeat(2,minmax(0,1fr))}
  /* Sync (2026-10-02 redesign): the list IS the page; `?src=` is a page of
   * its own with a back link, and the list's Overview row and the health
   * table go (the list is the overview). Rows and buttons reach 44 px. */
  .syncgrid{grid-template-columns:minmax(0,1fr)}
  .syncgrid:not(.picked) .srcmain,.syncgrid.picked .srclist,.synchead.picked{display:none}
  .syncback{display:inline-flex;align-self:flex-start;min-height:44px;margin-bottom:0}
  .srcrow2.srcov,.health,.hrow{display:none}
  .srcrow2,.srcrow2.on{padding:12px 14px;min-height:60px;background:var(--surface);border-color:var(--hair)}
  .srcrow2 .caret{display:inline-flex}
  .srcmain .card.pad{padding:18px 16px}
  .pacts,.nside .nacts{width:100%}
  .pacts>*,.nside .nacts>*{flex:1 1 0}
  .pacts .btn,.nside .nacts .btn,.notice .nacts .btn,.dupq .qacts .btn,.dlgdisc .dlgacts .btn{width:100%;min-height:44px}
  .pacts .stravabtn{flex:0 0 auto;width:auto}
  .notice .nacts>*{flex:1 1 auto}
  .pfigs{gap:10px}
  .pfigs .fig{flex-direction:row;justify-content:space-between;gap:4px 12px}
  .pfigs .v{font-size:12.5px}
  .run{grid-template-columns:minmax(0,1fr) auto}
  .run .res,.srow .res{grid-column:1 / -1}
  .srow.sh,.drow.dh{display:none}
  .srow,.srows.all .srow{grid-template-columns:minmax(0,1fr) auto;gap:2px 0}
  .srows.all .srow .src{grid-column:1 / -1;grid-row:2}
  .drow{grid-template-columns:1fr}
  .drow .dv{justify-content:space-between}
  .drow .dv i{display:inline}
  .dupq{grid-template-columns:1fr}
  .dupq .qb{padding-left:0;border-left:0}
  .dupq .qacts>*{flex:1 1 0}
  .firstcards{grid-template-columns:1fr}
  .dlgdisc .dlgacts>*{flex:1 1 0}
  .statnum{font-size:64px}
  .uniquecard{padding:20px 16px 14px}
  .uhead{gap:12px}
  .uyear{text-align:left}
  .streakcard{padding:20px 16px}
  .streak{grid-template-columns:minmax(0,1fr);gap:20px;align-items:start}
  .ybars,.yaxis{gap:3px}
  .webtools{padding:10px 12px}
  .wtab{flex:1}
  .setwrap{grid-template-columns:minmax(0,1fr);gap:0}
  .pf-menu{inset:auto 0 0 0;width:100%;max-width:none;min-width:0;padding:8px 8px calc(8px + env(safe-area-inset-bottom));border-radius:18px 18px 0 0;border-width:1px 0 0}
  .pf-menu::backdrop{background:rgba(5,5,6,.6)}
  .pf-menu [role=menuitem],.pf-menucancel{min-height:52px;font-size:16px}
  .pf-menucancel{display:flex;justify-content:center;margin-top:6px;border-top:1px solid #2a2a2f;border-radius:0 0 10px 10px;color:#9a9aa2}
  .setnav{top:56px;flex-direction:row;overflow-x:auto;scrollbar-width:none;margin:-20px -16px 16px;padding:8px 16px;background:rgba(9,9,11,.94);border-bottom:1px solid var(--hair-faint)}
  .setsec,#motorized{scroll-margin-top:112px}
  .card.signout{padding:14px 16px}
  .webtools .seg{max-width:100%}
  .overtime{padding:18px 16px 12px}
  .evdetail td{padding:12px 12px 14px}
  .shsum{grid-template-columns:repeat(2,minmax(0,1fr))}
  .dlgin{padding:18px 16px}
  .odo>span{font-size:44px;min-width:44px}
  /* ⚠️ BOTH TABLES FIT, AND THE CONTAINER CANNOT SCROLL SIDEWAYS.
   * Three separate causes, all fixed here. (1) `.fitm`'s 900px floor — that
   * alone pushed the name column off a ~286px canvas. (2) INTRINSIC content
   * width: even at min-width 0 these tables measured 404px, so the widths are
   * fixed percentages rather than negotiated. (3) Too many columns — nine in
   * 286px gives every cell 25px, which is worse than scrolling; the rest are
   * `hide-m`. Name cells wrap on ANY character because ride titles carry
   * unbreakable tokens ("Hayward — CAMBA Seeley·2026-08-17"). */
  .tscroll{overflow-x:hidden}
  .tbl.fitm{min-width:0;table-layout:fixed;width:100%}
  .tbl.fitm td,.tbl.fitm th{padding:9px 10px}
  .tbl.fitm td.name,.tbl.fitm th.name{white-space:normal;overflow-wrap:anywhere}
  .webtbl th:nth-child(1),.webtbl td:nth-child(1){width:9%}
  .webtbl th:nth-child(2),.webtbl td:nth-child(2){width:38%}
  .webtbl th:nth-child(3),.webtbl td:nth-child(3){width:26%}
  .webtbl th:nth-child(6),.webtbl td:nth-child(6){width:27%}
  .conntbl th:nth-child(1),.conntbl td:nth-child(1){width:30%}
  .conntbl th:nth-child(2),.conntbl td:nth-child(2){width:44%}
  .conntbl th:nth-child(5),.conntbl td:nth-child(5){width:26%}
  /* 2,914,600 overran its tile at 22px. */
  .tile.stat .v{font-size:19px}
  .detail{grid-template-columns:1fr}
  .tile .v{font-size:22px}
  .hide-m{display:none !important}
  .srcrow{grid-template-columns:minmax(0,1fr)}
  .srcrow .logo{min-height:72px}
  .signin-grid{grid-template-columns:1fr}.signin-art{display:none}
  .signin-form{padding:24px 16px}
  .mapbox{height:320px}
  /* The small maps' cluster: thumb targets, not pointer ones. 38px, down
     from 44 (Greg, 2026-10-19: "a bit too big" beside a 320px map). */
  .mapbox .maplibregl-ctrl.ectl{margin:0 10px 10px 0;gap:8px}
  .mapbox .maplibregl-ctrl-bottom-left{right:58px}
  .mapbox .maplibregl-ctrl.key{margin:0 0 8px 10px}
  .ectl .ec-compass{width:38px;height:38px}
  .ectl .ec-compass svg{width:23px;height:23px}
  .ectl .ec-zoom{width:38px;border-radius:13px}
  .ectl .ec-zoom button{height:38px;font-size:19px}
  .ectl .ec-bmbtn{width:38px;height:38px;border-radius:13px}
  .ectl .ec-bmbtn svg{width:17px;height:17px}
  .ectl .ec-bmpop{width:216px}
  .ectl .ec-bmgrid button{min-height:44px}
  .mapbox.webmap{height:340px}
  .mapbox.spanmap{height:400px}
  /* A gap's route is long and thin like a span, and its directions follow. */
  .mapbox.gapmap{height:380px}
  /* A web's gaps: the list is the page, the map its index. */
  .mapbox.webgapmap{height:240px}
  /* The key on one line beside the map's buttons: three entries at 12px
   * wrapped to two and covered half a 240px map. */
  .mapbox.webgapmap .key{gap:6px 10px;padding:6px 9px;font-size:11px}
  .mapbox.webgapmap .key i{width:14px}
  .gaprow.go{padding:14px 16px}
  .gaprow.go .acts .btn,.gaprow.go .acts .link{min-height:44px;padding:10px 13px}
  .wghead .seg a{padding:11px 14px}
  .wgdis>summary,.wgdis .link{min-height:44px}
  .back.wgback{min-height:44px}
  .mapbox .mapstate{top:12px;font-size:12px}
  .gapacts{width:100%}
  .cue.end .d{display:none}
  .mapbox.cropmap{height:340px}
  .cropends{gap:8px}
  .cropend .num.md{font-size:18px}
  .tbl th,.tbl td{padding:10px}
  /* z-index: the web card lives in .ov, and without it the compass, locate
   * button and status chip — later siblings — paint OVER the card. `.ov > *`
   * keeps pointer events on the children only. It also makes .ov a stacking
   * context, which is why the layers sheet is moved OUT of it on a phone. */
  .ov{bottom:16px;z-index:2}
  /* ⚠️ No transform animation and no backdrop-filter on the sheet's
   * ancestors. Either one makes that ancestor the containing block for a
   * `position:fixed` descendant, so #mapctl — which lives INSIDE the pill —
   * pinned itself to the pill instead of the screen and drew half-open over
   * the map; the animation also trapped its z-index under the compass.
   * Measured in a browser 2026-09-13, not visible in the handoff's mock. */
  .ov-col{flex:1 1 auto;animation:none}
  /* One bar, full width: the three modes, then the Layers trigger. */
  .modebar{width:100%;flex-wrap:nowrap;padding:4px;gap:2px;backdrop-filter:none;background:rgba(17,17,20,.94)}
  .modebar [data-mode]{flex:1;min-width:0;padding:10px 0;font-size:13px}
  .modebar .sep{display:none}
  .modebar .layerbtn{display:flex;align-items:center;gap:6px;flex:0 0 auto;height:44px;padding:0 12px;border-radius:999px;border:1px solid var(--hair-strong);background:transparent;color:var(--fg);font-size:12.5px;font-weight:600;cursor:pointer}
  .modebar .layerbtn[aria-expanded="true"]{background:rgba(255,45,45,.16);border-color:rgba(255,45,45,.55);color:var(--red-link)}
  /* The sheet — thumb-reachable, closed by Done, the scrim, Escape or a
     swipe down (map.js § layers sheet).
   * ⚠️⚠️ It is NOT drawn inside .ov on a phone (2026-10-05). `.ov{z-index:2}`
   * above makes .ov a stacking context, so a sheet inside it competed with
   * nothing outside it at its own z-index: the tab bar, and the map's credit,
   * drew over its bottom rows (Years and Type), and since the rows fitted
   * inside max-height there was nothing to scroll to. map.js moves #mapctl
   * to the end of .mapfull on a phone and back into the pill on a desktop,
   * and #sheetscrim is .mapfull's child in the template. Both are ABSOLUTE
   * there, not fixed: the sheet ends at the top of the tab bar, as the
   * design draws it, wherever Safari's own toolbar happens to be. */
  .mapfull{overflow:hidden}
  .mapctl{position:absolute;inset:auto 0 0 0;display:block;white-space:normal;background:rgba(9,9,11,.97);backdrop-filter:blur(14px);border-top:1px solid var(--hair);border-radius:16px 16px 0 0;box-shadow:0 -20px 50px -16px rgba(0,0,0,.9);padding:0 16px 22px;z-index:60;max-height:76vh;max-height:76dvh;overflow:auto;overscroll-behavior:contain;transform:translateY(105%);visibility:hidden;transition:transform .3s var(--ease),visibility 0s .3s}
  /* visibility, not just the transform: a closed sheet parked off-screen is
   * otherwise still in the tab order. */
  .mapctl.open{transform:none;visibility:visible;transition:transform .3s var(--ease)}
  /* Mid-swipe the sheet follows the finger, so nothing may ease behind it. */
  .mapctl.open.dragging{transition:none}
  /* The grab strip is the drag's thumb target: 26px tall and the sheet's full
   * width, with the 34×4 bar drawn in its middle. It and the title row take
   * the swipe (touch-action:none, or Safari scrolls the page instead); the
   * grid and the selects below keep their taps and their scrolling. */
  .sheetgrab{display:block;position:relative;height:26px;margin:0 -16px;touch-action:none;cursor:grab;-webkit-user-select:none;user-select:none}
  .sheetgrab::before{content:"";position:absolute;left:50%;top:11px;width:34px;height:4px;margin-left:-17px;border-radius:999px;background:rgba(255,255,255,.18);transition:background .15s}
  .mapctl.dragging .sheetgrab::before{background:rgba(255,255,255,.4)}
  .mapctl.dragging .sheetgrab{cursor:grabbing}
  .sheethead{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px;font-size:15px;font-weight:600;touch-action:none;-webkit-user-select:none;user-select:none}
  /* Out of the pill, so `.modebar button`'s pill shape no longer reaches
   * Done — this is the whole of its phone face. */
  .mapctl .sheethead .btn{border:1px solid var(--hair-strong);border-radius:8px;padding:6px 12px;min-height:36px;font-size:12px;font-weight:500;color:var(--fg);background:transparent}
  .mapctl button:active{background:rgba(255,255,255,.14)}
  .ctlgroup{display:block;margin-bottom:16px}
  .mapctl .g-base{margin-left:0}
  .ctlgroup:last-child{margin-bottom:0}
  .ctlcap{display:block;font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);font-weight:600;margin-bottom:8px}
  .bmgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}
  .bmgrid button{display:flex;flex-direction:column;gap:5px;min-height:44px;padding:6px;border-radius:10px;border:1px solid var(--hair);background:var(--surface);color:var(--fg2);font:inherit;text-align:left;cursor:pointer}
  .bmgrid .sw{height:26px;border-radius:6px}
  .bmgrid .nm{font-size:10.5px;font-weight:600}
  .bmgrid button[aria-pressed="true"]{border-color:rgba(255,45,45,.6);background:rgba(255,45,45,.12);color:var(--fg)}
  .mapctl .ctl{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:44px;margin-bottom:7px;padding:6px 8px 6px 12px;background:var(--surface);border:1px solid var(--hair);border-radius:10px;font-size:12.5px}
  .mapctl .ctl:last-child{margin-bottom:0}
  /* The select stays for a phone that never ran map.js; once the swatches
   * exist they replace it. Keyed on the grid being filled, so the no-JS
   * fallback is real — and three classes, because `.mapctl .ctl` above sets
   * display on the same element and would win a tie. */
  .bmgrid:empty{display:none}
  .mapctl .bmgrid:not(:empty) + .bmrow{display:none}
  .mapctl .ctl .cap{font-size:12.5px;letter-spacing:0;text-transform:none;font-weight:400;color:var(--fg2)}
  /* 16px or iOS zooms the whole map on focus. */
  .mapctl .ctl select{font-size:16px;font-weight:600;color:var(--fg);text-align:right;
    appearance:auto;-webkit-appearance:auto;field-sizing:fixed;padding:9px 6px;background:transparent;
    border:0;cursor:pointer}
  .mapctl .ctl select option{background:#111114;color:var(--fg)}
  .mapctl .ctl select:hover{background-image:none}
  .sheetscrim{display:block;position:absolute;inset:0;background:rgba(9,9,11,.55);z-index:55;opacity:0;pointer-events:none;transition:opacity .25s var(--ease)}
  .sheetscrim.on{opacity:1;pointer-events:auto}
  .sheetscrim.on.dragging{transition:none}
  .legend{display:none}
  .keywrap{display:none}
  /* On a phone the pill owns the bottom row, so the credit sits a row up. */
  .mapwrap .maplibregl-ctrl-bottom-right{left:12px}
  .mapwrap .maplibregl-ctrl-bottom-right .ec-credit{margin:0 68px 30px 0}
  /* Small, but present: bottom-left above the tab bar, clear of the compass
     cluster (.brctl is right:16px), one line, ellipsis rather than wrap. */
  .status{left:12px;bottom:12px;padding:3px 7px;border-radius:7px;font-size:9px;max-width:calc(100% - 76px)}
  .status .dot{width:5px;height:5px;margin-right:5px}
  .status .wide{display:none}
  .status .narrow{display:inline}
  /* The rebuild notice shows on a phone too (Greg, 2026-09-14) — in the pill's
     phone slot at the pill's phone size, and still instead of it. */
  .rebuild.pos{left:12px;bottom:12px;gap:5px;padding:3px 7px;border-radius:7px;font-size:9px;max-width:calc(100% - 76px)}
  .rebuild.pos .dot{width:5px;height:5px}
  /* A 9px × is not a thumb target; the hit area grows, the pill does not. */
  .rebuild.pos .x{position:relative;font-size:11px;padding:0 0 0 2px}
  .rebuild.pos .x::after{content:"";position:absolute;inset:-14px -12px}
  /* The compass and locate button sit in the lower-right corner, on the
     status pill's row (Greg, 2026-09-15); the pill's max-width above already
     leaves them the 76px they need. The 86px lift was the handoff's. */
  .brctl{right:16px;bottom:12px;align-items:center;gap:8px}
  /* The zoom pair shows on a phone too (Greg, 2026-09-17) — it was hidden
     here, leaving Locate alone as a circle. Thumb targets, the embeds' own
     (38px since 2026-10-19). */
  .compass{width:38px;height:38px}
  .compass svg{width:23px;height:23px}
  .zoomcluster{width:38px;border-radius:13px}
  .zoomcluster button{height:38px;font-size:19px}
  /* A bottom sheet, and it has to fit a 390px screen with the tab bar under
     it: tighter padding, 18px figures, a capped height that scrolls. */
  /* -16px undoes .ov's own 16px inset, so the sheet meets the map's edges. */
  .webcard{position:absolute;left:-16px;right:-16px;bottom:-16px;top:auto;width:auto;flex:none;border-radius:18px 18px 0 0;padding:12px 14px 18px;max-height:calc(100% - 56px);overflow:auto}
  .webcard h2{margin:3px 0 12px;font-size:18px}
  .webcard .stats{gap:8px 12px}
  .webcard .stats span.n{font-size:18px}
  .webcard .stats span.l{font-size:10.5px}
  .webcard p{margin:11px 0;padding-top:11px;font-size:11.5px;line-height:1.5}
  .webcard .acts .btn{padding:10px 8px;font-size:12.5px}
  /* A thumb's target, and the close button gets one too: it sits exactly
   * where a thumb reaching for the card's top edge lands. */
  .webcard .close{width:44px;height:44px;top:0;right:0}
  .actcard .eyebrow{padding-right:36px}
  .actcard .step{width:44px;height:44px}
  .actcard .pick{margin:10px 0}
  .readout{bottom:150px}
  /* Press feedback for the two controls that exist only on a phone. These
   * live INSIDE the one breakpoint deliberately — a second
   * `@media (max-width:720px)` block would split the mobile layout across
   * two places, which is the thing docs/V2-DESIGN.md rule 6 forbids. */
  .tabbar a:active{background:rgba(255,255,255,.12)}
  .tabbar a.on:active{background:rgba(255,45,45,.18)}
  .sheet a:active{background:rgba(255,255,255,.09)}
  /* Was its own `@media (max-width:720px)` one-liner beside the rule it
   * overrides. It is here now so the file has exactly ONE breakpoint —
   * which is what v2.tests.ResponsiveTests reads it as. */
  .addride .panel{position:static;width:auto;max-height:none;margin-top:8px}
  /* The connect editor on a phone (design handoff 2026-10-09): a 56px
   * header, the map, the status panel docked under it in the NORMAL FLOW,
   * and everything else in a sheet behind "Key & lines". The panel is a
   * child of `.cxfull`, which is absolute inside `.main`, so it sits above
   * the tab bar and cannot escape to the page the way the fixed one did —
   * see the note on `.cxfull`. It all lives in here rather than in its own
   * breakpoint for the reason docs/V2-DESIGN.md rule 6 gives and
   * v2.tests.ResponsiveTests enforces: this file has exactly one. */
  .cxfull>.topline,.cxhead .eyebrow,.cxhead .cxsep,.cxlong,.cxback span{display:none}
  .cxhead{flex-direction:row;align-items:center;gap:2px;height:56px;padding:0 14px 0 2px;border-bottom:1px solid var(--hair-faint)}
  .cxback{flex:none;justify-content:center;width:44px;height:44px;border-radius:10px}
  .cxback svg{display:block}
  .cxback:active{background:rgba(255,255,255,.08)}
  .cxtitle{flex:1;display:block}
  .cxtitle h1{font-size:15px;line-height:1.25;letter-spacing:normal;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cxlede{font-size:12px;line-height:1.35;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .cxshort{display:inline}
  .cxkeybtn{display:flex;flex:none;align-items:center;gap:6px;height:36px;padding:0 12px 0 10px;border-radius:999px;border:1px solid var(--hair-strong);background:transparent;color:var(--fg);font-size:12.5px;font-weight:500;white-space:nowrap;cursor:pointer}
  .cxkeybtn[aria-expanded="true"]{background:rgba(255,255,255,.1);border-color:rgba(255,255,255,.3)}
  .cxkeybtn:active{background:rgba(255,255,255,.12)}
  .messages.cxnotes{margin:8px 12px}
  .cxwork{min-height:0;flex-direction:column;gap:0;padding:0}
  /* The map is whatever the header, the panel and the tab bar leave: about
   * 470px at 390×760, about 260px on an iPhone SE — the price of Save staying
   * on screen. The floor is for a phone on its side, where that sum leaves
   * next to nothing: there the editor scrolls rather than draw a 40px map. */
  .mapbox.cxmap{height:auto;min-height:200px;border:0;border-radius:0}
  .cxtools{top:10px;left:10px;right:10px}
  .cxseg{height:38px}
  .cxseg button{padding:0 12px;font-size:12.5px}
  .cxside{width:auto;position:relative;z-index:2;gap:0}
  /* Above the sheet (z 5), so the sheet's shadow falls on the map only. */
  .cxpanel{position:relative;z-index:6;border-radius:0;border:0;border-top:1px solid var(--edge);background:var(--ground);box-shadow:none;padding:0 16px 10px}
  .cxstatus{padding-top:10px;min-height:90px}
  .cxsay strong{font-size:15px}
  .cxsay p{font-size:12.5px}
  .cxacts{margin-top:10px}
  /* The sheet: it rises from the panel's top edge over the map, never over
   * the panel, so Save stays reachable with it open. Absolute inside
   * `.cxside`, not fixed. */
  .cxmore{display:none;position:absolute;left:0;right:0;bottom:100%;z-index:5;max-height:min(50vh,420px);max-height:min(50dvh,420px);padding:8px 12px 12px;background:var(--ground);border-top:1px solid rgba(255,255,255,.1);border-radius:16px 16px 0 0;box-shadow:0 -20px 50px -16px rgba(0,0,0,.9);overscroll-behavior:contain}
  .cxmore.open{display:flex}
  /* Mid-swipe the sheet follows the finger. The grab strip and the title row
   * take the swipe (touch-action:none, or Safari scrolls instead); the cards
   * below keep their taps and their scrolling. */
  .cxgrab{display:block;position:relative;height:14px;margin:-4px 0 -6px;touch-action:none;cursor:grab}
  .cxgrab::before{content:"";position:absolute;left:50%;top:5px;width:34px;height:4px;margin-left:-17px;border-radius:999px;background:rgba(255,255,255,.18)}
  .cxmorehead{display:flex;align-items:center;gap:10px;padding:0 4px;font-size:15px;font-weight:600;touch-action:none;-webkit-user-select:none;user-select:none}
  .cxmorehead .mono{font-size:11px;font-weight:400;color:var(--muted)}
  .cxmorehead .btn{margin-left:auto;min-height:36px;padding:0 12px;border-radius:8px;font-size:12px}
  .cxscrim.on{display:block;position:absolute;inset:0;z-index:4;background:rgba(9,9,11,.55)}
}
