@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;700&family=Inter:wght@400;500;600;700&display=swap');

  :root{
    --paper: #FBFBF8;
    --paper-dim: #EFEDE6;
    --ink: #14201A;
    --ink-soft: #5C6A5F;
    --line: #DCDAD0;
    --blaze: #FF8A00;
    --blaze-dark: #E67A00;
    --blaze-text: #A65600;
    --moss: #0E8F49;
    --mustard: #D9A441;
    --steel: #0E8F49;
    --white: #FFFFFF;
    --shadow: 0 6px 20px rgba(20,32,26,0.16);
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
  }

  *{ box-sizing: border-box; }
  html, body{ margin:0; padding:0; height:100%; width:100%; overflow:hidden; background: var(--ink); color: var(--ink); }
  /* 100vh on iOS Safari is sized as if the address bar is always
     collapsed, so it doesn't shrink to match the actual visible viewport
     when the bar is expanded (e.g. right after load) — #app ends up
     shorter than the real viewport in that state, and the bare `body`
     background (--ink, dark green) shows through behind the address bar
     instead of the map. 100dvh tracks Safari's true visible area as the
     bar shows/hides; 100vh stays as a fallback for browsers that don't
     support dvh at all. */
  #app{ position:relative; height:100vh; height:100dvh; width:100vw; }

  #map{ position:absolute; inset:0; z-index:1; }
  .leaflet-container{ background: #E9E7DD; font-family: 'Inter', sans-serif; }

  /* ---------- Header ---------- */
  #header{
    position:absolute; top:0; left:0; right:0; z-index:500;
    display:flex; flex-direction:column; gap:10px;
    /* env(safe-area-inset-top) is 0 on devices without a notch/Dynamic
       Island, so this is a no-op there — on an iPhone, without it the
       logo/search/icon row would sit partly under the status bar, and
       viewport-fit=cover (needed so the header's own dark background
       fills that area instead of the bare page background showing
       through) is what makes the inset apply at all. */
    padding:calc(12px + env(safe-area-inset-top)) 16px 14px;
    background: linear-gradient(to bottom, rgba(20,32,26,0.97), rgba(20,32,26,0.0));
    pointer-events:none;
  }
  /* flex-end rather than center: the search bar (37px, padding+border)
     and the round icon buttons (38px fixed) differ by a hair, so
     center-alignment left the icons looking a half-pixel low relative to
     the search bar's bottom edge — bottom-aligning both fixes that
     regardless of any small height difference between them. */
  #headerTop{ display:flex; flex-wrap:wrap; align-items:flex-end; justify-content:space-between; gap:12px 12px; min-width:0; }
  #headerTop > *{ pointer-events:auto; }
  #brand{ display:flex; flex-direction:column; line-height:1; min-width:0; order:1; }
  #brand .mark{
    height:34px; width:auto; display:block;
    filter: drop-shadow(0 2px 6px rgba(0,0,0,0.35));
  }

  #headerIcons{ display:flex; align-items:center; gap:8px; flex-shrink:0; order:3; }
  @media (max-width:420px){ #headerIcons{ gap:6px; } }
  #routeBtn, #filtersBtn, #locateBtn, #accountBtn{
    width:38px; height:38px; border-radius:50%; background:var(--white); border:1px solid var(--line);
    box-shadow: var(--shadow); display:flex; align-items:center; justify-content:center; cursor:pointer;
    flex-shrink:0; position:relative;
  }
  #routeBtn:hover, #filtersBtn:hover, #locateBtn:hover, #accountBtn:hover{ background: var(--paper); }
  /* Open state — the button inverts to dark, matching how an active filter
     chip reads, so it's clear which menu the panel below belongs to. The
     icon also swaps to an X, since color alone is easy to miss at a glance. */
  #routeBtn.menu-open, #filtersBtn.menu-open, #accountBtn.menu-open{
    background: var(--ink); border-color: var(--ink);
  }
  #routeBtn.menu-open svg, #filtersBtn.menu-open svg, #accountBtn.menu-open svg{ stroke: var(--paper); }
  #routeBtn.menu-open:hover, #filtersBtn.menu-open:hover, #accountBtn.menu-open:hover{ background: var(--ink); }
  .menuOpenIcon{ display:none; }
  .menu-open .menuDefaultIcon{ display:none; }
  .menu-open .menuOpenIcon{ display:block; }
  /* Badges sit on the button, so their ring has to follow the background */
  #routeBtn.menu-open #routeBadge, #filtersBtn.menu-open #filtersBadge{ border-color: var(--ink); }
  #filtersBadge{
    display:none; position:absolute; top:-2px; right:-2px; width:9px; height:9px; border-radius:50%;
    background: var(--blaze); border:1.5px solid var(--white);
  }
  #filtersBadge.show{ display:block; }

  /* Mobile (default): full-width row below the logo/icons line — same
     look as before this was folded into #headerTop, achieved by flex-wrap
     plus a 100% flex-basis forcing it onto its own line. */
  #headerSearchRow{ pointer-events:none; order:2; flex-basis:100%; }
  #headerSearchRow > *{ pointer-events:auto; }
  #searchWrapOuter{ position:relative; width:100%; max-width:480px; min-width:0; }
  /* Desktop: sits inline between the logo and the icon menu, centered in
     the space between them rather than pinned to either side. */
  @media (min-width:721px){
    #headerSearchRow{ order:2; flex:1; flex-basis:auto; display:flex; justify-content:center; }
    #searchWrapOuter{ width:100%; }
  }
  #searchWrap{
    display:flex; align-items:center; min-width:0;
    background: var(--white); border-radius:999px; padding:8px 14px;
    box-shadow: var(--shadow); border:1px solid var(--line);
  }
  #searchWrap input{
    border:none; outline:none; background:transparent; flex:1; min-width:0; font-size:14px; color:var(--ink); font-family:inherit;
  }
  #searchWrap svg{ flex-shrink:0; }

  #searchResults{
    position:absolute; top:calc(100% + 6px); left:0; right:0; z-index:510;
    background:var(--white); border:1px solid var(--line); border-radius:12px; box-shadow:var(--shadow);
    max-height:280px; overflow-y:auto; display:none;
  }
  .searchResultItem{
    display:flex; align-items:center; gap:10px; padding:10px 12px; cursor:pointer; border-bottom:1px solid var(--line);
  }
  .searchResultItem:last-child{ border-bottom:none; }
  .categoryResultItem{
    background:#FFF1DC; border-left:3px solid var(--blaze); border-bottom:1px solid var(--line);
  }
  .categoryResultItem:hover{ background:#FFE7C2; }
  .categoryResultItem .name{ color:var(--blaze-text); font-weight:700; }
  .categoryResultItem .cat{ color:var(--blaze-text); }
  .searchResultItem:hover{ background: var(--paper); }
  .searchResultItem .icon{
    width:26px; height:26px; border-radius:50%; display:flex; align-items:center; justify-content:center;
    font-size:13px; flex-shrink:0; color:#fff;
  }
  .searchResultItem .info{ min-width:0; flex:1; }
  .searchResultItem .name{ font-size:13px; font-weight:600; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .searchResultItem .cat{ font-size:11px; color:var(--ink-soft); }
  .searchResultsMore{ padding:8px 12px; font-size:11.5px; color:var(--ink-soft); text-align:center; }

  #accountMenu{
    position:absolute; top:104px; right:16px; z-index:520; background:var(--white); border:1px solid var(--line);
    border-radius:10px; box-shadow: var(--shadow); min-width:220px; padding:8px; display:none;
  }
  #accountMenu .who{ font-size:12px; color:var(--ink-soft); padding:8px 10px 4px; }
  #accountMenu button{
    width:100%; text-align:left; background:none; border:none; padding:9px 10px; font-size:13.5px;
    font-family:inherit; color:var(--ink); cursor:pointer; border-radius:6px; display:flex; align-items:center; gap:8px;
  }
  #accountMenu button:hover{ background: var(--paper); }

  /* Set apart from the action list above it (divider + small label) and
     rendered as a real segmented control — not more stacked list buttons —
     so it reads as "pick one of these two states" rather than two more
     independent menu actions like "My favorites". */
  #accountMenu .menuLanguageSection{
    margin-top:6px; padding-top:10px; border-top:1px solid var(--line);
  }
  #accountMenu .menuLanguageLabel{
    font-size:11px; font-weight:600; letter-spacing:0.02em; text-transform:uppercase;
    color:var(--ink-soft); padding:0 10px 6px; display:flex; align-items:center; gap:6px;
  }
  #accountMenu .menuLanguageRow{
    display:flex; gap:2px; background:var(--paper-dim); border-radius:8px; padding:2px; margin:0 10px 4px;
    position:relative;
  }
  #accountMenu .menuLanguageBtn{
    flex:1; width:auto; justify-content:center; padding:7px 8px; font-size:12.5px; font-weight:600;
    border-radius:6px; color:var(--ink-soft); transition:color 0.15s ease, background 0.15s ease;
  }
  #accountMenu .menuLanguageBtn:hover{ background:none; color:var(--ink); }
  #accountMenu .menuLanguageBtn.active{ background:var(--white); color:var(--ink); box-shadow:0 1px 3px rgba(20,32,26,0.12); }
  #accountMenu .menuLanguageBtn.active:hover{ background:var(--white); }
  #accountMenu .menuLanguageBtn:disabled{ opacity:0.6; cursor:default; }
  #accountMenu .menuLanguageBtn .spinner-dot{
    display:inline-block; width:11px; height:11px; margin-left:6px; border-radius:50%;
    border:2px solid currentColor; border-right-color:transparent; animation:menuLangSpin 0.6s linear infinite;
    vertical-align:middle;
  }
  @keyframes menuLangSpin{ to{ transform:rotate(360deg); } }

  /* ---------- Category filters (collapsible — scales to any number of categories) ---------- */
  #filtersMenu{
    position:absolute; top:104px; right:16px; z-index:520; background:var(--white); border:1px solid var(--line);
    border-radius:12px; box-shadow: var(--shadow); width:min(300px, calc(100vw - 32px)); padding:12px; display:none;
  }
  #routeMenu{
    position:absolute; top:104px; right:16px; z-index:520; background:var(--white); border:1px solid var(--line);
    border-radius:12px; box-shadow: var(--shadow); width:min(320px, calc(100vw - 32px));
    padding:12px 12px 4px; display:none;
    /* Bottom padding is intentionally smaller than the other three sides:
       whichever button ends up last (varies by state — cancelPlanningBtn
       or the corridor-width select) commonly carries its own
       margin-bottom:8px via .routeAction. Without this, that 8px stacks on
       top of full padding, leaving ~20px below the last button instead of
       the intended ~12px that matches the menu's other edges. */

    /* Grows downward from a fixed top with no natural stop, so a tall
       state (active route + save form open) used to run off the bottom of
       a short screen and — on mobile, where #routeSidebar is a bottom
       sheet up to 55vh — slide underneath it, leaving the Save button
       unclickable with nothing on screen to say why. Capping the height
       and scrolling the overflow keeps every state reachable no matter
       how many controls it holds. 16px matches the menu's own right
       inset, so the gap under it reads as the same margin. */
    max-height:calc(100vh - 104px - 16px);
    overflow-y:auto; overscroll-behavior:contain;
  }
  @media (max-width:720px){
    /* Leave room for the stops sheet as well, so the two never overlap. */
    #routeMenu{ max-height:calc(45vh - 16px); }
  }
  #routeMenu .filtersHead{ display:flex; align-items:center; justify-content:space-between; margin-bottom:6px; }
  /* Collapsed, the title bar is the whole menu — drop the bottom margin
     that normally separates it from the body, so the panel shrinks to just
     the bar rather than leaving a stripe of empty white beneath it. */
  #routeMenu.collapsed .filtersHead{ margin-bottom:0; }
  #routeMenu.collapsed{ padding-bottom:12px; }
  #routeMenuCollapse{
    background:none; border:none; cursor:pointer; color:var(--ink-soft);
    padding:2px; margin:-2px; flex-shrink:0; border-radius:6px; line-height:0;
  }
  #routeMenuCollapse:hover{ background:var(--paper); color:var(--ink); }
  #routeMenuCollapse:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  /* Chevron flips to point down once collapsed — down being "bring the
     body back", the direction it will reappear from. */
  #routeMenu.collapsed #routeMenuCollapse svg{ transform:rotate(180deg); }
  #routeMenu select{
    width:100%; border:1px solid var(--line); background:var(--white); border-radius:8px; padding:9px 10px;
    font-size:13px; font-family:inherit; color:var(--ink); outline:none; margin-bottom:10px;
  }
  #routeMenu .reportLabel{
    display:block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em;
    color:var(--ink-soft); margin-bottom:6px;
  }
  /* Matches the select above rather than inheriting the form's .field input
     styling — these sit in the route menu, not in a panel form, and should
     read as part of the same stack of controls as the corridor-width and
     profile selects. */
  #routeMenu input[type=datetime-local], #routeMenu input[type=number]{
    flex:1; min-width:0; border:1px solid var(--line); background:var(--white); border-radius:8px;
    padding:9px 10px; font-size:13px; font-family:inherit; color:var(--ink); outline:none;
  }
  #routeMenu input[type=datetime-local]:focus, #routeMenu input[type=number]:focus{ border-color:var(--blaze); }
  /* Input grows, the reset button sits at its natural width beside it. The
     row carries the margin the inputs deliberately don't, so the hint under
     it can sit tight against the control it explains. */
  .routeStartRow{ display:flex; gap:6px; align-items:center; margin-bottom:6px; }
  .routeStartRow button{
    flex-shrink:0; background:transparent; border:1.5px solid var(--line); border-radius:8px;
    padding:9px 12px; font-size:12px; font-weight:600; color:var(--ink-soft); cursor:pointer;
    font-family:inherit;
  }
  .routeStartRow button:hover{ background:var(--paper); border-color:var(--ink); color:var(--ink); }
  .routeStartRow button:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  /* The hints under these two controls are the only thing between them and
     the next label, so they carry the gap that the labels' own margin-top
     would otherwise have to fight. Empty most of the time — collapse to
     nothing rather than leaving a stripe of dead space. */
  #routeStartHint, #routeSpeedHint{ margin-bottom:10px; }
  #routeStartHint:empty, #routeSpeedHint:empty{ margin-bottom:4px; }
  #routeSaveStatus{
    font-size:11.5px; font-weight:600; margin-bottom:8px; display:flex; align-items:center; gap:6px;
  }
  #routeSaveStatus.saved{ color:var(--moss); }
  #routeSaveStatus.unsaved{ color:var(--ink-soft); }
  .routeAction{
    display:block; width:100%; text-align:center; background:transparent; border:1.5px solid var(--line);
    border-radius:9px; padding:10px; font-size:13px; font-weight:600; color:var(--ink); cursor:pointer;
    font-family:inherit; margin-bottom:8px; box-sizing:border-box;
  }
  .routeAction:hover{ background:var(--paper); border-color:var(--ink); }
  .routeAction.primary{
    background:var(--blaze); border-color:var(--blaze); color:var(--ink); font-weight:700;
  }
  .routeAction.primary:hover{ background:var(--blaze-dark); border-color:var(--blaze-dark); }
  .routeAction.primary:disabled{ opacity:0.6; cursor:default; }
  /* Plain (non-primary) actions can be disabled too — "Plan a route" is
     unavailable offline, since it's the one route entry point that needs
     the network. Hover must not still light up as if it were clickable. */
  .routeAction:disabled{ opacity:0.5; cursor:default; }
  .routeAction:disabled:hover{ background:transparent; border-color:var(--line); }
  #routeMenu select:disabled{ opacity:0.5; cursor:default; }
  #routeSaveForm{
    border:1px solid var(--line); border-radius:10px; padding:12px; background:var(--paper); margin-bottom:8px;
  }
  #routeNameInput{
    width:100%; border:1px solid var(--line); background:var(--white); border-radius:8px;
    padding:9px 11px; font-size:13.5px; font-family:inherit; color:var(--ink); outline:none;
    box-sizing:border-box; margin-bottom:10px;
  }
  #routeNameInput:focus{ border-color:var(--blaze); }
  .exportHelp{ margin-bottom:8px; }
  .exportHelp summary{
    font-size:11.5px; font-weight:700; color:var(--blaze-text); cursor:pointer;
    list-style:none; padding:2px 0;
  }
  .exportHelp summary::-webkit-details-marker{ display:none; }
  .exportHelp summary::before{ content:'\25B8 '; }
  .exportHelp[open] summary::before{ content:'\25BE '; }
  .exportHelp summary:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; border-radius:3px; }
  .exportHelpBody{
    font-size:11.5px; line-height:1.5; color:var(--ink-soft);
    border-left:2px solid var(--line); padding:6px 0 2px 10px; margin-top:6px;
  }
  .exportHelpBody p{ margin:0 0 7px; }
  .exportHelpBody p:last-child{ margin-bottom:0; }
  .exportHelpBody ul{ margin:0 0 7px; padding-left:16px; }
  .exportHelpBody li{ margin-bottom:5px; }
  .exportHelpBody code{
    background:var(--paper-dim); padding:1px 4px; border-radius:3px; font-size:10.5px;
    font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  }
  /* The folded-away timing/day controls in the route menu. Styled to match
     #routeStats — the app's other collapsible section of a panel — rather
     than .exportHelp, which is a help note rather than a group of controls.
     A rule of its own instead of a shared class because the two sit in
     different containers with different padding. */
  #routeAdvanced{ margin:8px 0 4px; }
  #routeAdvanced > summary{
    font-family:'Space Grotesk', sans-serif; font-size:11.5px; font-weight:700;
    text-transform:uppercase; letter-spacing:0.04em; color:var(--ink-soft);
    cursor:pointer; list-style:none; display:flex; align-items:center; gap:6px;
    padding:4px 0;
  }
  #routeAdvanced > summary::-webkit-details-marker{ display:none; }
  /* Own chevron rather than the native marker, which can't be styled
     consistently across browsers. Rotates to point down when open. */
  #routeAdvanced > summary::before{
    content:''; width:0; height:0; flex-shrink:0;
    border-left:4px solid currentColor; border-top:3.5px solid transparent; border-bottom:3.5px solid transparent;
    transition:transform 0.15s ease;
  }
  #routeAdvanced[open] > summary::before{ transform:rotate(90deg); }
  #routeAdvanced > summary:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; border-radius:3px; }
  .routeExportRow{ display:flex; gap:8px; }
  .routeExportRow .routeAction{ margin-bottom:8px; }
  .routeSaveFormActions{ display:flex; gap:8px; }
  .routeSaveFormActions .routeAction{ margin-bottom:0; }
  #routeBadge{
    display:none; position:absolute; top:-2px; right:-2px; width:9px; height:9px; border-radius:50%;
    background: var(--moss); border:1.5px solid var(--white);
  }
  #routeBadge.show{ display:block; }
  #drawBanner{
    position:absolute; top:106px; left:50%; transform:translateX(-50%); z-index:600;
    background: var(--ink); color: var(--paper); padding:10px 12px 10px 18px; border-radius:10px;
    font-size:13px; font-weight:600; box-shadow: var(--shadow); display:none;
    align-items:center; gap:10px; max-width:calc(100vw - 32px);
  }
  #drawBanner .pulse{
    width:9px; height:9px; border-radius:50%; background: var(--moss); flex-shrink:0;
    animation: pulse 1.2s infinite ease-in-out;
  }
  #drawBanner span:nth-child(2){ white-space:nowrap; }
  /* The planning banner carries a longer message than the straight-line one
     ("N waypoints added · drag to move, tap to remove"), which would push
     Done/Cancel off a phone screen while nowrap holds it on one line. Let it
     wrap on narrow viewports rather than truncating the hint — but give the
     text a floor to wrap against, otherwise flex shrinks it to a ~6-line
     ribbon beside the buttons. Full width on the banner keeps the two
     buttons in a stable position instead of shifting with the text length. */
  @media (max-width:560px){
    #drawBanner{ width:calc(100vw - 32px); padding-left:14px; }
    #drawBanner span:nth-child(2){ white-space:normal; flex:1; min-width:11ch; line-height:1.35; }
  }
  #finishDrawBtn, #cancelDrawBtn{
    background: var(--moss); color: var(--white); border:none; border-radius:8px; padding:8px 14px;
    font-size:12.5px; font-weight:700; cursor:pointer; font-family:inherit; flex-shrink:0;
  }
  #cancelDrawBtn.secondary{ background:transparent; border:1.5px solid var(--ink-soft); color:var(--paper); }

  /* Shown while a route is being recalculated after a stop is ticked into
     the plan or a waypoint is dragged. Same banner family as #drawBanner
     above (and the same top offset), since it occupies the same slot — the
     two are never visible at once: drawing has ended by the time a route
     exists to recalculate. */
  #routeBusyBanner{
    position:absolute; top:106px; left:50%; transform:translateX(-50%); z-index:600;
    background: var(--ink); color: var(--paper); padding:10px 18px; border-radius:10px;
    font-size:13px; font-weight:600; box-shadow: var(--shadow); display:none;
    align-items:center; gap:10px; max-width:calc(100vw - 32px);
  }
  #routeBusyBanner .pulse{
    width:9px; height:9px; border-radius:50%; background: var(--blaze); flex-shrink:0;
    animation: pulse 1.2s infinite ease-in-out;
  }

  #filtersMenu .filtersHead{
    display:flex; align-items:center; justify-content:space-between; margin-bottom:10px;
  }
  .filtersTitle{ font-size:12px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--ink-soft); }
  .filtersClear{ background:none; border:none; font-size:12px; font-weight:700; color:var(--blaze-text); cursor:pointer; padding:0; font-family:inherit; }
  #filtersMenu .chipWrap{ display:flex; flex-wrap:wrap; gap:8px; max-height:240px; overflow-y:auto; }

  .chip{
    flex-shrink:0; display:flex; align-items:center; gap:6px;
    padding:7px 13px; border-radius:999px; font-size:12.5px; font-weight:600;
    background: var(--paper); color: var(--ink); border:1px solid var(--line);
    cursor:pointer; user-select:none; white-space:nowrap;
    font-family:inherit; line-height:1.2;
  }
  .chip .dot{ width:9px; height:9px; border-radius:50%; display:inline-block; }
  .chip.active{ background: var(--ink); color: var(--paper); border-color: var(--ink); }

  /* Anchored to #searchWrapOuter (position:relative) the same way
     #searchResults is, rather than pinned to the viewport — so it never
     exceeds the search box's own width, on mobile or the narrower
     centered desktop layout. #searchResults and this banner are mutually
     exclusive (both driven by whether there's a text-search match), so
     sharing the same top-offset anchor never causes overlap. */
  #searchElsewhereBanner{
    position:absolute; top:calc(100% + 6px); left:0; right:0; z-index:495;
    background: rgba(251,251,248,0.97); border:1px solid var(--line); border-radius:10px;
    padding:9px 13px; display:none; align-items:center; justify-content:space-between; gap:10px;
    box-shadow: var(--shadow); font-size:12.5px; color:var(--ink);
  }
  #searchElsewhereBanner span{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  #searchElsewhereBanner button{
    background:none; border:none; color:var(--blaze-text); font-weight:700; font-size:12.5px;
    cursor:pointer; white-space:nowrap; font-family:inherit; flex-shrink:0;
  }
  #searchElsewhereBanner button:disabled{ opacity:0.6; cursor:default; }

  /* ---------- FAB ---------- */
  #fab{
    position:absolute; right:18px; bottom:calc(28px + env(safe-area-inset-bottom)); z-index:500;
    width:60px; height:60px; background: var(--blaze); color:var(--ink);
    display:flex; align-items:center; justify-content:center;
    clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);
    box-shadow: var(--shadow); cursor:pointer; border:none;
    transition: transform 0.15s ease;
  }
  #fab:hover{ transform: scale(1.06); }
  #fab:active{ transform: scale(0.96); }
  #fab svg{ width:24px; height:24px; }

  #fab.cancel{ background: var(--ink); color: var(--white); }

  /* ---------- Placement banner ---------- */
  #placeBanner{
    position:absolute; top:106px; left:50%; transform:translateX(-50%); z-index:600;
    background: var(--ink); color: var(--paper); padding:10px 12px 10px 18px; border-radius:10px;
    font-size:13px; font-weight:600; box-shadow: var(--shadow); display:none;
    align-items:center; gap:10px; max-width:calc(100vw - 32px);
  }
  #placeBanner .pulse{
    width:9px; height:9px; border-radius:50%; background: var(--blaze); flex-shrink:0;
    animation: pulse 1.2s infinite ease-in-out;
  }
  #placeBanner span:nth-child(2){ white-space:nowrap; }
  #confirmPlaceBtn{
    background: var(--blaze); color: var(--ink); border:none; border-radius:8px; padding:8px 14px;
    font-size:12.5px; font-weight:700; cursor:pointer; font-family:inherit; flex-shrink:0;
  }
  #confirmPlaceBtn:hover{ background: var(--blaze-dark); }
  @keyframes pulse{ 0%,100%{ opacity:1; transform:scale(1);} 50%{ opacity:0.4; transform:scale(1.5);} }

  /* Live-collaboration indicator: shown in the route menu whenever more than
     one person is connected to the route. Blue rather than the moss/blaze
     used elsewhere — those already mean "planning" and "needs attention",
     and this is neither. Reuses the same #2E7DA6 the water-source category
     uses rather than introducing another hue. */
  #collabPresence{
    display:flex; align-items:center; gap:7px; margin-bottom:8px;
    font-size:11.5px; font-weight:600; color:#2E7DA6;
  }
  .collabDot{
    width:9px; height:9px; border-radius:50%; background:#2E7DA6; flex-shrink:0;
    animation: pulse 1.2s infinite ease-in-out;
  }
  /* Respect a stated preference for less motion: the dot still marks the
     state by being present and coloured, it just stops pulsing. */
  @media (prefers-reduced-motion: reduce){
    .collabDot{ animation:none; }
  }

  /* ---------- Route planning: waypoint cursor and pins ----------
     While planning, the map is a place to drop waypoints rather than
     something to pan around, so the cursor says so. Leaflet's own
     .leaflet-grab (cursor:grab) and .leaflet-interactive (cursor:pointer)
     both sit on the container/children, so this mirrors the specificity of
     Leaflet's built-in .leaflet-crosshair rule to win against them. The
     SVG is inlined as a data URI rather than a separate file so it can't
     miss the service worker's precache list. Falls back to `copy`, which
     every platform has, on the rare browser that rejects the URI. */
  .planning-map, .planning-map .leaflet-grab, .planning-map .leaflet-interactive{
    cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'><path d='M12 2C7.6 2 4 5.6 4 10c0 5.5 8 16 8 16s8-10.5 8-16c0-4.4-3.6-8-8-8z' fill='%23FF8A00' stroke='%23FFFFFF' stroke-width='2'/><circle cx='12' cy='10' r='3' fill='%23FFFFFF'/></svg>") 12 26, copy;
  }
  /* A waypoint already on the map is draggable, so it gets the grab cursor
     instead of the drop-a-new-pin one. Leaflet adds .leaflet-marker-draggable
     to these; .leaflet-dragging (set on the container mid-drag) already
     supplies `grabbing` from Leaflet's own stylesheet. */
  .planning-map .leaflet-marker-draggable{ cursor:grab; }

  /* Clicking a stop doesn't drop a waypoint — it adds that stop to the
     route and re-routes through it — so the drop-a-pin cursor above would
     promise the wrong thing. A plus-shaped cursor reads as "add this one",
     matching the ✓ badge the marker gains once it's planned. The rule sits
     on the marker's own children too because the emoji <span> and the
     badges each sit above the pin and would otherwise inherit the
     container's pin cursor on hover.
     `cell` is the fallback: it's the closest thing to a plus that every
     platform ships, so a browser rejecting the data URI still shows
     something clearly distinct from the pin. */
  .planning-map .poi-marker,
  .planning-map .poi-marker *,
  .planning-map .fav-badge,
  .planning-map .plan-badge,
  .planning-map .open-badge,
  .planning-map .unknown-badge{
    cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26' height='26' viewBox='0 0 26 26'><circle cx='13' cy='13' r='10' fill='%230E8F49' stroke='%23FFFFFF' stroke-width='2'/><path d='M13 8v10M8 13h10' stroke='%23FFFFFF' stroke-width='2.5' stroke-linecap='round'/></svg>") 13 13, cell;
  }
  /* A stop already in the plan: clicking removes it again, so it shouldn't
     keep advertising "add". Minus instead of plus, same shape and size.
     The badges are siblings of .poi-marker (they're positioned over it by
     the wrapper div, not nested inside), so :has() is what lets the wrapper
     switch them too — without it the ✓ badge, which only ever appears on a
     planned stop, would show "add" while the pin beneath it shows "remove". */
  .planning-map .poi-marker.is-planned,
  .planning-map .poi-marker.is-planned *,
  .planning-map div:has(> .poi-marker.is-planned) .fav-badge,
  .planning-map div:has(> .poi-marker.is-planned) .plan-badge,
  .planning-map div:has(> .poi-marker.is-planned) .open-badge,
  .planning-map div:has(> .poi-marker.is-planned) .unknown-badge{
    cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='26' height='26' viewBox='0 0 26 26'><circle cx='13' cy='13' r='10' fill='%23B0402A' stroke='%23FFFFFF' stroke-width='2'/><path d='M8 13h10' stroke='%23FFFFFF' stroke-width='2.5' stroke-linecap='round'/></svg>") 13 13, cell;
  }
  /* A cluster zooms in rather than planning anything — neither cursor above
     applies, so it keeps the ordinary pointer. */
  .planning-map .poi-cluster{ cursor:pointer; }

  /* The route line can be grabbed and pulled to route via somewhere else
     (enableRouteLineDragging in route-ui.js), so it gets the grab cursor
     rather than the drop-a-pin one — same signal the draggable waypoint
     pins already use. Leaflet renders the line as an SVG <path> inside the
     overlay pane, which .planning-map .leaflet-interactive would otherwise
     claim for the pin cursor. */
  .planning-map .leaflet-overlay-pane path.leaflet-interactive{ cursor:grab; }
  .planning-map .leaflet-overlay-pane path.leaflet-interactive:active{ cursor:grabbing; }

  .waypoint-pin{
    width:22px; height:22px; border-radius:50%;
    background:var(--blaze); border:3px solid var(--white);
    box-shadow:0 2px 6px rgba(0,0,0,0.4);
    display:flex; align-items:center; justify-content:center;
    font-size:11px; font-weight:700; color:var(--ink); line-height:1;
    box-sizing:border-box;
  }
  /* The endpoints are what someone reasons about ("from here to there"), so
     they're visually distinct from the intermediate points they dragged in. */
  .waypoint-pin.endpoint{ background:var(--moss); color:var(--white); }
  /* The finish carries a flag glyph rather than a digit — slightly smaller
     type so the emoji sits on the same visual weight as the numbers. */
  .waypoint-pin.finish{ font-size:12px; }

  /* Where a riding day ends for the night.
     Square-ish (6px radius) rather than round, so it never reads as one of
     the circular waypoint pins or POI markers it sits among — the shape
     carries the distinction at a glance, before any colour does. The
     background is set inline, per day, from DAY_COLORS. */
  .day-break-pin{
    width:24px; height:24px; border-radius:6px;
    border:2.5px solid var(--white);
    box-shadow:0 2px 6px rgba(0,0,0,0.4);
    display:flex; align-items:center; justify-content:center;
    font-size:11px; font-weight:700; color:var(--white); line-height:1;
    box-sizing:border-box;
  }
  /* A night actually spent somewhere — a campsite, a guesthouse — carries the
     crescent, matching the control in the sidebar that placed it. Slightly
     larger, since the glyph is thinner than a digit at the same size. */
  .day-break-pin.overnight{ font-size:14px; }

  /* Direction chevrons along the route. Drawn as a rotated CSS triangle
     rather than a glyph so it stays crisp at any size and needs no font.
     pointer-events:none matters: these sit on top of the route line, which
     is itself draggable, and must never intercept that gesture. */
  .route-arrow{
    width:0; height:0; pointer-events:none;
    border-left:5px solid transparent;
    border-right:5px solid transparent;
    border-bottom:9px solid var(--white);
    filter: drop-shadow(0 0 1.5px rgba(20,26,20,0.85));
    margin:3.5px 3px;
  }

  .placing-pin{
    width:40px; height:40px; border-radius:50% 50% 50% 0; transform:rotate(-45deg);
    background:var(--blaze); border:3px solid var(--white); box-shadow:0 3px 10px rgba(0,0,0,0.45);
    display:flex; align-items:center; justify-content:center; cursor:grab;
    animation: placingPulse 1.4s infinite ease-in-out;
  }
  .placing-pin:active{ cursor:grabbing; animation:none; }
  .placing-pin.confirmed{ background:var(--moss); cursor:default; animation:none; }
  .placing-pin span{ transform:rotate(45deg); font-size:18px; font-weight:700; color:var(--ink); line-height:1; }
  .placing-pin.confirmed span{ color:var(--white); }
  @keyframes placingPulse{
    0%,100%{ box-shadow:0 3px 10px rgba(0,0,0,0.45), 0 0 0 0 rgba(255,138,0,0.5); }
    50%{ box-shadow:0 3px 10px rgba(0,0,0,0.45), 0 0 0 10px rgba(255,138,0,0); }
  }

  /* ---------- Route stops sidebar ---------- */
  #routeSidebar{
    position:absolute; z-index:540; background:var(--white); box-shadow:var(--shadow);
    display:flex; flex-direction:column; border-right:4px solid var(--moss); overflow:hidden;
  }
  #routeSidebar[hidden]{ display:none; }
  @media (min-width:721px){
    #routeSidebar{ top:0; left:0; bottom:0; width:340px; }
  }
  @media (max-width:720px){
    #routeSidebar{
      left:0; right:0; bottom:0; max-height:55vh; border-right:none;
      border-top:4px solid var(--moss); border-radius:16px 16px 0 0;
    }
  }
  #routeSidebarHeader{
    display:flex; align-items:flex-start; justify-content:space-between; gap:10px;
    padding:16px 16px 12px; border-bottom:1px solid var(--line); flex-shrink:0;
  }
  #routeSidebarTitle{
    font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:16px; margin:0; color:var(--ink);
  }
  #routeSidebarSummary{ font-size:11.5px; color:var(--ink-soft); margin:3px 0 0; }
  #routeSidebarCollapse{
    background:none; border:none; cursor:pointer; color:var(--ink-soft); padding:4px; flex-shrink:0; border-radius:6px;
  }
  #routeSidebarCollapse:hover{ background:var(--paper); color:var(--ink); }

  /* The collapsed stop list: a thin tab against the same edge the sidebar
     docks to, carrying the stop count so collapsing doesn't hide how many
     there are. Same z-index as the sidebar it replaces. */
  #routeSidebarExpand{
    position:absolute; z-index:540; display:flex; align-items:center; justify-content:center;
    gap:4px; cursor:pointer; background:var(--white); color:var(--ink);
    border:none; border-right:4px solid var(--moss); box-shadow:var(--shadow);
    font-family:inherit; font-size:12px; font-weight:700; padding:0;
  }
  #routeSidebarExpand[hidden]{ display:none; }
  #routeSidebarExpand:hover{ background:var(--paper); }
  #routeSidebarExpand:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  #routeSidebarExpandCount{
    background:var(--moss); color:var(--white); border-radius:9px;
    min-width:18px; padding:1px 5px; font-size:10.5px; line-height:16px;
  }
  @media (min-width:721px){
    /* Full-height strip down the left edge, mirroring the sidebar's dock. */
    #routeSidebarExpand{
      top:0; left:0; bottom:0; width:38px; flex-direction:column; gap:6px;
      border-radius:0 10px 10px 0;
    }
  }
  @media (max-width:720px){
    /* Bottom-docked, so the collapsed form is a handle along the bottom
       edge rather than a side strip — matching where the sheet lives. */
    #routeSidebarExpand{
      left:0; right:0; bottom:0; height:calc(38px + env(safe-area-inset-bottom));
      padding-bottom:env(safe-area-inset-bottom);
      border-right:none; border-top:4px solid var(--moss); border-radius:14px 14px 0 0;
    }
    /* Chevron points up on mobile: the list expands upward from the bottom. */
    #routeSidebarExpand svg{ transform:rotate(-90deg); }
  }
  /* Route stats block. Sits between the sidebar header and the category
     filters, matching their padding and divider so the sidebar still reads
     as one stack of sections rather than a panel with something bolted on. */
  #routeStats{
    padding:10px 16px 12px; border-bottom:1px solid var(--line); flex-shrink:0;
  }
  #routeStats > summary{
    font-family:'Space Grotesk', sans-serif; font-size:11.5px; font-weight:700;
    text-transform:uppercase; letter-spacing:0.04em; color:var(--ink-soft);
    cursor:pointer; list-style:none; display:flex; align-items:center; gap:6px;
  }
  #routeStats > summary::-webkit-details-marker{ display:none; }
  /* Own chevron rather than the native marker, which can't be styled
     consistently across browsers. Rotates to point down when open. */
  #routeStats > summary::before{
    content:''; width:0; height:0; flex-shrink:0;
    border-left:4px solid currentColor; border-top:3.5px solid transparent; border-bottom:3.5px solid transparent;
    transition:transform 0.15s ease;
  }
  #routeStats[open] > summary::before{ transform:rotate(90deg); }
  #routeStats > summary:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; border-radius:3px; }
  /* Wraps rather than shrinking: three stats fit one line on a desktop
     sidebar, and on a narrow bottom sheet the third dropping to its own row
     reads better than three cramped columns. */
  .routeStatRow{ display:flex; flex-wrap:wrap; gap:9px 18px; margin-top:9px; }
  .routeStat{ display:flex; flex-direction:column; gap:1px; }
  .routeStatLabel{
    font-size:10.5px; font-weight:700; text-transform:uppercase; letter-spacing:0.03em;
    color:var(--ink-soft);
  }
  .routeStatValue{
    font-family:'Space Grotesk', sans-serif; font-size:17px; font-weight:700; color:var(--ink);
  }
  .routeStatsNote{ font-size:11.5px; color:var(--ink-soft); margin:8px 0 0; }
  /* The profile stretches to whatever width the sidebar gives it —
     preserveAspectRatio="none" on the SVG means the path distorts with it,
     which is what a profile chart wants (the x axis is "along the route",
     not a real distance the aspect ratio has to preserve). */
  .routeProfile{ margin-top:10px; }
  .routeProfile svg{
    display:block; width:100%; height:52px; overflow:visible;
  }
  /* Translucent moss rather than a new palette entry: the fill only ever
     sits on the sidebar's white, and deriving it keeps the profile in step
     if --moss is ever retuned. */
  .routeProfile path{ fill:color-mix(in srgb, var(--moss) 18%, transparent); stroke:var(--moss); stroke-width:1.5; }
  .routeProfileAxis{
    display:flex; justify-content:space-between; margin-top:3px;
    font-size:10px; color:var(--ink-soft);
  }
  #routeSidebarFilters{
    padding:10px 16px 12px; border-bottom:1px solid var(--line); flex-shrink:0;
  }
  .routeSidebarFiltersHead{
    display:flex; align-items:center; justify-content:space-between; gap:8px; margin-bottom:8px;
  }
  #routeSidebarFilters .chipWrap{ display:flex; flex-wrap:wrap; gap:6px; }
  #routeSidebarFilters .chip{ font-size:11.5px; padding:6px 11px; }
  #routeSidebarFilters .chip:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  #routeFiltersClearBtn:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  #routeSidebarFooter{
    flex-shrink:0; padding:10px 16px calc(10px + env(safe-area-inset-bottom)); border-top:1px solid var(--line); background:var(--white);
  }
  #routeSidebarFooter .routeAction{ margin-bottom:0; }
  #routeStopList{
    list-style:none; margin:0; padding:0; overflow-y:auto;
    flex:1; min-height:0; /* min-height:0 is required — without it a flex item won't shrink below its content, pushing the footer out of view */
  }
  /* The trailing padding matters: the row's last control used to be the
     favorite star, whose 44px box left the glyph visually inset from the
     panel edge for free. Adding the overnight button after it put that
     button hard against the edge instead, where its crescent read as
     clipped. Pad the row rather than the button so the two trailing
     controls keep identical boxes. */
  .routeStopRow{
    display:flex; align-items:stretch; border-bottom:1px solid var(--line);
    padding-right:6px;
  }
  .routeStopRow:hover{ background:var(--paper); }
  .routeStop{
    display:flex; align-items:flex-start; gap:10px; flex:1; min-width:0; text-align:left;
    padding:11px 4px 11px 2px; border:none;
    background:none; cursor:pointer; font-family:inherit;
  }
  .routeStop:focus-visible{ outline:2px solid var(--blaze); outline-offset:-2px; }
  .routeStopRow.planned{ background:#E3F5EA; }
  .routeStopRow.planned:hover{ background:#D6EEDF; }
  .routeStopPlan{
    position:relative; flex-shrink:0; width:34px;
    display:flex; align-items:center; justify-content:center;
    border:none; background:none; cursor:pointer; padding:0;
    font-family:inherit; font-size:11px; font-weight:700; line-height:1;
    color:var(--white); z-index:0;
  }
  .routeStopPlan::before{
    content:''; position:absolute; width:18px; height:18px; border-radius:5px;
    border:1.5px solid var(--line); background:var(--white); z-index:-1;
  }
  .routeStopPlan.active::before{ background:var(--moss); border-color:var(--moss); }
  .routeStopPlan:focus-visible{ outline:2px solid var(--blaze); outline-offset:-2px; }
  .routeStopPlan:disabled{ opacity:0.5; cursor:default; }
  .savedRouteActions{ display:flex; flex-wrap:wrap; gap:12px; margin-top:6px; }
  /* Retention warning on a route flagged for deletion (see
     db/migrations/030_retention.sql). Uses the same amber treatment as the
     other "act on this" notices rather than a red/destructive one: nothing
     has been lost yet, and the route still works normally — it's a prompt,
     not an error. */
  .expiryNotice{
    display:flex; align-items:baseline; flex-wrap:wrap; gap:8px; margin-top:6px;
    padding:7px 9px; border-radius:7px;
    background:#FFF1DC; border-left:3px solid var(--blaze);
  }
  .expiryNoticeText{ font-size:11.5px; color:var(--blaze-text); font-weight:600; }
  .expiryNotice .keepRouteBtn{ flex-shrink:0; }
  /* The row itself stays visually normal — greying it out would suggest the
     route is already unusable, which it isn't. */
  .expiringRouteItem .deletedItemName{ color:var(--ink); }
  .linkBtn{
    background:none; border:none; padding:0; font-family:inherit; font-size:12px; font-weight:700;
    color:var(--blaze-text); cursor:pointer; text-decoration:underline;
  }
  .linkBtn:hover{ color:var(--blaze-dark); }
  .linkBtn:disabled{ opacity:0.6; cursor:default; text-decoration:none; }
  .linkBtn:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; border-radius:3px; }
  /* Stacks the Routes topic's two walkthrough links (plan a route / upload a
     GPX) — .linkBtn is inline, so without this they'd sit on one line and
     read as a single run-on sentence. align-items keeps each underline the
     width of its own label rather than the full panel. */
  .helpTourLinks{ display:flex; flex-direction:column; align-items:flex-start; gap:8px; }
  .deleteRouteBtn{ color:#B0402A; border-color:var(--line); padding:8px 11px; }
  .deleteRouteBtn:hover{ border-color:#B0402A; }
  .shareRouteBlock{ margin-top:10px; padding-top:10px; border-top:1px dashed var(--line); }
  /* The edit-link half of the share block. Same dashed divider as the block
     itself so the two link types read as siblings rather than one being
     nested inside the other — they are independently revocable and should
     look it. */
  .editShareBlock{ margin-top:10px; padding-top:10px; border-top:1px dashed var(--line); }
  .editShareBlock:empty{ margin-top:0; padding-top:0; border-top:none; }
  .editShareHead{
    font-family:'Space Grotesk', sans-serif; font-size:11.5px; font-weight:700;
    text-transform:uppercase; letter-spacing:0.04em; color:var(--ink-soft); margin-bottom:6px;
  }
  /* Time remaining is advisory, not a warning — muted until the expired
     state, which uses .expiryNotice's amber treatment instead. */
  .editShareExpiry{ font-weight:600; }
  .editShareUpsell{ display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
  .shareRouteRow{ display:flex; gap:8px; margin-bottom:8px; }
  .shareRouteLink{
    flex:1; min-width:0; font-size:12px; font-family:inherit; color:var(--ink-soft);
    background:var(--paper-dim); border:1px solid var(--line); border-radius:6px; padding:8px 10px;
  }
  .shareRouteBlock .routeAction{ flex-shrink:0; width:auto; margin-bottom:0; padding:8px 14px; }
  .shareRouteBlock .fieldHint{ margin-bottom:8px; }
  .savedRouteItem{ gap:8px; }
  /* A saved route is now a card rather than a text row. The delete button
     sits beside a block that's several lines tall, so it aligns to the top
     instead of floating in the vertical middle of the card. */
  .savedRouteCard{ align-items:flex-start; }
  .savedRouteCard .restoreBtn{ margin-top:2px; }
  /* Outline thumbnail left, stats and profile right. */
  .routeCardVisual{ display:flex; align-items:stretch; gap:12px; margin-top:8px; }
  .routeCardOutline{
    flex-shrink:0; width:62px; height:62px; border-radius:8px;
    background:var(--paper); border:1px solid var(--line);
    display:flex; align-items:center; justify-content:center; padding:4px;
  }
  .routeCardOutline svg{ display:block; width:100%; height:100%; overflow:visible; }
  /* Stroke only — the outline is a path, not an area, and filling it would
     turn a route that loops back on itself into a solid blob. */
  .routeCardOutline path{
    fill:none; stroke:var(--moss); stroke-width:4;
    stroke-linecap:round; stroke-linejoin:round;
    /* The path is drawn in a 100-unit box scaled into ~54px, so the stroke
       scales down with it — vector-effect keeps it a consistent hairline
       regardless of how far the route's bounding box had to be scaled. */
    vector-effect:non-scaling-stroke;
  }
  /* min-width:0 so a long stat row can shrink inside the flex row rather
     than forcing the card wider than the panel. */
  .routeCardStats{ flex:1; min-width:0; display:flex; flex-direction:column; justify-content:center; gap:6px; }
  .routeCardStats .routeStatRow{ margin-top:0; gap:6px 14px; }
  /* Tighter than the sidebar's copy of the same figures: a card carries
     three of these in a list, where the sidebar shows one at a time. */
  .routeCardStats .routeStatValue{ font-size:14px; }
  .routeCardStats .routeStatLabel{ font-size:9.5px; }
  .routeCardProfile{ margin-top:0; }
  .routeCardProfile svg{ height:26px; }
  .routeStopFav{
    flex-shrink:0; width:44px; border:none; background:none; cursor:pointer;
    font-size:16px; line-height:1; color:var(--ink-soft); font-family:inherit;
    display:flex; align-items:center; justify-content:center; padding:0;
  }
  .routeStopFav:hover{ color:var(--blaze); }
  .routeStopFav.active{ color:var(--blaze); }
  .routeStopFav:focus-visible{ outline:2px solid var(--blaze); outline-offset:-2px; }
  .routeStopFav:disabled{ opacity:0.5; cursor:default; }
  /* "End the day here" — same 44px column and borderless shape as the
     favorite star beside it, so a planned row reads as one strip of
     controls rather than a star plus a bolted-on extra. */
  /* Same 44px column and borderless shape as the favorite star beside it, so
     a planned row reads as one strip of controls.

     Off/on is carried by colour and weight, as the star carries it by
     colour and fill — not by opacity. Dimming the glyph to keep it from
     outshouting the star made it too faint to find, which is the wrong
     trade: an affordance nobody can see isn't quiet, it's missing.

     The chosen night gets a filled disc behind the crescent, the same
     shape-plus-colour treatment .routeStopPlan uses for its checkbox.
     Colour alone would be doing all the work — and font-weight does nothing
     to a symbol glyph, which has no bold cut in these fonts (measured: the
     700 and 400 crescents render at identical width). */
  .routeStopNight{
    position:relative; flex-shrink:0; width:44px; z-index:0;
    border:none; background:none; cursor:pointer; padding:0;
    font-size:17px; line-height:1; color:var(--ink-soft); font-family:inherit;
    display:flex; align-items:center; justify-content:center;
  }
  .routeStopNight::before{
    content:''; position:absolute; width:26px; height:26px; border-radius:50%;
    background:transparent; z-index:-1;
  }
  .routeStopNight:hover{ color:var(--ink); }
  .routeStopNight:hover::before{ background:var(--paper-dim); }
  .routeStopNight.active{ color:var(--white); }
  .routeStopNight.active::before{ background:var(--moss); }
  .routeStopNight:focus-visible{ outline:2px solid var(--blaze); outline-offset:-2px; }
  /* Sticky, so the day you're scrolled into is always named — on a week-long
     tour the header that matters is usually just above the viewport. */
  /* The sticky wrapper, so the day you're scrolled into stays named. The
     button inside it is the fold control — sticky has to live on the <li>
     rather than the button, since a position:sticky element only sticks
     within its own scroll parent. */
  .routeDayHeaderRow{
    position:sticky; top:0; z-index:2;
    border-top:1px solid var(--line); border-bottom:1px solid var(--line);
  }
  .routeDayHeader{
    width:100%; display:flex; align-items:baseline; gap:10px;
    padding:8px 14px 8px 10px; background:var(--paper-dim);
    border:none; cursor:pointer; font-family:inherit; text-align:left;
  }
  .routeDayHeader:hover{ background:var(--line); }
  .routeDayHeader:focus-visible{ outline:2px solid var(--blaze); outline-offset:-2px; }
  /* Own chevron rather than the native marker, matching #routeStats and
     #routeAdvanced — the app's two other collapsible sections. Points down
     when open, right when folded. */
  .routeDayHeader::before{
    content:''; width:0; height:0; flex-shrink:0; align-self:center;
    border-left:4px solid var(--ink-soft);
    border-top:3.5px solid transparent; border-bottom:3.5px solid transparent;
    transform:rotate(90deg); transition:transform 0.15s ease;
  }
  .routeDayHeader.collapsed::before{ transform:rotate(0deg); }
  /* The name takes the slack so the stats stay hard right, where they line
     up down the list rather than drifting with each day's name length. */
  .routeDayName{
    font-size:12px; font-weight:700; color:var(--ink); letter-spacing:0.01em;
    flex:1; min-width:0;
  }
  .routeDayStats{ font-size:11px; color:var(--ink-soft); flex-shrink:0; }
  /* Only shown while folded — open, the stops are right there to be counted. */
  .routeDayCount{ font-size:11px; color:var(--ink-soft); flex-shrink:0; }
  /* The overnight that closes a day. Indented and quieter than a stop row —
     it's a marker in the list, not something you can act on from here. */
  .routeDayBreak{
    padding:7px 14px 7px 20px; font-size:11.5px; color:var(--ink-soft);
    background:var(--paper-dim); border-bottom:1px solid var(--line);
  }
  #routeSidebarCollapse:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  /* On mobile the sheet collapses downward rather than sideways, so the
     collapse chevron points that way too. */
  @media (max-width:720px){
    #routeSidebarCollapse svg{ transform:rotate(-90deg); }
  }
  .routeStopIndex{
    flex-shrink:0; width:22px; height:22px; border-radius:50%; background:var(--paper-dim);
    color:var(--ink-soft); font-size:11px; font-weight:700; display:flex; align-items:center;
    justify-content:center; margin-top:1px;
  }
  .routeStopIcon{
    flex-shrink:0; width:26px; height:26px; border-radius:50%; display:flex; align-items:center;
    justify-content:center; font-size:13px;
  }
  .routeStopInfo{ min-width:0; flex:1; display:flex; flex-direction:column; gap:3px; }
  .routeStopName{ display:block; font-size:13.5px; font-weight:600; color:var(--ink); line-height:1.35; }
  .routeStopMeta{ display:block; font-size:11.5px; color:var(--ink-soft); line-height:1.4; }
  .routeStopMeta .closed{ color:#B0402A; }
  .routeStopMeta .open{ color:var(--moss); }
  /* The open/closed state as a pill on its own line rather than another
     comma-separated scrap of meta text — it's the one thing on the row you
     scan for when planning around opening hours, and it has to survive being
     read at a glance. Same palette as the detail panel's .openBadge, at the
     smaller size a list row can carry (that one's margin is tuned for a
     panel, so this isn't just a modifier on it). */
  /* No flex gap: the separator between the verdict and the time is a real
     "·" character in the markup (so it survives being read as text), and a
     gap on top of it would space them twice. */
  .routeStopPill{
    display:inline-flex; align-items:center; padding:2px 8px; border-radius:999px;
    font-size:10.5px; font-weight:700; margin-top:3px; white-space:nowrap;
  }
  .routeStopPill.open{ background:#E3F5EA; color:var(--moss); }
  .routeStopPill.closed{ background:#F2DFD8; color:#B0402A; }
  /* Hours simply aren't known — deliberately the quietest of the three, so
     it reads as "no information" rather than as a third verdict. */
  .routeStopPill.unknown{ background:var(--paper-dim); color:var(--ink-soft); }
  /* The arrival time the verdict was reached at. Lighter than the verdict
     itself: it's the supporting evidence, not the answer. Its separator is a
     real character in the markup rather than a CSS gap or pseudo-element, so
     it's there when the row is read as text too — "OpenArrive 09:14" runs
     together otherwise. */
  .routeStopPill .pillTime{ font-weight:600; opacity:0.75; }

  /* ---------- Bottom sheet / side panel ---------- */
  #panelOverlay{
    position:absolute; inset:0; background:rgba(20,26,20,0.35); z-index:700;
    display:none;
  }

  /* ---------- Tier comparison (panel-tiers.js) ----------
     Full-screen rather than the 400px #panel: three tiers side by side is
     the entire point, and the panel is a bottom sheet on mobile. Sits just
     below #confirmOverlay so a confirm raised from here still lands on top. */
  #tiersOverlay{
    position:fixed; inset:0; z-index:940; background:rgba(20,26,20,0.45);
    display:flex; align-items:center; justify-content:center; padding:20px;
  }
  #tiersOverlay[hidden]{ display:none; }
  #tiersDialog{
    background:var(--paper); border-radius:16px; box-shadow:var(--shadow);
    border-top:4px solid var(--blaze);
    width:min(920px, 100%); max-height:calc(100vh - 40px);
    display:flex; flex-direction:column; box-sizing:border-box;
  }
  #tiersHeader{
    display:flex; align-items:center; justify-content:space-between;
    padding:18px 22px 12px; flex-shrink:0;
  }
  #tiersTitle{
    font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:19px;
    letter-spacing:0.01em; color:var(--ink); margin:0;
  }
  #tiersClose{
    background:none; border:none; cursor:pointer; font-size:16px;
    color:var(--ink-soft); padding:4px 8px; border-radius:8px; line-height:1;
  }
  #tiersClose:hover{ background:var(--paper-dim); color:var(--ink); }
  #tiersClose:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  /* The scrolling region. The header stays put so the close button is always
     reachable — this page is taller than the viewport on most laptops. */
  #tiersBody{ overflow-y:auto; padding:0 22px 22px; }

  .tierGrid{
    display:grid; grid-template-columns:repeat(3, 1fr); gap:12px;
    align-items:stretch; margin-bottom:20px;
  }
  .tierCard{
    display:flex; flex-direction:column; background:var(--white);
    border:1.5px solid var(--line); border-radius:12px; padding:16px 14px;
    box-sizing:border-box;
  }
  /* The visitor's own tier, marked from live data rather than a hardcoded
     "most popular" badge — honest, and it tells them where they stand. */
  .tierCard.isCurrent{ border-color:var(--blaze); border-width:2px; padding:15px 13px; }
  .tierName{
    font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:15px;
    color:var(--ink); margin:0 0 2px;
  }
  .tierPrice{ font-size:13px; font-weight:700; color:var(--ink); margin-bottom:2px; }
  .tierInterval{ font-size:11.5px; color:var(--ink-soft); margin-bottom:12px; }
  .tierFeatures{ list-style:none; margin:0 0 14px; padding:0; flex:1 1 auto; }
  .tierFeatures li{
    font-size:12.5px; line-height:1.45; color:var(--ink-soft);
    padding:4px 0 4px 16px; position:relative;
  }
  .tierFeatures li::before{
    content:"\2022"; position:absolute; left:4px; color:var(--blaze); font-weight:700;
  }
  .tierDisclosure{
    font-size:11px; line-height:1.4; color:var(--ink-soft);
    margin-bottom:10px; padding-top:8px; border-top:1px solid var(--line);
  }
  .tierAction{
    font-family:inherit; font-size:13px; font-weight:700; cursor:pointer;
    border-radius:9px; padding:10px 12px; border:1.5px solid var(--line);
    background:var(--white); color:var(--ink); width:100%; margin-top:auto;
  }
  .tierAction:hover:not(:disabled){ background:var(--paper-dim); border-color:var(--ink-soft); }
  .tierAction:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  .tierAction.primary{ background:var(--blaze); border-color:var(--blaze); color:var(--white); }
  .tierAction.primary:hover:not(:disabled){ filter:brightness(0.94); }
  /* "Your plan" — present so each column has the same shape, but inert. */
  .tierAction:disabled{ opacity:0.55; cursor:default; }

  /* The code box is collapsed: most visitors are choosing a tier, not
     redeeming. <details> rather than a scripted toggle — it works before
     any JS wiring and is keyboard-accessible for free. */
  .tierCodeBox{ margin-bottom:18px; }
  .tierCodeBox > summary{
    cursor:pointer; font-size:13px; font-weight:700; color:var(--ink);
    padding:8px 0; list-style:revert;
  }
  .tierCodeBox > summary:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  .tierCodeBox .detailRow{ margin-top:8px; }

  @media (max-width:720px){
    /* Three columns cannot work here. Stacked in sort_order, so free reads
       first and patron last — the same order as the grid. */
    #tiersOverlay{ padding:0; align-items:stretch; }
    #tiersDialog{
      width:100%; max-height:100vh; border-radius:0; border-top:none;
      border-left:4px solid var(--blaze);
    }
    .tierGrid{ grid-template-columns:1fr; }
    #tiersBody{ padding:0 16px 16px; }
    #tiersHeader{ padding:16px 16px 10px; }
  }

  /* ---------- Confirm dialog (ui-confirm.js) ----------
     Sits above everything except the guided tour: it can be raised from the
     panel (z-index 800) and from the route menu, and must never appear
     behind whatever it's asking about. */
  #confirmOverlay{
    position:fixed; inset:0; z-index:950; background:rgba(20,26,20,0.45);
    display:flex; align-items:center; justify-content:center; padding:20px;
  }
  #confirmOverlay[hidden]{ display:none; }
  #confirmDialog{
    background:var(--paper); border-radius:16px; box-shadow:var(--shadow);
    border-top:4px solid var(--blaze); /* same accent the panel uses */
    width:min(400px, 100%); padding:22px 22px 18px; box-sizing:border-box;
  }
  #confirmTitle{
    font-family:'Space Grotesk', sans-serif; font-weight:700; font-size:17px;
    letter-spacing:0.01em; color:var(--ink); margin:0 0 8px;
  }
  #confirmMessage{
    font-size:13.5px; line-height:1.5; color:var(--ink-soft); margin:0 0 18px;
  }
  #confirmActions{ display:flex; gap:10px; justify-content:flex-end; flex-wrap:wrap; }
  #confirmActions button{
    font-family:inherit; font-size:13px; font-weight:700; cursor:pointer;
    border-radius:9px; padding:10px 16px; border:1.5px solid var(--line);
    background:var(--white); color:var(--ink); flex:1 1 auto; min-width:120px;
  }
  #confirmActions button:hover{ background:var(--paper-dim); border-color:var(--ink-soft); }
  #confirmActions button:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
  /* The destructive choice. Deliberately not the app's orange — that's the
     "go" accent on Save; a discard needs to read as a different kind of
     action, not a brighter version of the same one. */
  #confirmActions button.danger{
    background:#B0402A; border-color:#B0402A; color:var(--white);
  }
  #confirmActions button.danger:hover{ background:#98371F; border-color:#98371F; }
  @media (max-width:420px){
    /* Stack on narrow screens, destructive action last so it isn't the
       one under your thumb by default. */
    #confirmActions{ flex-direction:column-reverse; }
    #confirmActions button{ width:100%; }
  }
  #panel{
    position:absolute; z-index:800; background: var(--paper);
    box-shadow: var(--shadow); display:none; flex-direction:column;
    border-top:4px solid var(--blaze);
  }
  @media (max-width: 720px){
    #panel{ left:0; right:0; bottom:0; max-height:85vh; border-radius:18px 18px 0 0; }
  }
  @media (min-width: 721px){
    #panel{ top:0; left:0; bottom:0; width:400px; border-top:none; border-right:4px solid var(--blaze); }
  }
  #panelHeader{
    display:flex; align-items:flex-start; justify-content:space-between;
    padding:18px 20px 8px; border-bottom:1px solid var(--line); flex-shrink:0;
  }
  #panelHeader h2{
    font-family:'Space Grotesk', sans-serif; font-weight:700; letter-spacing:0.01em;
    font-size:20px; margin:0; color:var(--ink); text-transform:uppercase;
  }
  #panelClose{
    background:none; border:none; cursor:pointer; color:var(--ink-soft); padding:4px; flex-shrink:0;
  }
  /* Set on #panel while a guided-tour step needs the panel to stay open
     (e.g. tour-edit-stop.js's fake detail/edit views) — keeps the ✕ and
     the dimmed overlay looking exactly as normal, just unclickable, rather
     than visually disabling them and drawing attention to a state that's
     only temporary. */
  #panel.tourLockClose #panelClose{ pointer-events:none; }
  #panelOverlay.tourLockClose{ pointer-events:none; }
  #panelBody{ padding:16px 20px calc(24px + env(safe-area-inset-bottom)); overflow-y:auto; flex:1; }
  /* The mobile sheet's own bottom padding, on top of whatever padding the
     footer's inner content (.detailFooter, .routeUsageFooter) already
     has — so Report a problem / the routes usage bar don't end up flush
     against, or under, the home-indicator area. Desktop's #panel doesn't
     reach the bottom edge (see the min-width:721px rule above), so this
     is a no-op there regardless of env() value. */
  #panelFooter{ flex-shrink:0; padding-bottom:env(safe-area-inset-bottom); }
  #panelFooter[hidden]{ display:none; }

  .field{ margin-bottom:16px; }
  .field label{
    display:block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em;
    color: var(--ink-soft); margin-bottom:6px;
  }
  .field .opt{ font-weight:400; text-transform:none; letter-spacing:0; opacity:0.75; }
  .field input[type=text], .field textarea, .field select{
    width:100%; border:1px solid var(--line); background:var(--white); border-radius:8px;
    padding:10px 12px; font-size:14px; font-family:inherit; color:var(--ink); outline:none;
  }
  .field input[type=text]:focus, .field textarea:focus, .field select:focus{ border-color: var(--blaze); }
  .field textarea{ resize:vertical; min-height:64px; }
  .catRow{ display:flex; gap:8px; flex-wrap:wrap; }
  .catOpt{
    flex:1; min-width:80px; border:1.5px solid var(--line); border-radius:8px; padding:9px 6px;
    display:flex; flex-direction:column; align-items:center; gap:4px; cursor:pointer; background:var(--white);
    font-size:11.5px; font-weight:600; text-align:center;
  }
  .catOpt.selected{ border-color: var(--ink); background: var(--paper-dim); }
  .catOpt .emoji{ font-size:18px; }

  .formActions{ display:flex; gap:10px; margin-top:6px; }
  .formCancelBtn{
    flex:1; background:transparent; color:var(--ink); border:1.5px solid var(--line); border-radius:10px;
    padding:13px; font-size:14.5px; font-weight:700; cursor:pointer; font-family:inherit;
    letter-spacing:0.02em;
  }
  .formCancelBtn:hover{ background:var(--paper); border-color:var(--ink); }
  #saveBtn, .formSaveBtn{
    flex:2; background: var(--blaze); color:var(--ink); border:none; border-radius:10px;
    padding:13px; font-size:14.5px; font-weight:700; cursor:pointer;
    letter-spacing:0.02em;
  }
  #saveBtn:hover, .formSaveBtn:hover{ background: var(--blaze-dark); }
  #saveBtn:disabled, .formSaveBtn:disabled{ opacity:0.5; cursor:not-allowed; }
  .formError{ color:#B0402A; font-size:12.5px; margin-top:-8px; margin-bottom:14px; display:none; }

  /* ---------- Schedule editor ---------- */
  .scheduleBox{ border:1px solid var(--line); border-radius:10px; background:var(--white); padding:10px 12px; }
  .always247{ display:flex; align-items:center; gap:9px; padding-bottom:10px; margin-bottom:10px; border-bottom:1px dashed var(--line); }
  .always247 label{ font-size:13px; font-weight:600; color:var(--ink); cursor:pointer; }
  .dayRow{ display:flex; align-items:center; gap:8px; padding:6px 0; flex-wrap:wrap; }
  .dayRow .dayLabel{ width:34px; flex-shrink:0; font-size:12.5px; font-weight:700; color:var(--ink); }
  .switch{ position:relative; width:36px; height:20px; flex-shrink:0; }
  .switch input{ opacity:0; width:0; height:0; }
  .slider{
    position:absolute; cursor:pointer; inset:0; background:var(--line); border-radius:999px; transition:0.15s;
  }
  .slider:before{
    content:""; position:absolute; height:14px; width:14px; left:3px; top:3px; background:var(--white);
    border-radius:50%; transition:0.15s;
  }
  .switch input:checked + .slider{ background: var(--moss); }
  .switch input:checked + .slider:before{ transform: translateX(16px); }
  .switch input:disabled + .slider{ opacity:0.5; cursor:not-allowed; }
  .dayTimes{ display:flex; align-items:center; gap:6px; flex:1; min-width:170px; }
  .dayTimes input[type=time]{
    border:1px solid var(--line); border-radius:6px; padding:5px 6px; font-size:12.5px; font-family:inherit;
    background:var(--paper); color:var(--ink); width:92px;
  }
  .dayTimes input[type=time]:disabled{ opacity:0.5; cursor:not-allowed; }
  .dayTimes .dash{ color:var(--ink-soft); font-size:12px; }
  .dayClosedLabel{ font-size:12px; color:var(--ink-soft); font-style:italic; flex:1; }
  .copyRow{ margin-top:8px; }
  .copyRow button:disabled{ opacity:0.5; cursor:not-allowed; }
  /* Opening hours unknown: the 24h switch + every day row stay visible
     (not hidden, per the "unknown" toggle's own disable behavior above)
     but read as inert alongside them. */
  .scheduleBox.hoursUnknown #s_daysWrap, .scheduleBox.hoursUnknown .copyRow, .scheduleBox.hoursUnknown #s_247Row{ opacity:0.6; }
  .copyRow button{
    background:none; border:none; color:var(--steel); font-size:12px; font-weight:700; cursor:pointer; padding:0;
    text-decoration:underline;
  }
  .fieldHint{ font-size:11.5px; color:var(--ink-soft); margin-top:5px; }

  /* ---------- Recently deleted ---------- */
  .deletedItem{
    display:flex; align-items:center; gap:12px; padding:12px 0; border-bottom:1px solid var(--line);
  }
  .deletedItem:last-child{ border-bottom:none; }
  .deletedItemIcon{
    width:34px; height:34px; border-radius:50%; display:flex; align-items:center; justify-content:center;
    font-size:16px; flex-shrink:0; opacity:0.55; filter:grayscale(1);
  }
  .deletedItemInfo{ min-width:0; flex:1; }
  .deletedItemName{ font-size:13.5px; font-weight:600; color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
  .deletedItemMeta{ font-size:11.5px; color:var(--ink-soft); margin-top:2px; }
  .restoreBtn{
    flex-shrink:0; background:none; border:1.5px solid var(--line); color:var(--ink); border-radius:8px;
    padding:8px 13px; font-size:12.5px; font-weight:700; cursor:pointer; font-family:inherit;
  }
  .restoreBtn:hover{ border-color:var(--ink); }
  .restoreBtn:disabled{ opacity:0.6; cursor:default; }

  /* ---------- Detail view ---------- */
  .detailTopRow{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:14px; }
  .detailCatBadge{
    display:inline-flex; align-items:center; gap:6px; padding:5px 11px; border-radius:999px;
    font-size:11.5px; font-weight:700; color:var(--white);
  }
  .favToggleBtn{
    background:none; border:1.5px solid var(--line); border-radius:50%; width:34px; height:34px;
    display:flex; align-items:center; justify-content:center; font-size:17px; line-height:1; cursor:pointer;
    color:var(--ink-soft); flex-shrink:0; font-family:inherit; padding:0;
  }
  .favToggleBtn:hover{ border-color:var(--ink); }
  .favToggleBtn.active{ border-color:var(--blaze); color:var(--blaze); background:#FFF1DC; }
  .openBadge{
    display:inline-flex; align-items:center; gap:6px; padding:5px 11px; border-radius:999px;
    font-size:11.5px; font-weight:700; margin-bottom:14px;
  }
  .openBadge.open{ background:#E3F5EA; color:var(--moss); }
  .openBadge.closed{ background:#F2DFD8; color:#B0402A; }
  .detailRow{ margin-bottom:14px; }
  .detailRow .k{ font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em; color:var(--ink-soft); margin-bottom:4px; }
  .detailRow .v{ font-size:14px; color:var(--ink); line-height:1.5; }
  .scheduleList{ width:100%; border-collapse:collapse; }
  .scheduleList td{ font-size:13px; padding:3px 0; color:var(--ink); }
  .scheduleList td.d{ font-weight:700; width:70px; color:var(--ink-soft); }
  .scheduleList tr.today td{ color:var(--blaze-text); font-weight:700; }
  .tagRow{ display:flex; flex-wrap:wrap; gap:6px; }
  .tag{ background:var(--paper-dim); border:1px solid var(--line); border-radius:999px; padding:4px 10px; font-size:12.5px; }

  .modeRow{ display:flex; gap:6px; margin-bottom:10px; }
  .modeBtn{
    flex:1; border:1.5px solid var(--line); background:var(--white); border-radius:8px;
    padding:8px 4px; font-size:12px; font-weight:600; cursor:pointer; text-align:center; color:var(--ink);
    font-family:inherit;
  }
  .modeBtn.active{ border-color: var(--ink); background: var(--paper-dim); }
  .directionsBtn{
    display:block; text-align:center; background: var(--blaze); color: var(--ink); text-decoration:none;
    border-radius:10px; padding:11px; font-weight:700; font-size:13.5px; letter-spacing:0.02em;
  }
  .directionsBtn:hover{ background: var(--blaze-dark); }
  .utilRow{ display:flex; gap:8px; margin-top:8px; }
  .utilBtn{
    flex:1; display:flex; align-items:center; justify-content:center; gap:6px;
    border:1.5px solid var(--line); background:var(--white); border-radius:8px;
    padding:9px 6px; font-size:12.5px; font-weight:600; color:var(--ink); cursor:pointer; font-family:inherit;
  }
  .utilBtn:hover{ background: var(--paper); }
  .utilBtn:disabled{ opacity:0.6; cursor:default; }
  .utilBtn.copied{ border-color: var(--moss); color: var(--moss); background:#E3F5EA; }
  #deleteBtn{
    width:100%; background:transparent; border:1.5px solid #B0402A; color:#B0402A; border-radius:10px;
    padding:11px; font-size:13.5px; font-weight:700; cursor:pointer; margin-top:10px;
  }
  #deleteBtn:hover{ background:#B0402A; color:var(--white); }
  .editStopBtn{
    width:100%; background:transparent; border:1.5px solid var(--line); color:var(--ink); border-radius:10px;
    padding:11px; font-size:13.5px; font-weight:700; cursor:pointer; margin-top:10px; font-family:inherit;
  }
  .editStopBtn:hover{ background:var(--paper); border-color:var(--ink); }

  /* ---------- Still accurate? confirmation ---------- */
  .confirmBtn{
    width:100%; background:transparent; border:1.5px solid var(--moss); color:var(--moss); border-radius:10px;
    padding:10px; font-size:13px; font-weight:700; cursor:pointer; font-family:inherit;
  }
  .confirmBtn:hover{ background:#E3F5EA; }
  .confirmBtn:disabled{ opacity:0.7; cursor:default; }

  /* ---------- Report a problem ---------- */
  .reportToggleBtn{
    background:none; border:none; color:var(--ink-soft); font-size:12.5px; font-weight:600; cursor:pointer;
    padding:0; font-family:inherit; text-decoration:underline; margin-bottom:8px;
  }
  .reportForm{ border:1px solid var(--line); border-radius:10px; padding:12px; background:var(--paper); }
  .reportLabel{
    display:block; font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em;
    color:var(--ink-soft); margin-bottom:6px;
  }
  .reportForm select, .reportForm textarea{
    width:100%; border:1px solid var(--line); background:var(--white); border-radius:8px; padding:9px 10px;
    font-size:13.5px; font-family:inherit; color:var(--ink); outline:none; margin-bottom:8px;
  }
  .reportForm textarea{ min-height:56px; resize:vertical; }
  .reportActions{ display:flex; gap:8px; justify-content:flex-end; }
  #reportSubmitBtn{
    background:var(--blaze); color:var(--ink); border:none; border-radius:8px; padding:8px 14px;
    font-size:12.5px; font-weight:700; cursor:pointer; font-family:inherit;
  }
  #reportSubmitBtn:disabled{ opacity:0.6; cursor:default; }
  #reportCancelBtn{ padding:8px 14px; }

  /* ---------- Marker ---------- */
  .poi-cluster{
    display:flex; align-items:center; justify-content:center; border-radius:50%;
    background:var(--moss); color:var(--white); font-family:'Space Grotesk', sans-serif; font-weight:700;
    border:3px solid var(--white); box-shadow:0 3px 8px rgba(0,0,0,0.35);
  }
  .poi-marker{
    width:34px; height:34px; border-radius:50% 50% 50% 0; transform: rotate(-45deg);
    display:flex; align-items:center; justify-content:center; box-shadow:0 3px 8px rgba(0,0,0,0.35);
    border:2px solid var(--white); transition: filter 0.2s, opacity 0.2s;
  }
  .poi-marker span{ transform: rotate(45deg); font-size:16px; }
  .poi-marker.is-closed{ filter: grayscale(1); opacity:0.45; }
  .poi-marker.is-pending{ border-style:dashed; opacity:0.88; }
  .poi-marker.is-pending-error{ border-color:#E24B4A; border-style:dashed; }
  /* Signal color for a stop that's in the current route plan (see
     .routeStopPlan.active in the sidebar, which uses the same --moss) —
     a ring around the pin rather than recoloring it, so the category
     color/emoji stays legible and this reads as an overlay, not a
     different kind of marker. */
  .poi-marker.is-planned{
    border-color:var(--moss); box-shadow:0 0 0 3px rgba(14,143,73,0.35), 0 3px 8px rgba(0,0,0,0.35);
  }
  /* ---------- POI hover card ----------
     Bound to each marker with Leaflet's bindTooltip (see poiTooltipHtml in
     map-render.js), so the base .leaflet-tooltip styles come from Leaflet's
     own stylesheet and are overridden here rather than rebuilt. Only on
     devices that can hover — markers on a touchscreen never get one bound.

     Styled as the app's own small dark surface (--ink/--paper, the same
     vocabulary as the nudges) rather than Leaflet's default white box with
     a grey border, which reads as belonging to the map library instead of
     to the app. */
  .leaflet-tooltip.poiTip{
    background:var(--ink); color:var(--paper);
    border:none; border-radius:9px; box-shadow:var(--shadow);
    padding:7px 10px; max-width:220px; white-space:normal;
    font-family:inherit; font-size:12px; line-height:1.4;
  }
  .leaflet-tooltip.poiTip::before{ border-top-color:var(--ink); } /* the callout arrow, which Leaflet colours to match its own default background */
  .poiTip .poiTipName{ display:block; font-weight:700; }
  /* Deliberately dimmed against the name: the category is context for the
     name, not a second heading. Not --paper-dim, which is near-white and
     would read as loud as the name on this surface. */
  .poiTip .poiTipCat{ display:block; color:rgba(251,251,248,0.65); font-size:11px; margin-top:1px; }
  /* The same three verdicts, in the same colour language, as the sidebar's
     .routeStopPill — a stop must not read as open in one place and closed
     in another. Lightened here because these sit on --ink rather than on a
     pale pill: the pill's own #B0402A and --moss don't clear 4.5:1 against
     a dark background. "Unknown" stays the quietest of the three, matching
     the pill's reasoning that it's an absence of information rather than a
     third verdict. */
  .poiTip .poiTipOpen{ display:block; font-size:11px; font-weight:600; margin-top:3px; }
  .poiTip .poiTipOpen.is-open{ color:#6FD79A; }
  .poiTip .poiTipOpen.is-closed{ color:#FFA995; }
  .poiTip .poiTipOpen.is-unknown{ color:rgba(251,251,248,0.6); }
  .fav-badge{
    position:absolute; top:-3px; right:-3px; width:15px; height:15px; border-radius:50%;
    background:var(--blaze); color:var(--ink); font-size:9px; line-height:15px; text-align:center;
    border:1.5px solid var(--white); box-shadow:0 1px 3px rgba(0,0,0,0.3); font-weight:700;
  }
  .plan-badge{
    position:absolute; bottom:-2px; right:-3px; width:15px; height:15px; border-radius:50%;
    background:var(--moss); color:var(--white); font-size:9px; line-height:15px; text-align:center;
    border:1.5px solid var(--white); box-shadow:0 1px 3px rgba(0,0,0,0.3); font-weight:700;
  }
  /* Bottom-left — the one corner .fav-badge/.plan-badge don't use. Only
     ever rendered for a confirmed "open now" on a supermarket/gas station
     (see map-render.js's showsOpenBadge()), so a plain dot is enough —
     unlike the star/checkmark badges, there's no "which state" ambiguity
     to resolve with a glyph, since its mere presence already means one
     specific thing. */
  .open-badge{
    position:absolute; bottom:-2px; left:-3px; width:15px; height:15px; border-radius:50%;
    background:var(--moss); border:1.5px solid var(--white); box-shadow:0 1px 3px rgba(0,0,0,0.3);
  }
  /* Same corner and size as the open dot — the two are mutually exclusive
     (see makeIcon), so they can share the spot. Amber rather than green or
     red: this is neither good news nor bad, it's "nobody has recorded this",
     and it should read as a caution to check rather than as a verdict.
     Deliberately not greyed into the background — a stop whose hours are
     unknown is exactly the one worth noticing while planning. */
  .unknown-badge{
    position:absolute; bottom:-2px; left:-3px; width:15px; height:15px; border-radius:50%;
    background:var(--blaze); border:1.5px solid var(--white); box-shadow:0 1px 3px rgba(0,0,0,0.3);
    display:flex; align-items:center; justify-content:center;
    font-size:10px; font-weight:800; line-height:1; color:var(--ink);
  }

  /* ---------- "You are here" marker — distinct blue, never used by any POI category ---------- */
  .user-loc-wrap{ position:relative; width:50px; height:50px; display:flex; align-items:center; justify-content:center; }
  .user-loc-halo{
    position:absolute; width:44px; height:44px; border-radius:50%; background: rgba(30,136,229,0.28);
    animation: userLocPulse 2.2s infinite cubic-bezier(0.2,0.6,0.4,1);
  }
  .user-loc-dot{
    position:relative; width:16px; height:16px; border-radius:50%; background:#1E88E5;
    border:3px solid var(--white); box-shadow:0 1px 5px rgba(0,0,0,0.4); z-index:1;
  }
  @keyframes userLocPulse{
    0%{ transform:scale(0.25); opacity:0.9; }
    100%{ transform:scale(1); opacity:0; }
  }

  /* ---------- Loading / empty states ---------- */
  #stateOverlay{
    position:absolute; inset:0; z-index:900; background: var(--paper);
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; text-align:center; padding:24px;
  }
  #stateOverlay .mark{ height:34px; width:auto; }
  #stateOverlay p{ color: var(--ink-soft); font-size:14px; max-width:280px; margin:0; }
  #stateOverlay button{
    background: var(--ink); color:var(--paper); border:none; border-radius:8px; padding:10px 18px;
    font-weight:700; font-size:13px; cursor:pointer;
  }
  .spinner{
    width:26px; height:26px; border:3px solid var(--line); border-top-color: var(--blaze); border-radius:50%;
    animation: spin 0.8s linear infinite;
  }
  @keyframes spin{ to{ transform: rotate(360deg); } }

  #shareNote{
    position:absolute; bottom:calc(10px + env(safe-area-inset-bottom)); left:14px; z-index:400; font-size:10.5px; color: var(--paper-dim);
    background: rgba(20,32,26,0.55); padding:5px 10px; border-radius:14px; pointer-events:none;
    max-width: calc(58% - 20px); line-height:1.4;
  }

  #offlineNote{
    position:absolute; bottom:calc(10px + env(safe-area-inset-bottom)); right:14px; z-index:400; font-size:10.5px; font-weight:700;
    background: var(--blaze); color: var(--ink); padding:5px 11px; border-radius:14px;
    pointer-events:none; display:none; align-items:flex-start; gap:5px;
    max-width: calc(38% - 20px); line-height:1.4;
  }
  #offlineNote .dot{ width:6px; height:6px; border-radius:50%; background:var(--ink); flex-shrink:0; margin-top:4px; }

  /* Transient toast, e.g. "'Weekend loop' is ready offline" once a region
     download finishes (see offline-regions.js's downloadOfflineRegion() /
     main.js's watcher). Top-center, matching #placeBanner/#drawBanner's
     existing banner styling so it reads as the same family of "active status"
     UI, but higher (top:16px, above the search bar's dropdown) since a toast
     needs to stay clear of the header controls that banner sits below. */
  #toast{
    position:absolute; top:16px; left:50%; transform:translateX(-50%) translateY(-8px);
    z-index:900; background: var(--ink); color: var(--paper); padding:10px 16px;
    border-radius:10px; font-size:13px; font-weight:600; box-shadow: var(--shadow);
    max-width:calc(100vw - 32px); text-align:center; pointer-events:none;
    opacity:0; transition: opacity 0.25s ease, transform 0.25s ease;
  }
  #toast.show{ opacity:1; transform:translateX(-50%) translateY(0); }

  /* Shown only if a viewport fetch is still pending after a short delay
     (see showMapLoadingIndicator() in map-render.js) — a cache hit never
     reaches this, since it resolves before the delay elapses. */
  #mapLoadingIndicator{
    position:absolute; top:14px; right:14px; z-index:400; pointer-events:none; display:none;
    background: rgba(250,248,244,0.9); border-radius:50%; padding:6px;
  }
  #mapLoadingIndicator .spinner{ width:16px; height:16px; border-width:2px; }
  #shareNote .short{ display:none; }
  @media (max-width:480px){
    #shareNote .long{ display:none; }
    #shareNote .short{ display:inline; }
  }

  .leaflet-top.leaflet-left{ top: 158px; }
  @media (min-width:721px){
    .leaflet-top.leaflet-left{ top: 108px; }
    /* keep zoom controls clear of the route sidebar when it's open */
    body.route-sidebar-open .leaflet-top.leaflet-left{ left: 344px; }
    /* Collapsed: only the 38px strip to clear, not the full sidebar. */
    body.route-sidebar-collapsed .leaflet-top.leaflet-left{ left: 42px; }
  }

/* ---------- Accounts ---------- */
/* The sign-in form is the first place in the app with two text inputs in
   one row. Inputs are inline-block by default, so without this the email
   and password sit side by side, each too narrow to read. */
#panelBody input[type="email"],
#panelBody input[type="password"]{
  display:block; width:100%; box-sizing:border-box; margin-bottom:8px;
}

/* ---------- Recovery codes ---------- */
.recoveryCodeBox{
  display:flex; align-items:center; gap:10px; margin:10px 0 8px;
  background:var(--paper); border:1px solid var(--line); border-radius:10px; padding:12px;
}
.recoveryCodeBox code{
  flex:1; font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size:15px; font-weight:700; letter-spacing:0.06em; color:var(--ink);
  word-break:break-all;
}
.recoveryWarning{
  background:#FFF1DC; border-left:3px solid var(--blaze); border-radius:0;
  padding:9px 11px; color:var(--blaze-text); line-height:1.5;
}
#claimCodeInput{
  width:100%; box-sizing:border-box; border:1px solid var(--line); background:var(--white);
  border-radius:8px; padding:10px 12px; margin-bottom:8px;
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size:14px; letter-spacing:0.05em; color:var(--ink); outline:none; text-transform:uppercase;
}
#claimCodeInput:focus{ border-color:var(--blaze); }

