  :root{
    --bg:#E8EAE5;
    --panel:#F7F8F4;
    --card:#FFFFFF;
    --ink:#171A1E;
    --muted:#5C6167;
    --line:#D3D6CC;
    --blue:#1D4ED8;
    --k2:#F0A62E;   /* slightly curvy */
    --k3:#E86A24;   /* curvy */
    --k4:#D42B2B;   /* very curvy */
    --k5:#7C1F44;   /* serpentines */
    --ok:#1E7F4F;
    --r:10px;
    /* Desktop base. Everything else is in rem, so this one value sets the whole scale.
       15px reads fine on a phone held close; on a monitor at arm's length it reads small,
       so the mobile @media below pins the old 15px and desktop gets a step up. */
    font-size:16.5px;
  }
  *{box-sizing:border-box;margin:0;padding:0}
  html,body{height:100%}
  body{
    font-family:'Barlow',system-ui,sans-serif;
    background:var(--bg);color:var(--ink);
    height:100dvh;overflow:hidden;position:relative;
  }
  h1,h2,h3,.disp{font-family:'Barlow Condensed','Barlow',sans-serif;text-transform:uppercase;letter-spacing:.06em}

  /* ---------- Sidebar ---------- */
  aside{
    background:var(--panel);border-right:1px solid var(--line);
    overflow-y:auto;display:flex;flex-direction:column;
  }
  header.brand{
    padding:18px 20px 14px;border-bottom:2px solid var(--ink);
    display:flex;align-items:baseline;gap:10px;
  }
  header.brand h1{font-size:1.6rem;font-weight:700}
  header.brand span{color:var(--muted);font-size:.8rem;letter-spacing:.12em;text-transform:uppercase}

  section{padding:16px 20px;border-bottom:1px solid var(--line)}
  section h2{
    font-size:.85rem;font-weight:600;color:var(--muted);
    display:flex;align-items:center;gap:8px;margin-bottom:12px;
  }
  section h2::before{
    content:attr(data-step);
    display:inline-flex;align-items:center;justify-content:center;
    width:20px;height:20px;border:1.5px solid var(--ink);border-radius:50%;
    color:var(--ink);font-size:.72rem;letter-spacing:0;
  }

  .row{display:flex;gap:8px;align-items:center;margin-bottom:8px}
  .row:last-child{margin-bottom:0}
  /* A control that cannot act right now (the segment count, once you have picked roads
     yourself). Dimmed and disabled in place rather than hidden, so selecting a segment does not
     make the panel jump; #segsNote says why. */
  .row.off{opacity:.5}
  .row.off input{cursor:not-allowed}
  label{font-size:.82rem;color:var(--muted)}
  .val{font-family:'Barlow Condensed';font-size:1.15rem;font-weight:600;min-width:64px;text-align:right}

  /* min-width:0 so the slider is what yields when the row runs out of space. A flex item defaults
     to min-width:auto, which for a range input is its intrinsic width — on a 320 px sheet the row
     (label + slider + value) then overflows instead of the slider simply getting shorter. */
  input[type=range]{flex:1;min-width:0;accent-color:var(--ink)}
  input[type=number]{
    width:74px;padding:7px 8px;border:1px solid var(--line);border-radius:6px;
    font:inherit;background:var(--card);
  }
  select{padding:7px 8px;border:1px solid var(--line);border-radius:6px;font:inherit;background:var(--card)}

  button{
    font-family:'Barlow Condensed';text-transform:uppercase;letter-spacing:.08em;
    font-size:.95rem;font-weight:600;cursor:pointer;border-radius:var(--r);
    padding:10px 14px;border:1.5px solid var(--ink);background:var(--card);color:var(--ink);
    transition:transform .05s ease, background .15s ease;
  }
  button:hover{background:#eef0ea}
  button:active{transform:translateY(1px)}
  button.primary{background:var(--ink);color:#fff}
  button.primary:hover{background:#000}
  button:disabled{opacity:.45;cursor:not-allowed;transform:none}
  button.small{padding:5px 10px;font-size:.8rem;border-width:1px;border-radius:6px}
  .btn-wide{width:100%}

  .hint{font-size:.78rem;color:var(--muted);line-height:1.4;margin-top:6px}

  /* Caption line above the legend. */
  .row-head{display:flex;align-items:center;gap:8px;margin-top:10px}
  .row-head label{margin-right:auto}

  /* Curviness scale (legend) */
  .skala{display:flex;gap:6px;margin-top:10px}
  .row-head + .skala{margin-top:6px}   /* the caption already provides the gap */
  /* The chips are <button>s (they are toggles, and as <div>s no keyboard could reach them), so
     they have the global button rule to undo: no border, no uppercase transform of their own.
     Their grade colour is an inline style, which outranks button:hover — the hover state cannot
     wash a grade out. */
  .skala [data-grade]{
    flex:1;text-align:center;font-family:'Barlow Condensed';font-weight:600;font-size:.78rem;
    color:#fff;padding:4px 0;border:0;border-radius:4px;letter-spacing:.05em;text-transform:none;
    cursor:pointer;user-select:none;touch-action:manipulation;transition:opacity .15s ease, filter .15s ease;
  }
  .skala [data-grade].off{opacity:.35;filter:grayscale(.55)}   /* grade hidden from the map */

  /* Segment list — no own scroll; the card body is the single scroll region. */
  #segList{display:flex;flex-direction:column;gap:6px}
  /* One row per segment in range, but only the picked ones are shown: selection stays a single
     `.sel` toggle shared with the map, and the sheet lists what you have chosen rather than
     repeating the map. */
  .seg{display:none}
  .seg.sel{
    display:flex;align-items:center;gap:10px;padding:7px 8px 7px 10px;background:var(--card);
    border:1px solid var(--line);border-radius:8px;font-size:.85rem;
  }
  .seg .grade{
    font-family:'Barlow Condensed';font-weight:700;color:#fff;border-radius:4px;
    width:32px;text-align:center;padding:2px 0;flex-shrink:0;font-size:.8rem;
  }
  .seg .name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .seg .len{color:var(--muted);font-variant-numeric:tabular-nums;flex-shrink:0}
  /* The row is a record; only this drops the road. It carries the app's warning red, like the
     clear-selection pill, so the one destructive control in the list says so on sight. */
  .seg .drop{
    width:22px;height:22px;flex-shrink:0;padding:0;border:1px solid var(--line);border-radius:6px;
    background:transparent;color:var(--muted);font-size:.95rem;line-height:1;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
  }
  .seg .drop:hover{color:var(--k4);border-color:var(--k4)}

  /* ---------- Route deck: the generated route, floating above the sheet ----------
     The route is not a row inside the plan sheet any more — it rides over the map on a card of
     its own, positioned by ui.js (above the bottom sheet on mobile, beside the panel on desktop).
     Every card sits in the SAME grid cell, so an outgoing and an incoming card overlap while both
     are on screen: that is what makes re-rolling a random route read as one reel scrolling on
     rather than as a list being rebuilt. */
  .deck{
    position:fixed;z-index:880;display:grid;grid-template-columns:1fr;
    pointer-events:none;                     /* only the cards take input — the map stays draggable */
    transition:bottom .24s ease;             /* rides the sheet up and down with the FABs and pill */
  }
  .deck > .prop{grid-area:1/1;pointer-events:auto}
  /* Desktop: the card sits beside the plan panel (ui.js sets `left` to the panel's right edge), so
     it slides along as the panel opens and closes. `right` stays set as well, so on a window too
     narrow to hold the full 380px the card gets shorter instead of running off the screen.
     The `left` transition is DESKTOP-ONLY on purpose: mobile measures the deck's height to give
     the sheet its room, and an animating width would be measured mid-flight — where the mobile
     deck's left/right never move anyway, so there is nothing to animate. */
  /* `right` clears the FAB column (54px wide at right:16, plus a gap), not just the window edge:
     on a wide window `left` + max-width decide the card's size and this never binds, but on a
     narrow one the card stretches to fill the space beside the panel and would otherwise run
     straight under the FABs. */
  @media (min-width:641px){
    .deck{right:86px;max-width:380px;transition:left .24s ease, bottom .24s ease}
  }

  /* The route card itself. */
  .prop{
    background:var(--card);border:1px solid var(--line);border-radius:14px;
    padding:10px 13px;cursor:grab;position:relative;overflow:hidden;
    box-shadow:0 10px 30px rgba(0,0,0,.22);
    /* The card owns the horizontal axis (swipe to clear); the browser keeps the vertical one. */
    touch-action:pan-y;-webkit-user-select:none;user-select:none;
    /* Reel transitions. Driven by CLASS, never by inline keyframes, so a swipe in progress — which
       writes `transform` inline — can hand back to the stylesheet by dropping the inline value. */
    transition:transform .34s cubic-bezier(.22,.85,.30,1), opacity .26s ease;
  }
  .prop::before{content:'';position:absolute;left:0;top:0;bottom:0;width:5px;background:var(--pc,var(--ink))}
  .prop.active{border-color:var(--ink)}
  .prop .top{display:flex;align-items:baseline;gap:10px}
  .prop .etappe{font-family:'Barlow Condensed';font-weight:700;font-size:1.15rem;text-transform:uppercase;letter-spacing:.05em}
  /* Roll pager, sitting with the route's name because it is that name's position in the session.
     Quiet by default — it is a way back, not the thing to press — and the whole group disappears
     when there is only one roll to be at. */
  .prop .roll{display:inline-flex;align-items:center;gap:1px;font-size:.7rem;color:var(--muted)}
  /* `display` on a class outranks the UA's [hidden] rule, so the attribute route.js sets would
     otherwise do nothing and a single roll would show an empty pager. */
  .prop .roll[hidden]{display:none}
  .prop .roll .roll-at{font-variant-numeric:tabular-nums;padding:0 3px;white-space:nowrap}
  .prop .roll button{
    width:22px;height:22px;padding:0;border:1px solid var(--line);border-radius:6px;
    background:var(--card);color:var(--ink);font-size:.95rem;line-height:1;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
  }
  .prop .roll button:disabled{opacity:.35;cursor:default}
  .prop .dist{margin-left:auto;font-family:'Barlow Condensed';font-weight:700;font-size:1.35rem;font-variant-numeric:tabular-nums}
  .prop .dist small{font-size:.75rem;color:var(--muted);font-weight:600}
  /* Fold control: a chevron that turns to point the other way when the card is folded. */
  .prop .fold{
    width:24px;height:24px;padding:0;border:1px solid var(--line);border-radius:50%;
    background:var(--card);color:var(--muted);font-size:.7rem;line-height:1;cursor:pointer;
    display:flex;align-items:center;justify-content:center;transition:transform .18s ease;
  }
  .prop.folded .fold{transform:rotate(180deg)}
  /* Folded, the card keeps its headline row — name, pager, distance — and gives the rest back to
     the map. The route is still on screen; only its paperwork is put away. */
  .prop.folded .meta,.prop.folded .actions,.prop.folded .gmaps-note{display:none}
  .prop .meta{font-size:.78rem;color:var(--muted);margin-top:3px;line-height:1.4}
  .prop .actions{margin-top:7px;display:flex;gap:6px;flex-wrap:wrap}   /* wraps once "Keep" joins GPX + Maps */
  /* Clear button in the card header — the pointer/keyboard twin of the swipe. */
  .prop .clear{
    flex:0 0 auto;align-self:center;width:28px;height:28px;padding:0;border-radius:50%;
    border-width:1px;border-color:var(--line);color:var(--muted);
    font-family:'Barlow',sans-serif;font-size:1.05rem;line-height:1;letter-spacing:0;
  }
  .prop .clear:hover{background:#eef0ea;color:var(--ink);border-color:var(--ink)}

  /* Enter: pre-frame below the fold, then the class comes off and it slides + fades up into place.
     Leave: up and out, which is what a replaced route does while its successor flies in. */
  .prop.enter{transform:translateY(34px) scale(.97);opacity:0}
  .prop.leaving{transform:translateY(-46px) scale(.97);opacity:0;pointer-events:none}
  .prop.swiping{transition:none;cursor:grabbing}   /* mid-gesture the finger owns the transform */
  /* Thrown off the edge (JS sets the direction and the target opacity). The throw holds its speed
     and the fade beats it there — the card is gone by about half the travel, while it is still
     visibly moving. The deck's default curve decelerates, which made the card glide to a stop and
     only then disappear; beside a desktop panel it never leaves the viewport, so that stop was the
     animation's last and longest beat. */
  .prop.flung{pointer-events:none;transition:transform .28s linear, opacity .15s ease-out}

  /* Motion is the whole point here, so reduced-motion keeps the states and drops the travel. */
  @media (prefers-reduced-motion: reduce){
    #panel,.card-head,.card-body{transition:none}     /* the sheet still moves; it just arrives at once */
    .prop,.prop.flung{transition:opacity .2s ease}   /* the fling's own curve must not win here */
    .prop.enter,.prop.leaving{transform:none}
    .prop .fold{transition:none}
    .deck{transition:none}
  }

  /* Top-center overlay stack over the map: the transient status toast, and the selection pill
     under it. Laying them out as a flex column is what keeps the pill's position honest for free —
     `.gone` takes the toast out of the flow, so the pill sits at the top when there is no toast
     and drops below one the moment it appears. No second `top` offset to keep in sync. */
  /* `left` + `max-width` are set by ui.js (topAcross) to centre this on the area the panel leaves
     free; the values here are what applies before the first layout pass. */
  #topStack{
    position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:700;
    display:flex;flex-direction:column;align-items:center;gap:8px;
    max-width:min(92vw,540px);pointer-events:none;   /* the gaps pass taps through to the map */
    transition:left .24s ease;                       /* follows the desktop panel as it slides */
  }
  #topStack > *{pointer-events:auto}

  /* Status = floating toast. */
  #status{
    max-width:100%;padding:8px 14px;font-size:.8rem;color:var(--muted);
    background:var(--card);border:1px solid var(--line);border-radius:20px;
    box-shadow:0 2px 10px rgba(0,0,0,.12);display:flex;align-items:center;gap:8px;
  }
  /* Selection is made on the map, so the control that drops it floats over the map too, rather
     than costing the sheet a row it only needs some of the time. */
  .pill{
    border-radius:20px;padding:6px 14px;border-width:1px;
    box-shadow:0 2px 10px rgba(0,0,0,.18);white-space:nowrap;
  }
  /* Red, with a cross: dropping the selection is the one destructive thing on the map, and it
     sits over the map rather than in a menu, so it says which kind of button it is on sight.
     --k4 is already the app's warning red (the route card's warning line uses it). */
  #btnClearSelVis{display:inline-flex;align-items:center;gap:6px;color:var(--k4)}
  /* A set the app rolled rather than one the user built: the pill is a description of the route
     first and a control second, so it drops the warning red and reads as a label. */
  #btnClearSelVis.auto{color:var(--muted)}
  #btnClearSelVis.auto svg{opacity:.65}
  #btnClearSelVis svg{width:12px;height:12px;flex:0 0 auto}   /* inherits the label's colour */
  #status.busy::before{
    content:'';width:12px;height:12px;border:2px solid var(--muted);border-top-color:transparent;
    border-radius:50%;animation:spin .8s linear infinite;flex-shrink:0;
  }
  #status.err{color:var(--k4)}
  /* Some toasts carry their own way out (see setStatus's `action`). It is the toast's button, not
     a link in its prose, so it is a target rather than a thing to read. */
  #status .status-act{padding:3px 10px;font-size:.74rem;flex-shrink:0;white-space:nowrap}
  @keyframes spin{to{transform:rotate(360deg)}}

  /* ---------- Map ---------- */
  /* The map is a real pane, not a full-screen backdrop: JS insets its left (desktop side
     panel) / bottom (mobile sheet) so it occupies only the free area, and Leaflet recenters
     into it. Leaflet's own controls (zoom, etc.) live inside #map, so they follow along. */
  #map{position:fixed;top:0;right:0;bottom:0;left:0;background:#dfe2da;z-index:0}
  .start-pin{
    width:16px;height:16px;border-radius:50%;background:var(--ink);
    border:3px solid #fff;box-shadow:0 1px 5px rgba(0,0,0,.4);
  }
  /* Hover tooltip = a MapLibre popup; strip its tip and chrome down to a compact label. */
  .segtip{pointer-events:none}
  .segtip .maplibregl-popup-content{
    font-family:'Barlow',sans-serif;font-size:.8rem;padding:4px 9px;border-radius:6px;
    box-shadow:0 1px 6px rgba(0,0,0,.28);white-space:nowrap;
  }
  .segtip .maplibregl-popup-tip{display:none}
  .seg-num{
    width:22px;height:22px;border-radius:50%;background:#171A1E;color:#fff;
    border:2px solid #fff;box-shadow:0 1px 4px rgba(0,0,0,.4);
    font-family:'Barlow Condensed';font-weight:700;font-size:.82rem;
    display:flex;align-items:center;justify-content:center;
  }
  .dir-arrow{
    width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;
    border-bottom:11px solid #171A1E;filter:drop-shadow(0 0 1px rgba(255,255,255,.9));
    transform-origin:50% 60%;
  }

  /* ---------- Floating UI ---------- */
  .gone{display:none!important}

  /* Attribution pill — always visible (tile-license requirement). Bottom-center keeps it clear
     of the top status toast, the left side panel, and the bottom-right FABs. z above the
     coach-mark so the first-run tooltip never hides the license/legal links. */
  #attrib{
    position:fixed;left:50%;bottom:6px;transform:translateX(-50%);z-index:970;
    font-size:.66rem;color:var(--muted);text-align:center;max-width:calc(100vw - 20px);transition:bottom .24s ease;
    background:rgba(247,248,244,.86);padding:2px 8px;border-radius:10px;pointer-events:none;
  }
  #attrib a{color:inherit;pointer-events:auto}   /* pill ignores clicks (pass-through to map); links stay clickable */
  #attrib .beta-badge{font-size:.58rem;padding:0 4px;vertical-align:middle}   /* subtler in the thin footer */
  /* Fixed two-line layout: attributions + Beta on top, legal links below (no wrap coincidence). */
  #attrib .attrib-row{display:block;line-height:1.5;white-space:nowrap}

  /* Docked: with the mobile sheet up, the licence line belongs to the sheet rather than floating
     in the gap between the sheet and the route card, where it cost two rows of map. ui.js moves
     the element into .card-foot under the action button (see dockAttrib) and adds this class; the
     rows run inline on one line, and if that does not fit they wrap centred at the row boundary
     rather than mid-phrase — the same two lines it had before, minus the pill around them. */
  #attrib.docked{
    position:static;transform:none;background:none;padding:0;max-width:100%;
    margin-top:9px;padding-top:8px;border-top:1px solid var(--line);
  }
  /* The rows must be allowed to wrap here (they are `nowrap` as a floating pill, where the two
     lines are the layout). Each link keeps its own words together, so a break lands between links
     rather than through "OpenStreetMap contributors" — and the separator carries the spaces that
     make a break between the two rows possible at all: they are adjacent spans in the markup, so
     without them there is no break opportunity and the line is clipped by the sheet instead. */
  #attrib.docked .attrib-row{display:inline;white-space:normal}
  #attrib.docked .attrib-row a{white-space:nowrap}
  #attrib.docked .attrib-row + .attrib-row::before{content:" · "}

  /* FAB stack, bottom-right (JS lifts it above the route card when shown). */
  .fab-stack{position:fixed;right:16px;bottom:36px;z-index:900;display:flex;flex-direction:column;
    align-items:flex-end;gap:12px;transition:bottom .24s ease}
  .fab{
    width:54px;height:54px;padding:0;border-radius:50%;display:flex;align-items:center;justify-content:center;
    background:var(--card);border:1.5px solid var(--ink);color:var(--ink);box-shadow:0 3px 12px rgba(0,0,0,.22);
  }
  .fab.primary{background:var(--ink);color:#fff}
  .fab svg{width:24px;height:24px;flex:0 0 auto}
  /* Until the first route is built the two FABs say what they are; after that they are icons
     again (ui.js owns .labeled). Right-aligned by the stack, so the pills grow leftwards and the
     circles they turn into stay exactly where the pills ended. */
  .fab-lbl{display:none;font-family:'Barlow Condensed','Barlow',sans-serif;font-weight:700;
    text-transform:uppercase;letter-spacing:.06em;font-size:.82rem;white-space:nowrap}
  .fab-stack.labeled .fab{width:auto;height:50px;border-radius:25px;padding:0 18px 0 14px;gap:9px}
  .fab-stack.labeled .fab-lbl{display:inline}

  /* The wordmark, in the map's top-left control corner (see app/map.js). It is a control, so it
     gets the same margin MapLibre gives its own, and it undoes the global button rule rather than
     inheriting a bordered pill. */
  /* A label, not a control: it says what the app is, and nothing happens when you press it.
     pointer-events stays OFF so a tap near the top-left corner still pans the map. */
  .brand{
    display:flex;align-items:center;gap:7px;margin:10px 0 0 10px;padding:5px 10px;
    background:var(--card);border:1px solid var(--line);border-radius:9px;
    box-shadow:0 2px 8px rgba(0,0,0,.14);user-select:none;
  }
  .brand .bm-name{
    font-family:'Barlow Condensed','Barlow',sans-serif;font-weight:700;font-size:1.05rem;
    text-transform:uppercase;letter-spacing:.07em;line-height:1;color:var(--ink);
  }
  .brand .beta-badge{padding:1px 5px;font-size:.6rem}
  /* Phones: the getting-started strip is centred in the same row and the two do not fit across a
     narrow screen. The strip is temporary and the welcome card behind it already says the name,
     so the strip wins for as long as it is up. */
  @media (max-width:640px){
    body.steps-up .brand{display:none}
  }

  /* The one help control, in the map's top-right corner. Same size and weight as the zoom
     buttons opposite it — it is map furniture, not a call to action. MapLibre's control corner is
     pointer-events:none and hands input back only to its own .maplibregl-ctrl children; ours is
     not one, so without pointer-events:auto the canvas underneath swallows every press and the
     button looks alive but does nothing. */
  .helpbtn{
    pointer-events:auto;
    width:34px;height:34px;margin:10px 10px 0 0;padding:0;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:var(--card);border:1.5px solid var(--ink);color:var(--ink);
    box-shadow:0 2px 8px rgba(0,0,0,.16);
    font-family:'Barlow Condensed','Barlow',sans-serif;font-weight:700;font-size:1.15rem;
    line-height:1;letter-spacing:0;text-transform:none;
  }
  .helpbtn[aria-expanded="true"]{background:var(--ink);color:#fff}

  /* Hideable overlays: fade/rise in via .show. */
  .popover,.pin-bar,#crosshair{opacity:0;pointer-events:none;transition:opacity .18s ease, transform .18s ease}
  .popover.show,.pin-bar.show,#crosshair.show{opacity:1;pointer-events:auto}
  .popover.show{transform:none}

  /* One panel holds controls + route results. Google-Maps style: side panel on desktop
     (slides in from the left), bottom sheet on mobile (slides up). Slide via transform;
     .show toggles it. Base = desktop side panel; the mobile @media below overrides. */
  #panel{
    position:fixed;z-index:850;background:var(--panel);
    border-right:1px solid var(--line);
    box-shadow:0 8px 30px rgba(0,0,0,.20);display:flex;flex-direction:column;overflow:hidden;
    transition:transform .24s ease, height .24s ease;
    top:0;left:0;bottom:0;width:min(405px,100vw);border-radius:0 16px 16px 0;  /* flush to top/left/bottom; only the map-facing edge is rounded */
    transform:translateX(-101%);                              /* hidden off-screen left (incl. shadow) */
  }
  #panel.show{transform:none}
  /* touch-action:none hands the vertical axis to the pointer handler in main.js instead of the
     browser's scroll, and user-select:none is what stops a mouse drag here highlighting the whole
     panel — the handle looked draggable long before anything let a mouse drag it.
     The box is deliberately much taller than the 4px bar it draws: the bar is the affordance, the
     hitbox is the whole width of the sheet and a thumb's worth of height, so grabbing it does not
     require aiming at it. */
  .grabber{display:none;flex:0 0 auto;height:34px;cursor:grab;touch-action:none;
    -webkit-user-select:none;user-select:none}
  .grabber::before{margin-top:15px}
  .grabber:active{cursor:grabbing}
  /* While the finger owns the sheet nothing may animate — the transition would lag the drag. The
     title row is included for a second reason: ui.js takes .peek off at the start of a gesture and
     measures the row's natural height straight away, and with a transition still attached that
     read returns whatever frame the animation is on (measured: 25px of a 43px row) rather than
     the height it is heading for. */
  #panel.dragging,#panel.dragging .card-head,#panel.dragging .card-body{transition:none}
  /* The three things that ride the sheet animate their offset so a detent change carries them
     along. A DRAG writes that offset every frame instead (floatAboveSheet), and a .24s curve on
     top of a per-frame write is a quarter second of lag behind the finger. */
  body.sheet-drag .deck,body.sheet-drag #attrib,body.sheet-drag .fab-stack{transition:none}
  /* The title row is a natural place to grab a sheet, so it drags too (its buttons keep their own
     press; main.js skips those). */
  #panel .card-head{touch-action:none}
  .grabber::before{content:'';display:block;width:40px;height:4px;margin:9px auto 0;border-radius:2px;background:var(--line)}
  .card-head{
    flex:0 0 auto;display:flex;align-items:center;gap:8px;padding:9px 14px;
    border-bottom:1px solid var(--line);
  }
  .card-head h3{font-size:1.05rem;font-weight:700}
  /* Dragging the sheet by its head is a natural thing to try; selecting its title is not. */
  .card-head{-webkit-user-select:none;user-select:none}
  /* Peek: the sheet's floor, and the only collapsed state. Handle and action footer, with the
     title row collapsed to nothing.
     Collapsing is a HEIGHT, not display:none — display cannot animate, and it made the state a
     different kind of thing rather than a shorter version of the same one. Two flexbox details
     fall out of that: min-height:0 lets the body's CONTENT box reach zero, but its padding cannot
     shrink, and 24px of unshrinkable padding kept the children summing past the sheet's height —
     which overflowed and clipped the licence line off the footer while leaving a sliver of the
     first slider showing. Hence padding-block:0 here.
     During a drag the class comes off and ui.js drives the title row's height by hand, so it
     grows in with the sheet instead of appearing all at once. */
  #panel.peek .card-body{padding-block:0}
  #panel.peek .card-head{
    height:0;padding-top:0;padding-bottom:0;border-bottom-width:0;
  }
  .card-head{overflow:hidden;transition:height .24s ease, padding .24s ease}
  .card-head .x{margin-left:auto;padding:4px 9px;border-width:1px;border-radius:6px;line-height:1}
  /* The ⓘ sits with the title it belongs to, not with the close button: it explains this sheet,
     so it reads as part of the heading. `.x` keeps the auto margin and stays on the far right,
     where a dismiss control belongs and where a thumb cannot hit it by mistake. */
  /* min-height:0 is what lets a flex item shrink below its own content; without it the sheet
     would refuse to go compact at all. */
  /* The padding transitions for the same reason the head's does: it is collapsed at peek and full
     when open, so a detent change has to carry it rather than snap it. A drag drives it by hand
     (applyHeadFor), and the .dragging rule above takes the curve off while it does. */
  .card-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px 14px;-webkit-overflow-scrolling:touch;
    transition:padding .24s ease}
  /* Pinned footer holding the primary action, so it stays put while the body scrolls. */
  .card-foot{flex:0 0 auto;padding:10px 14px;border-top:1px solid var(--line);background:var(--panel)}
  .grp{margin-bottom:12px}
  .grp:last-child{margin-bottom:0}

  /* Location popover above the location FAB. */
  .popover{
    position:fixed;right:16px;z-index:950;background:var(--panel);border:1px solid var(--line);
    border-radius:12px;box-shadow:0 8px 30px rgba(0,0,0,.20);padding:8px;transform:translateY(10px);
    display:flex;flex-direction:column;gap:6px;min-width:190px;
  }
  .popover button{text-align:left}

  /* Getting-started strip. Rides in #topStack with the toast and the selection pill, so it is
     centred on the free area and stacks with them for free — no second set of offsets. It is one
     dark bar rather than three chips so it reads as a single sequence at a glance, and it is
     small enough that a 320px screen still fits all three steps. */
  /* Hidden until shown, like every other .show overlay in here — and via `display`, not
     `.gone`: the strip is a flex item in #topStack, so it has to leave the flow entirely or it
     would hold a gap open above the selection pill before it is ever displayed. */
  .steps{display:none}
  .steps.show{
    display:flex;align-items:center;gap:2px;margin:0;padding:4px 4px 4px 6px;list-style:none;
    background:var(--ink);color:#fff;border-radius:20px;box-shadow:0 3px 12px rgba(0,0,0,.26);
    font-size:.74rem;line-height:1;max-width:100%;
  }
  .steps .step{display:flex;align-items:center;gap:5px;padding:4px 7px;border-radius:14px;white-space:nowrap}
  .steps .sn{
    width:15px;height:15px;flex:0 0 auto;border-radius:50%;border:1px solid rgba(255,255,255,.45);
    display:flex;align-items:center;justify-content:center;font-size:.62rem;font-weight:700;
  }
  .steps .st{opacity:.62;font-weight:500}
  /* Done: the number becomes a tick, the label recedes. Now: the whole item lifts out of the bar
     so the one thing to do next is the one thing that is bright. */
  .steps .step.done .sn{background:#fff;color:var(--ink);border-color:#fff;font-size:.7rem}
  .steps .step.done .st{opacity:.42;text-decoration:line-through}
  .steps .step.now{background:rgba(255,255,255,.16)}
  .steps .step.now .sn{background:#fff;color:var(--ink);border-color:#fff}
  .steps .step.now .st{opacity:1;font-weight:600}
  .steps .step-x{display:flex;align-items:center}
  .steps .step-x button{
    width:20px;height:20px;padding:0;border:0;border-radius:50%;background:transparent;
    color:rgba(255,255,255,.6);font-size:.95rem;line-height:1;cursor:pointer;
  }
  .steps .step-x button:hover{color:#fff;background:rgba(255,255,255,.14)}
  /* Very narrow phones: the ticked-off steps give up their words first, the live one keeps them.
     Three full labels plus the numbers overflow a 320px screen; the current step is the only one
     that has to be readable. */
  @media (max-width:359px){
    .steps .step.done .st{display:none}
  }

  /* First-run welcome card: full-screen backdrop + centered card, above everything.
     The help pane rides the same pattern — one dialog look for the whole app. */
  #welcome,#gmapsConfirm,#helpSheet{
    position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;
    padding:20px;background:rgba(23,26,30,.42);opacity:0;pointer-events:none;transition:opacity .2s ease;
  }
  #welcome.show,#gmapsConfirm.show,#helpSheet.show{opacity:1;pointer-events:auto}
  .wcard{
    background:var(--panel);border:1px solid var(--line);border-radius:16px;
    box-shadow:0 14px 44px rgba(0,0,0,.32);width:100%;max-width:400px;padding:24px 24px 20px;
    transform:translateY(10px);transition:transform .2s ease;
  }
  #welcome.show .wcard,#gmapsConfirm.show .wcard,#helpSheet.show .wcard{transform:none}
  /* Layered notice: the detail is one tap away, on the same screen, and looks like the aside it
     is rather than another paragraph of the lead. */
  .wcard .disclose{
    margin:10px 0 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);
  }
  .wcard .disclose summary{
    padding:8px 12px;cursor:pointer;font-size:.84rem;font-weight:600;list-style:none;
    display:flex;align-items:center;gap:7px;
  }
  .wcard .disclose summary::-webkit-details-marker{display:none}
  .wcard .disclose summary::before{content:'▸';font-size:.7rem;color:var(--muted);transition:transform .15s ease}
  .wcard .disclose[open] summary::before{transform:rotate(90deg)}
  .wcard .disclose p{padding:0 12px 10px;margin:0;font-size:.82rem;color:var(--muted);line-height:1.45}
  .wcard .wnote{font-size:.82rem;color:var(--muted);margin-bottom:12px}
  /* Consent is a decision, so its checkbox is a target you can hit, not a 13px square. */
  .wcard .wcheck{
    display:flex;align-items:center;gap:9px;margin:0 0 14px;padding:7px 2px;
    font-size:.84rem;color:var(--muted);cursor:pointer;-webkit-user-select:none;user-select:none;
  }
  .wcard .wcheck input{width:17px;height:17px;flex:0 0 auto;accent-color:var(--ink);cursor:pointer}
  @media (prefers-reduced-motion: reduce){
    .wcard .disclose summary::before{transition:none}
  }
  /* Consent dialog: two-button action row. */
  .wcard .wbtns{display:flex;gap:8px;margin-top:4px}
  .wcard .wbtns button{flex:1}
  .wcard h2{font-family:'Barlow Condensed','Barlow',sans-serif;font-weight:700;font-size:2rem;
    letter-spacing:.05em;text-transform:uppercase;line-height:1;display:flex;align-items:center;gap:10px}
  /* "Beta" badge: quiet maturity signal next to the title + a persistent copy in the footer. */
  .beta-badge{
    font-family:'Barlow Condensed','Barlow',sans-serif;font-weight:700;font-size:.7rem;
    letter-spacing:.12em;text-transform:uppercase;color:var(--blue);
    border:1px solid var(--blue);border-radius:5px;padding:2px 6px;line-height:1;
  }
  .wcard .wtag{color:var(--muted);font-size:.78rem;letter-spacing:.14em;text-transform:uppercase;margin:4px 0 14px}
  /* Early-access + drive-safe note under the welcome steps. */
  .wcard .wnote{color:var(--muted);font-size:.76rem;line-height:1.45;margin:-6px 0 16px}
  .wcard p{font-size:.9rem;line-height:1.5;margin-bottom:16px}
  .wcard ol{list-style:none;display:flex;flex-direction:column;gap:11px;margin-bottom:20px}
  .wcard li{display:flex;gap:11px;align-items:flex-start;font-size:.86rem;line-height:1.4}
  .wcard li .wnum{
    flex:0 0 auto;width:22px;height:22px;border-radius:50%;background:var(--ink);color:#fff;
    font-family:'Barlow Condensed';font-weight:700;font-size:.82rem;
    display:flex;align-items:center;justify-content:center;
  }

  /* Help pane: one card holding every control's explanation, sized to be read in one piece —
     no scrolling for the four entries it carries. 100% is the overlay's content box (its 20px
     padding is already subtracted), so the card can never grow past the screen; the body's
     overflow is a safety valve for a landscape phone or a 200% zoom, not the normal path.
     Head/body split as in the plan panel, so the close button stays put if it ever engages. */
  .help-card{
    display:flex;flex-direction:column;max-height:100%;
    padding:16px 20px 14px;overflow:hidden;
  }
  .help-head{display:flex;align-items:center;gap:8px;flex:0 0 auto}
  .help-head h2{font-size:1.5rem}
  .help-head .x{margin-left:auto;padding:4px 9px;border-width:1px;border-radius:6px;line-height:1}
  .help-body{
    flex:0 1 auto;overflow-y:auto;-webkit-overflow-scrolling:touch;
    margin-top:11px;display:flex;flex-direction:column;gap:11px;
  }
  /* Group label. Two of them split the pane into "what the controls do" and "what the map is
     showing"; nine flat entries is a list to search, two groups of four and six is a place to
     look. Spans the full width so the landscape two-column grid keeps its headings on their own
     rows instead of shunting one into the second column. */
  .help-group{
    grid-column:1/-1;margin:2px 0 -4px;font-family:'Barlow Condensed','Barlow',sans-serif;
    font-weight:700;text-transform:uppercase;letter-spacing:.14em;font-size:.68rem;color:var(--muted);
  }
  .help-group + .help-item{margin-top:0}
  /* Same left rule the inline tips carried, so an explanation still reads as an aside. */
  .help-item{padding-left:10px;border-left:2px solid var(--line)}
  .help-item h3{font-size:.92rem;font-weight:700;margin-bottom:1px}
  .help-item p{font-size:.82rem;color:var(--muted);line-height:1.45;margin:0}
  /* Short screens — an older phone upright, or any phone in landscape. The pane trades air
     before it trades being readable in one piece, so the entries stay whole instead of
     handing the reader a scrollbar. */
  @media (max-height:620px){
    .help-card{padding:11px 15px 11px}
    .help-head h2{font-size:1.2rem}
    .help-body{margin-top:9px;gap:8px}
    .help-item{padding-left:8px}
    .help-item h3{font-size:.86rem}
    .help-item p{font-size:.78rem;line-height:1.4}
  }
  /* Wide enough to spend some: the card grows with the viewport and the entries lay out in as
     many columns as fit. The 400px .wcard cap is right for the welcome card and the consent
     dialog — a paragraph and two buttons — but the guide carries ten entries, and holding it to
     the same column on a monitor made a keyhole of it: a quarter of the window wide, most of it
     below the fold, read by scrolling a strip. Width is the one thing a desktop has going spare.
     auto-fit rather than a written-down column count, because the same rule then covers the case
     it was first added for — a phone on its side, where there is no height left to give but
     plenty of width — and every window width in between, without a breakpoint per column. */
  @media (min-width:560px){
    .help-card{max-width:min(880px,100%)}
    .help-body{
      display:grid;grid-template-columns:repeat(auto-fit,minmax(232px,1fr));
      gap:11px 22px;align-content:start;
      /* Each entry keeps its own height: stretched to its row's tallest, the left rule an entry
         draws would run on past the end of its text. */
      align-items:start;
    }
  }
  /* Short *and* wide is a phone on its side: the columns above are already doing the work, so
     this only takes the air back out between them. */
  @media (max-height:620px) and (min-width:560px){
    .help-body{gap:7px 18px}
  }

  /* Set-Pin confirm bar, bottom-center. */
  .pin-bar{
    position:fixed;left:50%;bottom:52px;transform:translate(-50%,10px);z-index:950;
    display:flex;gap:8px;background:var(--panel);border:1px solid var(--line);
    border-radius:30px;box-shadow:0 6px 24px rgba(0,0,0,.22);padding:8px 10px;
  }
  .pin-bar.show{transform:translate(-50%,0)}

  /* Center crosshair for Set-Pin mode. */
  #crosshair{position:fixed;left:50%;top:50%;z-index:800;transform:translate(-50%,-50%);color:var(--ink)}
  #crosshair.show{transform:translate(-50%,-50%)}
  #crosshair svg{width:44px;height:44px;filter:drop-shadow(0 1px 3px rgba(255,255,255,.9))}

  /* Mobile: the panel becomes a bottom sheet that slides up. */
  @media (max-width:640px){
    /* Hold the pre-#36 scale on phones: the desktop step-up is for reading distance, and on a
       narrow sheet the extra rem would cost layout instead of legibility. */
    :root{font-size:15px}
    #panel{
      top:auto;left:0;right:0;bottom:0;width:auto;
      /* What the sheet may not grow past, and everything it owes the screen above it.
         --deck-h is the route card's room: the card floats directly above the sheet, so the sheet
         yields exactly what it takes, or a tall sheet on a small phone pushes the card off the
         top. --fab-room is the same debt owed to the map's own furniture — the top-right controls
         and the FAB column that has to clear them (both set by positionFabs). Without it an
         expanded sheet lifted the card past the point the clamped stack could rise to, and the
         buttons came to rest on the card.
         82dvh stays as the upper bound for the case where neither is in play, and the 180px floor
         is a last resort: on a viewport too short to honour the debts, a sheet that still shows
         its handle, some body and its action beats one collapsed to nothing. The body scrolls. */
      max-height:min(82dvh, max(180px, calc(100dvh - var(--fab-room,0px) - var(--deck-h,0px))));
      border-radius:18px 18px 0 0;border-bottom:none;
      transform:translateY(100%);                             /* hidden below the fold */
    }
    #panel.show{transform:none}
    .grabber{display:block}
  }