/* ---------- Recovery nudge / welcome help nudge ----------
   Same dismissible-banner shape, same bottom position — the two are never
   shown at the same time (see main.js's maybeShowWelcomeHelp() /
   maybeShowRecoveryNudge()), so sharing one rule set keeps them visually
   identical without duplicating ~25 lines per nudge. */
#recoveryNudge, #welcomeHelpNudge{
  position:absolute; z-index:560; left:16px; right:16px; bottom:88px;
  max-width:520px; margin:0 auto;
  background:var(--ink); color:var(--paper); border-radius:12px;
  box-shadow:var(--shadow); padding:12px 14px;
  display:flex; align-items:center; gap:10px; font-size:12.5px; line-height:1.45;
}
#recoveryNudge[hidden], #welcomeHelpNudge[hidden]{ display:none; }
#recoveryNudge .nudgeIcon, #welcomeHelpNudge .nudgeIcon{ flex-shrink:0; font-size:16px; }
#recoveryNudge .nudgeText, #welcomeHelpNudge .nudgeText{ flex:1; min-width:0; }
#nudgeActionBtn, #welcomeHelpActionBtn{
  flex-shrink:0; background:var(--blaze); color:var(--ink); border:none; border-radius:8px;
  padding:8px 13px; font-size:12.5px; font-weight:700; font-family:inherit; cursor:pointer;
}
#nudgeActionBtn:hover, #welcomeHelpActionBtn:hover{ background:var(--blaze-dark); }
#nudgeDismissBtn, #welcomeHelpDismissBtn{
  flex-shrink:0; background:none; border:none; color:var(--paper-dim); cursor:pointer;
  font-size:14px; padding:4px 2px; font-family:inherit; line-height:1;
}
#nudgeDismissBtn:hover, #welcomeHelpDismissBtn:hover{ color:var(--white); }
#nudgeActionBtn:focus-visible, #nudgeDismissBtn:focus-visible,
#welcomeHelpActionBtn:focus-visible, #welcomeHelpDismissBtn:focus-visible{ outline:2px solid var(--blaze); outline-offset:2px; }
@media (max-width:560px){
  #recoveryNudge, #welcomeHelpNudge{ flex-wrap:wrap; bottom:96px; }
  #recoveryNudge .nudgeText, #welcomeHelpNudge .nudgeText{ flex-basis:100%; order:1; }
  #nudgeActionBtn, #welcomeHelpActionBtn{ order:2; }
  #nudgeDismissBtn, #welcomeHelpDismissBtn{ order:3; margin-left:auto; }
  #recoveryNudge .nudgeIcon, #welcomeHelpNudge .nudgeIcon{ display:none; }
}

/* ---------- Route usage indicator (compact — in the route menu) ---------- */
#routeUsageIndicator{ display:flex; align-items:center; gap:8px; }
#routeUsageIndicator .usageBar{
  display:block; flex:1; height:5px; border-radius:3px; background:var(--paper-dim); overflow:hidden; max-width:120px;
}
/* display:block is required here — .usageBar/.usageBarFill are <span>s (see
   route-ui.js/panel-lists.js), and width/height are no-ops on an inline
   element per the CSS spec, so without this the fill silently collapses to
   0x0 regardless of the width percentage set on it. */
#routeUsageIndicator .usageBarFill{ display:block; height:100%; background:var(--moss); border-radius:3px; transition:width 0.3s ease; }
/* Shared thresholds with the fuller bar below: orange at 75% used, red at
   90%, matching the fuller bar in My routes so the signal reads the same
   wherever it appears. */
#routeUsageIndicator.warning .usageBarFill{ background:var(--blaze); }
#routeUsageIndicator.critical .usageBarFill, #routeUsageIndicator.at-limit .usageBarFill{ background:#B0402A; }
.fieldHint.at-limit{ color:#B0402A; font-weight:600; }

/* ---------- Route usage footer (prominent — bottom of My routes) ---------- */
/* position:sticky on the last child of #panelBody (a scrolling container)
   keeps this pinned to the bottom of the visible list regardless of scroll
   position, without needing a separate fixed-footer slot in the shared
   panel chrome — the panel is reused for many other views this shouldn't
   affect. */
.routeUsageFooter{
  padding:14px 20px; background:var(--paper); border-top:1px solid var(--line);
  box-shadow:0 -4px 10px rgba(0,0,0,0.05);
}
.routeUsageFooterLabel{ font-size:12.5px; font-weight:600; color:var(--ink-soft); margin-bottom:8px; }
.routeUsageFooter .usageBar{
  display:block; height:8px; border-radius:4px; background:var(--paper-dim); overflow:hidden;
}
.routeUsageFooter .usageBarFill{ display:block; height:100%; background:var(--moss); border-radius:4px; transition:width 0.3s ease; }
.routeUsageFooter.warning .usageBarFill{ background:var(--blaze); }
.routeUsageFooter.warning .routeUsageFooterLabel{ color:var(--blaze-text); }
.routeUsageFooter.critical .usageBarFill, .routeUsageFooter.at-limit .usageBarFill{ background:#B0402A; }
.routeUsageFooter.critical .routeUsageFooterLabel, .routeUsageFooter.at-limit .routeUsageFooterLabel{ color:#B0402A; font-weight:700; }

/* ---------- Stop-detail footer (report a problem, OSM attribution) ---------- */
/* Same treatment as .routeUsageFooter above — both live in the shared
   #panelFooter flex slot (flex-shrink:0, outside #panelBody's scroll
   area), so this is always the last thing in the panel regardless of how
   much detail content is above it. */
.detailFooter{
  padding:14px 20px; background:var(--paper); border-top:1px solid var(--line);
  box-shadow:0 -4px 10px rgba(0,0,0,0.05);
}
.detailFooter .fieldHint{ margin-bottom:8px; }
.detailFooter .fieldHint:last-child{ margin-bottom:0; }

/* ---------- Guided tour (tour.js) ----------
   Sits above everything else in the app — #panel is the previous highest
   fixed layer at z-index 800, #stateOverlay/#toast at 900 — so the tour
   needs 1000+ to spotlight controls even while a panel or toast is
   visible underneath. Four scrim pieces frame a rectangular cutout around
   the live target rather than a single div with a CSS mask, so there's no
   mask-support fallback to worry about and the math is just four rects. */
#tourScrim{ position:fixed; inset:0; z-index:1000; pointer-events:none; }
.tourScrimPiece{ background:rgba(20,26,20,0.55); }
#tourHole{
  position:fixed; z-index:1000; pointer-events:none;
  border:2px solid var(--blaze); border-radius:8px;
  box-shadow:0 0 0 4px rgba(255,138,0,0.25);
}
#tourPopover{
  position:fixed; z-index:1001; background:var(--white); border-radius:12px;
  box-shadow:var(--shadow); padding:14px 16px; max-width:280px; display:none;
}
.tourStepCount{
  font-size:11px; font-weight:700; text-transform:uppercase; letter-spacing:0.06em;
  color:var(--blaze-text); margin-bottom:6px;
}
.tourStepText{ font-size:13px; color:var(--ink); line-height:1.5; margin-bottom:10px; }
.tourExitBtn{
  background:none; border:none; padding:0; font-family:inherit; font-size:12px;
  font-weight:600; color:var(--ink-soft); cursor:pointer; text-decoration:underline;
}
.tourExitBtn:hover{ color:var(--ink); }

/* ---------- Tour launcher (the first-run "Show me around" popover) ----------
   Sits where the welcome nudge it replaces sat, and borrows that nudge's
   positioning rules (centred, inset from both edges, clear of the bottom
   controls) so one hands off to the other in place rather than the list
   appearing somewhere unrelated to the button that asked for it.

   z-index is deliberately below #tourScrim's 1000: the launcher hides itself
   for the duration of a tour (tour-launcher.js's runTour()), so it never has
   to compete with the spotlight — and if it somehow did, being underneath is
   the harmless way round. */
#tourLauncher{
  position:absolute; z-index:560; left:16px; right:16px; bottom:88px;
  max-width:380px; margin:0 auto;
  background:var(--white); border-radius:12px; box-shadow:var(--shadow);
  padding:14px 16px;
}
#tourLauncher[hidden]{ display:none; }
.tourLauncherHead{ display:flex; align-items:center; gap:10px; margin-bottom:10px; }
.tourLauncherTitle{ flex:1; min-width:0; font-size:13px; font-weight:700; color:var(--ink); }
.tourLauncherClose{
  flex-shrink:0; background:none; border:none; color:var(--ink-soft); cursor:pointer;
  font-size:14px; padding:4px 2px; font-family:inherit; line-height:1;
}
.tourLauncherClose:hover{ color:var(--ink); }
.tourLauncherList{ display:flex; flex-direction:column; gap:2px; margin-bottom:10px; }
.tourLauncherItem{
  display:flex; align-items:center; gap:9px; width:100%; text-align:left;
  background:none; border:none; border-radius:8px; padding:9px 8px;
  font-family:inherit; font-size:13px; color:var(--ink); cursor:pointer;
}
.tourLauncherItem:hover{ background:var(--paper); }
.tourLauncherItemLabel{ flex:1; min-width:0; }
/* Fixed width whether or not there's a tick in it, so the labels line up as
   one column instead of shifting left as tours get completed. */
.tourLauncherCheck{
  flex-shrink:0; width:14px; text-align:center;
  font-size:12px; font-weight:700; color:var(--moss);
}
/* Done rows stay fully legible rather than being greyed out — they're still
   worth re-running, and the tick already says which ones are finished. */
.tourLauncherItem.done .tourLauncherItemLabel{ color:var(--ink-soft); }
.tourLauncherClose:focus-visible, .tourLauncherItem:focus-visible{
  outline:2px solid var(--blaze); outline-offset:-2px; border-radius:8px;
}
/* The note left behind when the launcher is dismissed — says where the
   walkthroughs went, since the welcome nudge that opened it is gone for
   good by then. Borrows the dark-pill look of #recoveryNudge/#welcomeHelpNudge
   rather than the launcher's own white card: it's a passing status message,
   not another thing to interact with, and it sits in the same spot the
   launcher just vacated. */
#tourLauncherHint{
  position:absolute; z-index:560; left:16px; right:16px; bottom:88px;
  max-width:380px; margin:0 auto;
  background:var(--ink); color:var(--paper); border-radius:12px;
  box-shadow:var(--shadow); padding:12px 14px;
  font-size:12.5px; line-height:1.45;
  animation:tourHintIn 160ms ease-out;
}
@keyframes tourHintIn{ from{ opacity:0; transform:translateY(4px); } to{ opacity:1; transform:none; } }
@media (prefers-reduced-motion:reduce){
  #tourLauncherHint{ animation:none; }
}
@media (max-width:560px){
  #tourLauncher, #tourLauncherHint{ bottom:96px; }
}

/* ---------- Supporter panel ---------- */

.supporterPerks{
  margin:0 0 14px; padding:0 0 0 20px; color:var(--ink);
  font-size:14px; line-height:1.7;
}
.supporterPerks li{ margin-bottom:2px; }

/* Same monospace/uppercase treatment as #claimCodeInput: a supporter code
   is read off a receipt email and typed by hand, so it wants the same
   character-by-character legibility a recovery code does. */
#supporterCodeInput{
  width:100%; box-sizing:border-box; border:1px solid var(--line); background:var(--white);
  border-radius:8px; padding:10px 12px; margin-bottom:8px;
  font-family:ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size:14px; letter-spacing:0.05em; color:var(--ink); outline:none; text-transform:uppercase;
}
#supporterCodeInput:focus{ border-color:var(--blaze); }

/* Not uppercased or monospaced, unlike the code field — this is a person's
   chosen name, shown publicly exactly as they typed it. */
#supporterNameInput{
  width:100%; box-sizing:border-box; border:1px solid var(--line); background:var(--white);
  border-radius:8px; padding:10px 12px; margin-bottom:8px;
  font-size:14px; color:var(--ink); outline:none;
}
#supporterNameInput:focus{ border-color:var(--blaze); }

.supporterList{ display:flex; flex-wrap:wrap; gap:6px; }
.supporterChip{
  background:var(--paper); border:1px solid var(--line); border-radius:999px;
  padding:4px 11px; font-size:13px; color:var(--ink);
}
