
  *{margin:0;padding:0;box-sizing:border-box}

  /* ── DARK MODE (default) ── */
/* ── LIGHT MODE ── */
body{font-family:var(--font-sans);background:var(--bg);color:var(--text);height:100vh;height:100dvh;display:flex;flex-direction:column;overflow:hidden}

  /* ── HEADER ── */
  header{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:var(--panel);border-bottom:1px solid var(--border);flex-shrink:0;z-index:1000}
  .logo{display:flex;align-items:center;gap:10px}
  .logo h1{font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--text)}
  .logo p{font-size:13px;color:var(--muted);font-weight:300}
  .header-right{display:flex;align-items:center;gap:10px}
  .counter{display:flex;align-items:center;gap:8px;background:var(--panel2);border:1px solid var(--border);border-radius:20px;padding:6px 14px}
  .counter-nums{font-family:var(--font-num);font-size:18px;font-weight:700;color:var(--green)}
  .counter-label{font-size:13px;color:var(--muted);font-weight:300}
  .progress-ring-wrap{width:32px;height:32px}
  svg.ring{transform:rotate(-90deg)}
  .ring-bg{fill:none;stroke:var(--border);stroke-width:3}
  .ring-fill{fill:none;stroke:var(--green);stroke-width:3;stroke-linecap:round;transition:stroke-dashoffset 0.6s ease}

  /* auth buttons */
  .auth-btn{display:flex;align-items:center;gap:6px;border:1px solid var(--border);border-radius:8px;font-family:var(--font-sans);font-size:14px;font-weight:500;padding:7px 13px;cursor:pointer;transition:all 0.2s}
  .btn-login{background:transparent;color:var(--muted)}
  .btn-login:hover{border-color:var(--green-dim);color:var(--green)}
  .btn-signup{background:var(--green-dark);border-color:var(--green-dark);color:var(--on-accent)}
  .btn-signup:hover{background:var(--green-dim)}
  .btn-logout{background:transparent;color:var(--muted);border-color:var(--border);font-size:13px;padding:5px 10px}
  .btn-logout:hover{color:var(--red);border-color:var(--red)}
  .user-chip{display:flex;align-items:center;gap:7px;background:var(--panel2);border:1px solid var(--border);border-radius:20px;padding:5px 12px;font-size:13px;color:var(--muted)}
  .user-avatar{width:22px;height:22px;border-radius:50%;background:var(--green-dark);display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--on-accent);flex-shrink:0;overflow:hidden}
  .user-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
  .user-avatar.has-photo{background:transparent}

  /* ── MAP ── */
  /* min-height:0 is critical: flex items default to min-height:auto and refuse
   to shrink below content size, which can blow the 100vh layout apart and push
   the footer/ads off-layout. This pins the map to exactly the leftover space. */
  #map{flex:1;min-height:0;width:100%}
  .map-search-wrap{
    position:relative;z-index:800;
    padding:8px 12px;
    background:var(--panel);
    border-bottom:1px solid var(--border);
    flex-shrink:0;
  }
  .map-search-inner{
    display:flex;align-items:center;gap:8px;
    background:var(--panel2);border:1px solid var(--border);
    border-radius:22px;padding:0 14px;
    transition:border-color 0.2s;
  }
  .map-search-inner:focus-within{border-color:var(--green);}
  .map-search-icon{color:var(--green);font-size:15px;flex-shrink:0;}
  .map-search-input{
    flex:1;background:transparent;border:none;outline:none;
    color:var(--text);font-size:15px;font-family:var(--font-sans);
    padding:10px 0;
  }
  .map-search-input::placeholder{color:var(--muted);}
  .map-search-clear{
    background:transparent;border:none;color:var(--muted);
    font-size:18px;cursor:pointer;padding:0 0 0 4px;
    display:none;line-height:1;
  }
  .map-search-results{
    position:absolute;top:calc(100% - 4px);left:12px;right:12px;
    background:var(--panel2);border:1px solid var(--border);border-radius:12px;
    box-shadow:0 8px 24px rgba(0,0,0,0.6);
    max-height:240px;overflow-y:auto;z-index:900;display:none;
  }
  /* A row, not a stack: the name and the address go on the left and the price
     and the distance sit against the right edge, which is where every list of
     places puts them and where the eye scans for them. */
  .map-search-result-item{
    padding:10px 14px;cursor:pointer;border-bottom:1px solid var(--border);
    display:flex;align-items:center;gap:10px;
  }
  .map-search-result-text{
    display:flex;flex-direction:column;gap:2px;min-width:0;flex:1;
  }
  .map-search-result-item:last-child{border-bottom:none;}
  .map-search-result-item:hover,.map-search-result-item:active{background:var(--panel);}
  .map-search-result-name{color:var(--text);font-size:15px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
  .map-search-result-addr{color:var(--muted);font-size:13px;}
/* Price and distance, on the right, in the list - because "which of these
   four is nearest and what does it cost" is the question the search box is
   actually being asked, and answering it three taps later behind a pin is
   answering it too late. */
.map-search-result-meta{
  margin-left:auto;flex-shrink:0;text-align:right;
  display:flex;flex-direction:column;gap:2px;
  font-size:12px;color:var(--text-3);line-height:1.2;
}
.map-search-result-meta b{color:var(--accent);font-weight:700;font-variant-numeric:tabular-nums}
.map-search-result-item.is-active{background:var(--hover)}

  /* ── THEME TOGGLE ── */
  .theme-toggle{
    width:36px;height:36px;border-radius:50%;
    background:var(--panel2);border:1px solid var(--border);
    color:var(--muted);font-size:16px;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
    transition:all 0.2s;flex-shrink:0;
  }
  .theme-toggle:hover{border-color:var(--green);color:var(--green)}

  /* ── POPUP ── */
  .leaflet-popup-content-wrapper{background:var(--panel)!important;border:1px solid var(--border)!important;border-radius:14px!important;box-shadow:0 8px 32px rgba(0,0,0,0.6)!important;padding:0!important;min-width:290px}
  .leaflet-popup-content{margin:0!important;width:100%!important}
  .leaflet-popup-tip-container{display:none}
  .leaflet-popup-close-button{color:var(--muted)!important;font-size:18px!important;top:10px!important;right:12px!important}
  .leaflet-popup-close-button:hover{color:var(--text)!important}

  /* visited toggle */
  .visited-toggle{display:flex;align-items:center;gap:8px;cursor:pointer;user-select:none}
  .toggle-track.on{background:var(--green-dark)}
  .toggle-track.on .toggle-thumb{left:21px}
  .visited-label{font-size:14px;font-weight:500}

  /* stars */
  .star.lit{color:var(--amber)}
  .star.readonly{cursor:default}
  .star.readonly:hover{transform:none}

  /* drinks */
  .drinks-section{margin-bottom:4px}
  .price-currency{font-size:13px;color:var(--muted);flex-shrink:0}
  .drink-readonly span{color:var(--green);font-weight:500}

  /* notes */

  /* save btn */
  .save-btn.saved{background:var(--brand-600)}

  /* guest lock banner */
  .guest-banner p{font-size:13px;color:var(--muted)}
  .guest-banner button{background:var(--green-dark);border:none;border-radius:7px;color:var(--on-accent);font-family:var(--font-sans);font-size:13px;padding:5px 12px;cursor:pointer;white-space:nowrap}

  /* toast */
  /* Anchored to the bottom, above whatever the nav and the filter bar occupy.
     It used to be top:70px with a max-width:600px rule adding a `bottom` on top
     of that, top and bottom both set with height:auto is over-constrained, so
     the pill stretched into a full-height green column down every phone
     screen. One anchor, one rule, one shape at every width. */
  /* Shape, colour and motion live in shared.css. What stays here is only
     where the toast sits when this page has something else on the bottom
     edge. */
  body:has(.filter-bar.show) .toast{bottom:calc(var(--tabbar-safe) + 56px)}
  /* During a crawl the active bar owns the bottom edge. */
  body.crawl-active .toast{bottom:calc(var(--tabbar-safe) + 96px)}

  /* ── AUTH MODAL ── */
  /* Signing in can be asked for from inside any other dialog, logging a price
     from the pub sheet, saving a crawl, so it sits above the dialog band
     rather than in it. */
  .auth-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:calc(var(--z-dialog) + 40);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.25s;padding:20px;overflow-y:auto;-webkit-overflow-scrolling:touch}
  .auth-backdrop.open{opacity:1;pointer-events:all}
  .modal{background:var(--panel);border:1px solid var(--border);border-radius:18px;padding:32px;width:360px;max-width:92vw;box-shadow:0 24px 64px rgba(0,0,0,0.7)}
  .modal-logo{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:24px}
  /* Radius scaled from .home-brand-mark, 9px on 34px, so the tile keeps the
     same corner as the one on the launcher and in the headers. */
  .modal-logo-mark{width:44px;height:44px;flex-shrink:0;border-radius:12px;display:block}
  .modal-logo h2{font-family:var(--font-display);font-size:22px;font-weight:700}
  .modal-logo p{font-size:13px;color:var(--muted)}
  .modal-tabs{display:flex;background:var(--panel2);border-radius:10px;padding:3px;margin-bottom:20px}
  .modal-tab{flex:1;text-align:center;padding:7px;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;color:var(--muted);transition:all 0.2s}
  .modal-tab.active{background:var(--green-dark);color:var(--on-accent)}
  .modal-field{margin-bottom:14px}
  .modal-field label{display:block;font-size:13px;color:var(--muted);margin-bottom:5px;font-weight:500;text-transform:uppercase;letter-spacing:0.5px}
  .modal-field input{width:100%;background:var(--panel2);border:1px solid var(--border);border-radius:9px;color:var(--text);font-family:var(--font-sans);font-size:15px;padding:9px 12px;outline:none;transition:border-color 0.2s}
  .modal-field input:focus{border-color:var(--green-dim)}
  .modal-submit{width:100%;background:var(--green-dark);border:none;border-radius:10px;color:var(--on-accent);font-family:var(--font-sans);font-size:15px;font-weight:600;padding:11px;cursor:pointer;margin-top:4px;transition:background 0.2s}
  .modal-submit:hover{background:var(--green-dim)}
  .modal-error{font-size:13px;color:var(--red);margin-top:8px;min-height:16px}
  .modal-close{position:absolute;top:14px;right:16px;background:transparent;border:none;color:var(--muted);font-size:20px;cursor:pointer;line-height:1}
  .modal-close:hover{color:var(--text)}
  .auth-modal-box{position:relative;background:var(--panel);border-radius:16px;padding:28px;width:100%;max-width:400px;box-shadow:0 24px 80px rgba(0,0,0,0.7);max-height:calc(100vh - 40px);max-height:calc(100dvh - 40px);overflow-y:auto;-webkit-overflow-scrolling:touch;margin:auto}

  /* loading */
  #loading{position:fixed;inset:0;background:var(--bg);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:var(--z-toast);gap:14px}
  #loading p{color:var(--muted);font-size:15px}
  .spinner{width:34px;height:34px;border:3px solid var(--border);border-top-color:var(--green);border-radius:50%;animation:spin 0.8s linear infinite}
  @keyframes spin{to{transform:rotate(360deg)}}

  /* ── PASSPORT ── */
  /* `overflow-y:auto` + `margin:auto` on the child, rather than
     `align-items:center`, is deliberate. A flex item centred with align-items
     that is TALLER than its container overflows equally in both directions, and
     the half above the top is unreachable, you cannot scroll to it, because
     scroll containers have no negative scroll range. Auto margins centre the
     same way but keep the overflow reachable. Without this the passport was cut
     off at BOTH ends on a phone in landscape (510px of card, 390px of screen)
     with no way to see either. */
  .passport-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:var(--z-dialog);display:flex;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.35s ease;padding:20px;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
  .passport-backdrop.open{opacity:1;pointer-events:all}
  .passport-tap-wrap{display:flex;flex-direction:column;align-items:center;width:100%;max-width:380px;margin:auto}
  .tap-nozzle{display:flex;flex-direction:column;align-items:center}
  .tap-body{width:18px;height:22px;background:linear-gradient(180deg,var(--panel-2),var(--border));border-radius:4px 4px 0 0;border:1.5px solid var(--accent);border-bottom:none}
  .tap-head{width:30px;height:10px;background:linear-gradient(180deg,var(--border),var(--panel-2));border-radius:3px;border:1.5px solid var(--accent);margin-top:-1px}
  .tap-stream{width:6px;height:0px;background:linear-gradient(180deg,var(--accent),rgba(var(--brand-rgb),0.2));border-radius:3px;transition:height 0.45s cubic-bezier(0.4,0,0.6,1)}
  /* ── FRIEND PROFILE MODAL ── */
  /* Opens from on top of the friends panel, so it has to outrank it. */
  .friend-profile-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.85);z-index:calc(var(--z-dialog) + 20);display:flex;align-items:center;justify-content:center;padding:16px;backdrop-filter:blur(6px)}
  .friend-profile-card{width:100%;max-width:380px;max-height:90vh;overflow-y:auto;-webkit-overflow-scrolling:touch;overflow-x:hidden;background:linear-gradient(160deg,var(--bg) 0%,var(--panel) 50%,var(--panel-2) 100%);border:1px solid var(--border);border-radius:20px;position:relative;box-shadow:0 40px 100px rgba(0,0,0,0.95),inset 0 1px 0 rgba(var(--brand-rgb),0.15);animation:fpSlideUp 0.35s cubic-bezier(0.22,1,0.36,1)}
  @keyframes fpSlideUp{from{opacity:0;transform:translateY(40px)}to{opacity:1;transform:translateY(0)}}
  .friend-profile-banner{background:linear-gradient(90deg,var(--on-accent),var(--brand-700));padding:12px 16px;display:flex;align-items:center;justify-content:space-between;font-size:13px;font-weight:700;letter-spacing:1px;color:var(--accent);text-transform:uppercase}
  .friend-profile-inner{padding:20px}
  .friend-profile-head{display:flex;align-items:center;gap:14px;margin-bottom:16px}
  .friend-profile-av{width:68px;height:68px;border-radius:50%;flex-shrink:0;background:linear-gradient(135deg,var(--brand-600),var(--accent));display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:700;color:var(--on-accent);box-shadow:0 0 0 3px rgba(var(--brand-rgb),0.2),0 4px 20px rgba(0,0,0,0.5);overflow:hidden}
  .friend-profile-name{font-size:20px;font-weight:800;color:var(--text);letter-spacing:-0.3px}
  .friend-profile-sub{font-size:13px;color:var(--text-2);margin-top:2px}
  .friend-profile-rank{display:inline-block;margin-top:6px;background:rgba(var(--brand-rgb),0.15);border:1px solid rgba(var(--brand-rgb),0.3);border-radius:20px;padding:3px 10px;font-size:13px;font-weight:700;color:var(--accent)}
  .friend-profile-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin:14px 0}
  .fp-stat{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.07);border-radius:12px;padding:10px 12px;text-align:center}
  .fp-stat-val{font-size:22px;font-weight:800;color:var(--accent)}
  .fp-stat-label{font-size:12px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.5px;margin-top:2px}
  .fp-stat.wide{grid-column:1/-1;display:flex;align-items:center;gap:10px;text-align:left}
  .fp-divider{border:none;border-top:1px solid rgba(var(--brand-rgb),0.1);margin:14px 0}
  .fp-progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
  .fp-progress-title{font-size:13px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.5px}
  .fp-progress-pct{font-size:13px;color:var(--accent);font-weight:700}
  .fp-progress-bar{height:6px;background:rgba(255,255,255,0.08);border-radius:99px;overflow:hidden}
  .fp-progress-fill{height:100%;background:linear-gradient(90deg,var(--brand-600),var(--accent));border-radius:99px;transition:width 0.8s cubic-bezier(0.22,1,0.36,1)}
  .fp-chat-btn{width:100%;margin-top:14px;padding:11px;background:rgba(var(--brand-rgb),0.15);border:1px solid rgba(var(--brand-rgb),0.4);color:var(--accent);border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;font-family:var(--font-sans);transition:background 0.2s}
  .fp-chat-btn:hover{background:rgba(var(--brand-rgb),0.28)}
  .fp-remove-btn{width:100%;margin-top:8px;padding:10px;background:rgba(var(--danger-rgb),0.1);border:1px solid rgba(var(--danger-rgb),0.3);color:var(--danger);border-radius:10px;font-size:14px;font-weight:700;cursor:pointer;font-family:var(--font-sans);transition:background 0.2s}
  .fp-remove-btn:hover{background:rgba(var(--danger-rgb),0.2)}
  .friend-profile-overlay{display:none}
  .friend-profile-overlay.open{display:flex}

  .passport-card{width:100%;background:linear-gradient(160deg,var(--bg) 0%,var(--panel) 50%,var(--panel-2) 100%);border:1px solid var(--border);border-radius:20px;overflow:hidden;position:relative;box-shadow:0 40px 100px rgba(0,0,0,0.95),inset 0 1px 0 rgba(var(--brand-rgb),0.15);clip-path:inset(100% 0 0 0 round 20px);transition:none}
  .passport-card.poured{clip-path:inset(0% 0 0 0 round 20px);transition:clip-path 0.8s cubic-bezier(0.22,1,0.36,1)}
  .passport-shimmer{position:absolute;inset:0;background:linear-gradient(135deg,rgba(var(--brand-rgb),0.07) 0%,transparent 60%);pointer-events:none;z-index:0}
  .passport-banner{background:linear-gradient(90deg,var(--brand-600),var(--accent) 50%,var(--brand-600));padding:11px 18px;display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1}
  /* The title yields, the controls do not. There are now two buttons and a
     close in this row, and on a 360px phone something has to give, a
     truncated "PINPINT PASSP…" over a card that says PinPint Passport
     everywhere else costs nothing, and a wrapped or clipped button is a
     control somebody cannot press. */
  .passport-banner > span{
    font-size:13px;font-weight:800;letter-spacing:2px;text-transform:uppercase;
    color:var(--on-accent);
    min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }
  @media (max-width:420px){
    .passport-banner{padding:11px 12px;gap:8px}
    .passport-banner > span{letter-spacing:1px}
  }
  .passport-close{width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,0.3);border:none;color:var(--on-accent);font-size:18px;font-weight:900;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0;line-height:1}
  .passport-close:hover{background:rgba(0,0,0,0.6);color:var(--on-accent);transform:scale(1.1)}
  .passport-inner{position:relative;z-index:1;padding:22px}
  .passport-profile{display:flex;align-items:center;gap:14px;margin-bottom:16px}
  .passport-avatar{width:58px;height:58px;border-radius:50%;flex-shrink:0;background:linear-gradient(135deg,var(--brand-600),var(--accent));display:flex;align-items:center;justify-content:center;font-size:24px;font-weight:700;color:var(--on-accent);box-shadow:0 0 0 3px rgba(var(--brand-rgb),0.2),0 4px 20px rgba(0,0,0,0.5);overflow:hidden;cursor:pointer;position:relative}
  .passport-avatar .pp-av-inner{width:58px;height:58px;border-radius:50%;overflow:hidden;display:flex;align-items:center;justify-content:center;position:absolute;inset:0;z-index:1}
  .passport-avatar .pp-cam-hint{position:absolute;inset:0;border-radius:50%;background:rgba(0,0,0,0.5);display:flex;align-items:center;justify-content:center;font-size:18px;opacity:0;transition:opacity 0.2s;z-index:2;pointer-events:none}
  .passport-avatar:hover .pp-cam-hint,.passport-avatar:active .pp-cam-hint{opacity:1}
  .passport-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
  .passport-name{font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--text);line-height:1.2}
  .passport-since{font-size:13px;color:var(--text-2);margin-top:2px}
  .passport-rank-pill{display:inline-flex;align-items:center;gap:4px;margin-top:6px;background:linear-gradient(90deg,var(--warn),var(--warn));border-radius:20px;padding:3px 11px;font-size:13px;font-weight:700;color:var(--on-warn)}
  .pp-divider{border:none;border-top:1px solid rgba(var(--brand-rgb),0.1);margin:14px 0}
  .passport-stats{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:14px}
  .pp-stat{background:rgba(0,0,0,0.35);border:1px solid rgba(var(--brand-rgb),0.1);border-radius:14px;padding:13px 14px}
  .pp-stat-val{font-family:var(--font-num);font-size:26px;font-weight:700;color:var(--accent);line-height:1}
  .pp-stat-label{font-size:12px;color:var(--text-2);margin-top:5px;text-transform:uppercase;letter-spacing:0.5px}
  .pp-stat.wide{grid-column:1/-1;display:flex;align-items:center;gap:12px}
  .pp-stat.wide .pp-stat-val{font-size:15px;color:var(--text)}
  .pp-stat-icon{font-size:24px}
  .pp-progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:7px}
  .pp-progress-title{font-size:13px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.5px}
  .pp-progress-pct{font-size:15px;font-weight:700;color:var(--accent)}
  .pp-progress-bar{height:8px;background:rgba(0,0,0,0.5);border-radius:4px;overflow:hidden;border:1px solid rgba(var(--brand-rgb),0.1)}
  .pp-progress-fill{height:100%;width:0%;background:linear-gradient(90deg,var(--brand-600),var(--accent));border-radius:4px;transition:width 1.1s cubic-bezier(0.4,0,0.2,1) 0.6s;box-shadow:0 0 10px rgba(var(--brand-rgb),0.4)}
  /* Deliberately quieter than the two buttons in the banner above it. Sharing
     is the last thing you do with this card, not the first, the card has to
     be worth sharing before the button matters. */
  /* Two buttons, one row: a link and an image. Made a grid rather than given
     the buttons a width, so the margin-top stays on one element and the pair
     cannot drift apart when one label is longer than the other. */
  .pp-share-row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:18px}
  .pp-share-row .pp-share-btn{margin-top:0;padding:11px 10px}
  .pp-share-btn{
    display:flex;align-items:center;justify-content:center;gap:8px;
    width:100%;margin-top:18px;min-height:44px;padding:11px 16px;
    border-radius:10px;
    background:var(--brass-ghost);border:1px solid var(--brass-500);
    color:var(--brass-300);
    font-family:var(--font-sans);font-size:14px;font-weight:700;
    letter-spacing:0.3px;cursor:pointer;
    transition:background var(--dur,.18s) ease;
  }
  .pp-share-btn:hover{background:rgba(var(--brand-rgb),0.1)}
  .pp-share-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .pp-share-btn[disabled]{opacity:.6;cursor:default}
  /* The same button at the bottom of the badges panel, where somebody has just
     looked at the wall and is most likely to want a copy of it. */
  .pp-card-btn{
    display:flex;align-items:center;justify-content:center;gap:8px;
    width:100%;margin-top:18px;min-height:48px;padding:12px 16px;
    border-radius:12px;
    background:var(--brass-ghost);border:1px solid var(--brass-500);
    color:var(--brass-300);
    font-family:var(--font-sans);font-size:15px;font-weight:700;
    letter-spacing:0.3px;cursor:pointer;
    transition:background var(--dur,.18s) ease;
  }
  .pp-card-btn:hover{background:rgba(var(--brand-rgb),0.1)}
  .pp-card-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .pp-card-btn[disabled]{opacity:.6;cursor:default}
  @media(max-width:600px){
    .passport-backdrop{align-items:flex-end;padding:0}
    .passport-tap-wrap{max-width:100%}
    .tap-nozzle,.tap-stream{display:none}
    .passport-card{border-radius:20px 20px 0 0}
    .passport-card.poured{clip-path:inset(0% 0 0 0 round 20px 20px 0 0)}
  }

  /* Panel */
  /* == THE INSET BELONGS TO THE OPEN PANEL, NOT THE CLOSED ONE ==
     `bottom` used to be var(--kb-inset) in both states. The point of the inset
     is to keep an OPEN sheet's footer above the keyboard; a closed panel has
     no footer to protect and no reason to be lifted off the floor. Lifting it
     was what put a strip of it on screen: closed means translateY(100%), which
     moves it down by its own height and by nothing else, so any `bottom` above
     zero is exactly how much of it stays visible.
     Closed sits on the floor and is therefore always fully cleared, whatever
     the inset happens to say. See .crawl-panel.open below for the other half. */
  .crawl-panel{
    position:fixed;top:0;right:0;bottom:0;width:340px;
    background:linear-gradient(160deg,var(--bg),var(--panel),var(--panel-2));
    border-left:1px solid var(--border);
    z-index:var(--z-sheet);
    display:flex;flex-direction:column;
    transform:translateX(100%);
    transition:transform 0.35s cubic-bezier(0.22,1,0.36,1);
    box-shadow:-8px 0 40px rgba(0,0,0,0.6);
  }
  .crawl-panel.open{transform:translateX(0);bottom:var(--kb-inset)}

  .crawl-panel-header{
    background:linear-gradient(90deg,var(--warn),var(--warn) 50%,var(--warn));
    padding:14px 18px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
  }
  .crawl-panel-title{font-size:13px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:var(--on-warn)}
  .crawl-panel-close{width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,0.3);border:none;color:var(--on-warn);font-size:16px;font-weight:900;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s;flex-shrink:0}
  .crawl-panel-close:hover{background:rgba(0,0,0,0.6);color:var(--on-accent)}

  .crawl-panel-body{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:14px}

  /* Search */
  .crawl-search-wrap{position:relative}
  .crawl-search{width:100%;background:rgba(0,0,0,0.35);border:1px solid var(--border);border-radius:10px;color:var(--text);font-family:var(--font-sans);font-size:14px;padding:9px 12px 9px 34px;outline:none;transition:border-color 0.2s}
  .crawl-search:focus{border-color:var(--accent)}
  .crawl-search-icon{position:absolute;left:10px;top:50%;transform:translateY(-50%);font-size:15px;pointer-events:none}
  .crawl-search-results{background:var(--bg);border:1px solid var(--border);border-radius:10px;margin-top:4px;max-height:180px;overflow-y:auto;display:none}
  .crawl-search-results.show{display:block}
  .crawl-result-item{padding:9px 12px;font-size:14px;color:var(--text);cursor:pointer;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(42,63,46,0.5);transition:background 0.15s}
  .crawl-result-item:last-child{border-bottom:none}
  .crawl-result-item:hover{background:rgba(var(--brand-rgb),0.08)}
  .crawl-result-item.added{color:var(--text-2);cursor:default}
  .crawl-add-icon{font-size:16px;color:var(--accent)}

  /* Stop list */
  .crawl-stops-label{font-size:13px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.6px;font-weight:600}

  /* ── THERE IS MORE BELOW ──
     A bottom sheet on a phone has no visible scrollbar, so a crawl with five
     stops hides the name, the date, the times and the plan under the fold with
     nothing on screen to suggest they exist, people set two stops and pressed
     the only button they could see. The chevron is the missing scrollbar: it
     sits on the right of "Your stops", it says how to get to the rest, and
     updateCrawlScrollHint() hides it once there is nothing left down there. */
  .crawl-stops-head{display:flex;align-items:center;justify-content:space-between;gap:10px}
  .crawl-scroll-hint{
    flex-shrink:0;display:inline-flex;align-items:center;gap:5px;
    min-height:32px;padding:0 10px;
    background:var(--brand-ghost);border:1px solid var(--brand-ring);
    border-radius:999px;color:var(--accent);
    font-family:var(--font-sans);font-size:12px;font-weight:700;
    text-transform:uppercase;letter-spacing:.5px;
    cursor:pointer;transition:background var(--dur-fast) ease;
  }
  .crawl-scroll-hint[hidden]{display:none}
  .crawl-scroll-hint:hover{background:var(--brand-ring)}
  /* A nudge, not a beacon: three bounces and it stops. An arrow that animates
     forever becomes part of the furniture and stops being read at all. */
  .crawl-scroll-chev{
    display:inline-block;font-size:11px;line-height:1;
    animation:crawlChev 1.1s ease-in-out 3;
  }
  @keyframes crawlChev{
    0%,100%{transform:translateY(0)}
    50%{transform:translateY(3px)}
  }
  @media (prefers-reduced-motion:reduce){ .crawl-scroll-chev{animation:none} }
  /* How far the crawl is, and the one control that changes it. */
  .crawl-distance{
    display:flex;justify-content:space-between;gap:10px;
    font-size:13px;color:var(--text-2);
    padding:8px 12px;margin-top:6px;
    background:var(--panel-2);border:1px solid var(--border);border-radius:10px;
  }
  .crawl-distance[hidden]{display:none}
  .crawl-optimise-btn{
    width:100%;margin-top:6px;min-height:42px;
    background:var(--brand-ghost);border:1px solid var(--brand-ring);
    border-radius:10px;color:var(--accent);
    font-family:var(--font-sans);font-size:14px;font-weight:600;
    cursor:pointer;transition:all var(--dur-fast) ease;
  }
  .crawl-optimise-btn:hover:not(:disabled){background:var(--brand-ring)}
  .crawl-optimise-btn:disabled{opacity:.45;cursor:default;color:var(--text-3);border-color:var(--border);background:transparent}
  .crawl-stops{display:flex;flex-direction:column;gap:6px}
  .crawl-stop{
    background:rgba(0,0,0,0.35);border:1px solid var(--border);border-radius:12px;
    padding:10px 12px;display:flex;align-items:center;gap:10px;
    cursor:grab;user-select:none;transition:border-color 0.2s,box-shadow 0.2s;
  }
  .crawl-stop:active{cursor:grabbing}
  .crawl-stop.dragging{opacity:0.5;border-color:var(--accent);box-shadow:0 4px 20px rgba(var(--brand-rgb),0.2)}
  .crawl-stop.drag-over{border-color:var(--warn);box-shadow:0 0 0 1px var(--warn)}
  .crawl-stop-num{width:22px;height:22px;border-radius:50%;background:linear-gradient(135deg,var(--warn),var(--warn));display:flex;align-items:center;justify-content:center;font-size:13px;font-weight:700;color:var(--on-warn);flex-shrink:0}
  .crawl-stop-name{flex:1;font-size:14px;font-weight:500;color:var(--text)}
  .crawl-stop-addr{font-size:13px;color:var(--text-2);margin-top:1px}
  .crawl-stop-remove{background:transparent;border:none;color:var(--text-2);font-size:18px;cursor:pointer;padding:0 2px;transition:color 0.15s;line-height:1;flex-shrink:0}
  .crawl-stop-remove:hover{color:var(--danger)}
  .crawl-drag-handle{color:var(--border);font-size:15px;cursor:grab;flex-shrink:0}

  .crawl-empty{text-align:center;padding:24px 16px;color:var(--text-2);font-size:14px}
  .crawl-empty-icon{font-size:32px;margin-bottom:8px}

  /* ── PICK FROM THE MAP ──
     Was a sentence of advice about a map that, on a phone, is entirely behind
     this panel. Now a control that gets the panel out of the way. */
  .crawl-pick-btn{
    width:100%;min-height:44px;
    background:var(--brand-ghost);border:1px solid var(--brand-ring);
    border-radius:10px;color:var(--accent);
    font-family:var(--font-sans);font-size:14px;font-weight:600;
    cursor:pointer;transition:all var(--dur-fast) ease;
  }
  .crawl-pick-btn:hover{background:var(--brand-ring)}

  /* ── NAME IT AND KEEP IT ── */
  .crawl-save[hidden]{display:none}
  .crawl-save-lab{
    display:block;font-size:13px;color:var(--text-2);font-weight:600;
    text-transform:uppercase;letter-spacing:.6px;margin:4px 0 6px;
  }
  .crawl-save-row{display:flex;gap:8px}
  .crawl-save-input{
    flex:1;min-width:0;min-height:44px;
    background:rgba(0,0,0,.35);border:1px solid var(--border);border-radius:10px;
    color:var(--text);font-family:var(--font-sans);
    font-size:16px; /* never below 16px: iOS zooms the viewport on focus */
    padding:9px 12px;outline:none;
  }
  .crawl-save-input:focus{border-color:var(--accent)}
  .crawl-save-btn{
    flex-shrink:0;min-height:44px;padding:0 18px;
    background:var(--accent-solid);border:1px solid var(--accent-solid);
    border-radius:10px;color:var(--on-accent);
    font-family:var(--font-sans);font-size:14px;font-weight:700;
    cursor:pointer;
  }
  .crawl-save-btn:hover:not(:disabled){background:var(--brand-500)}
  /* ── NOTHING TO SAVE ──
     The button reads "Saved" and stops being pressable the moment the crawl on
     screen matches the crawl in storage. It is the panel's only answer to "did
     that go in?", and a button that is always pressable cannot give it: people
     pressed Save twice, got the same toast twice, and still did not know. */
  .crawl-save-btn:disabled{
    background:transparent;border-color:var(--border);color:var(--text-3);
    cursor:default;
  }

  /* ── PUBLIC OR PRIVATE ──
     Two buttons rather than a switch. A switch has one label and a position,
     which means the reader has to work out which way is on, and the cost of
     getting that wrong here is somebody's Saturday night in a public list.
     Both options are written out, both say what they mean, and the one in force
     is the one with the ring. */
  .crawl-vis{display:flex;gap:8px;margin-top:10px}
  .crawl-vis-opt{
    flex:1;min-width:0;display:flex;align-items:flex-start;gap:8px;text-align:left;
    background:var(--panel-2);border:1px solid var(--border);border-radius:10px;
    padding:9px 10px;cursor:pointer;
    font-family:var(--font-sans);color:var(--text-2);
    transition:border-color var(--dur-fast) ease,background var(--dur-fast) ease;
  }
  .crawl-vis-opt:hover{border-color:var(--border-strong)}
  .crawl-vis-ico{font-size:15px;line-height:1.3;flex-shrink:0}
  .crawl-vis-txt{min-width:0}
  .crawl-vis-txt strong{display:block;font-size:13px;font-weight:700;color:var(--text)}
  .crawl-vis-txt small{display:block;font-size:12px;line-height:1.35;margin-top:2px;color:var(--text-3)}
  .crawl-vis-opt.is-on{
    background:var(--brand-ghost);border-color:var(--accent);
    box-shadow:inset 0 0 0 1px var(--accent);
  }
  .crawl-vis-opt.is-on .crawl-vis-txt strong{color:var(--accent)}

  /* ── THE ASK ──
     Set by maybeAskToSaveCrawl() the first time a crawl has both stops and
     times. Until then this box was just there, unhidden the moment there were
     two stops, waiting to be noticed, and it wasn't, so five minutes of work
     died with the tab. It draws its own border for one moment rather than
     interrupting with a dialog: the crawl is still the thing on screen. */
  .crawl-save.is-asking{
    background:var(--brand-ghost);border:1px solid var(--brand-ring);
    border-radius:12px;padding:10px 12px 12px;margin-top:2px;
    animation:crawlSaveAsk var(--dur-slow) ease;
  }
  .crawl-save.is-asking .crawl-save-lab{color:var(--accent);margin-top:0}
  @keyframes crawlSaveAsk{
    from{opacity:.4;transform:translateY(-4px)}
    to{opacity:1;transform:none}
  }
  @media (prefers-reduced-motion:reduce){ .crawl-save.is-asking{animation:none} }

  .crawl-save-hint{
    margin:8px 0 0;font-size:13px;line-height:1.45;color:var(--text-2);
  }
  .crawl-save-hint[hidden]{display:none}

  .crawl-saved[hidden]{display:none}
  .crawl-saved-hint{margin:4px 0 0;font-size:12px;line-height:1.4;color:var(--text-3)}
  /* The way back out of the fold. Quiet, it is an escape hatch, not an offer,
     but a full-width row rather than a link, because everything else you press
     in this panel is one. */
  .crawl-saved-more{
    display:block;width:100%;margin-top:4px;padding:6px 2px;
    background:transparent;border:none;
    color:var(--text-3);font-family:var(--font-sans);
    font-size:12px;text-align:left;cursor:pointer;text-decoration:underline;
    text-underline-offset:2px;
  }
  .crawl-saved-more:hover{color:var(--accent)}
  .crawl-saved-row{display:flex;align-items:stretch;gap:6px;margin-top:6px}
  .crawl-saved-open{
    flex:1;min-width:0;text-align:left;
    background:rgba(0,0,0,.35);border:1px solid var(--border);border-radius:10px;
    padding:9px 12px;cursor:pointer;font-family:var(--font-sans);
    transition:border-color var(--dur-fast) ease;
  }
  .crawl-saved-open:hover{border-color:var(--accent)}

  /* ── THE ONE THAT IS LOADED ──
     The green ring is not decoration: it is the same green as the primary
     button in its Edit state, and the two are the same fact said twice. This
     row is the crawl the builder is holding; that button is what you can do
     with it. Edit a stop or a time and the ring goes, because the builder is no
     longer holding what this row describes, which is also the moment Save
     lights up and the primary button greys. */
  .crawl-saved-row.is-selected .crawl-saved-open{
    border-color:var(--accent);background:var(--brand-ghost);
    box-shadow:inset 0 0 0 1px var(--accent);
  }
  .crawl-saved-row.is-selected .crawl-saved-name{color:var(--accent)}
  /* A tick, so the state does not rest on colour alone. */
  .crawl-saved-row.is-selected .crawl-saved-name::after{
    content:" \2713";font-weight:700;
  }
  .crawl-saved-name{display:block;font-size:14px;font-weight:600;color:var(--text);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .crawl-saved-meta{display:block;font-size:13px;color:var(--text-3);margin-top:2px}
  .crawl-saved-del{
    flex-shrink:0;width:38px;
    background:transparent;border:1px solid var(--border);border-radius:10px;
    color:var(--text-3);font-size:18px;line-height:1;cursor:pointer;
  }
  .crawl-saved-del:hover{border-color:var(--danger);color:var(--danger)}

  /* ── WHEN ──
     Sits in the scrolling body under "Name this crawl", not in the pinned
     footer. It gets a rule above it because it is the last question the panel
     asks and it reads as a section rather than as two more fields. */
  .crawl-when{
    display:flex;flex-direction:column;
    border-top:1px solid var(--border);padding-top:12px;margin-top:2px;
  }
  .crawl-times{display:flex;gap:8px}
  /* The date gets its own full-width row above the two times. Three controls
     across a 340px panel gives each of them about 100px, and a date input at
     100px truncates its own value in every browser that renders one. */
  .crawl-date{display:flex;flex-direction:column;gap:4px;margin-bottom:8px}
  .crawl-time{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
  .crawl-date span,
  .crawl-time span{
    font-size:13px;color:var(--text-2);font-weight:600;
    text-transform:uppercase;letter-spacing:.6px;
  }
  .crawl-date input,
  .crawl-time input{
    width:100%;min-height:44px;
    background:rgba(0,0,0,.35);border:1px solid var(--border);border-radius:10px;
    color:var(--text);font-family:var(--font-sans);
    font-size:16px;padding:9px 10px;outline:none;
    -webkit-appearance:none;appearance:none;
  }
  .crawl-date input:focus,
  .crawl-time input:focus{border-color:var(--accent)}
  /* Safari and Chrome hand the time and date pickers a dark-on-dark glyph. */
  .crawl-date input::-webkit-calendar-picker-indicator,
  .crawl-time input::-webkit-calendar-picker-indicator{filter:invert(.65)}
  :root[data-theme="light"] .crawl-date input::-webkit-calendar-picker-indicator,
  :root[data-theme="light"] .crawl-time input::-webkit-calendar-picker-indicator{filter:none}
  /* iOS renders an empty date input as a blank box with no hint that it is one.
     A minimum height alone is not enough, the field has to look occupied. */
  .crawl-date input::-webkit-date-and-time-value{text-align:left}

  .crawl-plan{
    font-size:13px;line-height:1.45;color:var(--text-2);
    background:var(--panel-2);border:1px solid var(--border);border-radius:10px;
    padding:9px 12px;
  }
  .crawl-plan[hidden]{display:none}
  .crawl-plan strong{color:var(--text)}
  /* The footer was a flex column with a gap; the body is not, so the readout
     spaces itself off the times above it. */
  .crawl-when .crawl-plan{margin-top:8px}

  /* Actions */
  /* The panel now covers the nav rather than stopping short of it, so the last
     button in this stack is the last thing on the screen, it needs the
     home-indicator inset itself or it sits under the gesture bar. */
  .crawl-actions{
    padding:14px 16px max(14px, env(safe-area-inset-bottom));
    border-top:1px solid var(--border);
    display:flex;flex-direction:column;gap:8px;flex-shrink:0;
  }
  /* ── OUT OF THE WAY WHILE TYPING ──
     This row is pinned to the foot of the panel, and on a phone the foot of
     the panel is wherever the keyboard leaves it. Searching for a pub means
     the panel is at its shortest and the search results are at their longest,
     and the footer was landing across both.
     Nothing here can be pressed mid-search, you cannot start a crawl you are
     still assembling, so it stands down and comes back the moment you stop
     typing. */
  body.crawl-typing .crawl-actions{display:none}

  /* ── THE PRIMARY BUTTON, IN THREE STATES ──
     It used to be one button, permanently amber, permanently reading "Start
     Crawl", so the loudest control in the panel offered to begin a Saturday
     night at ten past two on a Tuesday, and it would start a crawl that had
     never been saved anywhere.

     Now the colour is the state, and there are only three:
       grey   nothing to start, no stops, no times, or nothing saved yet
       green  a saved crawl, selected, for later. Pressing it edits.
       amber  it is the crawl's start time. This is the only state that walks.
     Amber is reserved for that last one on purpose. It is the one irreversible
     thing in the panel, and it should not be wearing the same colour as a
     button that means "you have not finished yet".
     The base is the grey: least state, least paint. */
  .crawl-start-btn{
    width:100%;min-height:46px;padding:12px;
    background:transparent;border:1px solid var(--border);border-radius:12px;
    color:var(--text-3);
    font-family:var(--font-sans);font-size:15px;font-weight:700;
    letter-spacing:0.3px;cursor:not-allowed;
    transition:background var(--dur-fast) ease,color var(--dur-fast) ease,
               border-color var(--dur-fast) ease,box-shadow var(--dur-fast) ease;
  }
  /* Green: saved, selected, and not yet time. */
  .crawl-start-btn.is-edit{
    background:var(--brand-ghost);border-color:var(--accent);
    color:var(--accent);cursor:pointer;
  }
  .crawl-start-btn.is-edit:hover{background:var(--brand-ring)}
  /* Amber: go. Solid rather than ghosted, because this is the one control in
     the panel that changes what the whole screen is doing. */
  .crawl-start-btn.is-ready{
    background:var(--warn);border-color:var(--warn);
    color:var(--on-warn);cursor:pointer;
    box-shadow:0 2px 14px var(--warn-soft);
  }
  .crawl-start-btn.is-ready:hover{opacity:.92}
  /* Disabled only ever coincides with the grey state, so it needs no fade of
     its own, a 40% opacity grey on a dark panel was below any contrast floor
     worth having, and the label is the part that has to be readable: it is the
     only place the panel says what is still missing. */
  .crawl-start-btn:disabled{opacity:1}

  /* Moved out of the pinned footer to the end of the body, under the plan it
     would be discarding. Full width still, but no longer sitting under Start. */
  .crawl-clear-btn{width:100%;margin-top:12px;background:transparent;border:1px solid var(--border);border-radius:12px;color:var(--text-2);font-family:var(--font-sans);font-size:14px;padding:9px;cursor:pointer;transition:all 0.2s}
  .crawl-clear-btn:hover{border-color:var(--danger);color:var(--danger)}

  /* ── THE LINK, WRITTEN DOWN ──
     A share sheet is a one-shot: it opens, you pick an app or you dismiss it,
     and either way there is nothing left on screen that says what got sent.
     This is the link, in the panel, short enough to read and with its own Copy,
     so "what did I send them?" and "send it again" both have an answer. */
  .crawl-link{
    display:flex;align-items:center;gap:8px;
    background:var(--panel-2);border:1px solid var(--border);border-radius:10px;
    padding:8px 10px;font-size:13px;
  }
  .crawl-link[hidden]{display:none}
  .crawl-link-ico{flex-shrink:0}
  .crawl-link-url{
    flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    color:var(--accent);font-weight:600;text-decoration:none;
  }
  .crawl-link-url:hover{text-decoration:underline}
  .crawl-link-copy{
    flex-shrink:0;min-height:30px;padding:0 10px;
    background:var(--brand-ghost);border:1px solid var(--brand-ring);
    border-radius:8px;color:var(--accent);
    font-family:var(--font-sans);font-size:12px;font-weight:700;
    cursor:pointer;
  }
  .crawl-link-copy:hover{background:var(--brand-ring)}

  /* Active crawl overlay on map */
  .crawl-active-bar{
    position:fixed;bottom:0;left:0;right:0;
    background:linear-gradient(90deg,var(--bg),var(--panel));
    border-top:1px solid var(--warn);
    padding:12px 16px;
    padding-bottom:max(12px, env(safe-area-inset-bottom));
    z-index:var(--z-bar);
    /* A grid, not a row. Four controls and a stop name did not fit across a
       360px Android: End, Add Info and Next Stop were clipping each other and
       the pub name was truncating to two words. */
    display:grid;grid-template-columns:1fr auto;align-items:center;
    gap:8px 10px;
    box-shadow:0 -4px 20px rgba(0,0,0,0.5);
    transform:translateY(110%);
    transition:transform 0.35s cubic-bezier(0.22,1,0.36,1);
    pointer-events:none;
  }
  .crawl-active-bar.show{transform:translateY(0);pointer-events:all;}
  .crawl-active-info{display:flex;align-items:center;gap:10px;min-width:0}
  .crawl-active-info > div{min-width:0}
  .crawl-active-label{font-size:13px;color:var(--warn);font-weight:600;text-transform:uppercase;letter-spacing:0.5px}
  .crawl-active-stop{
    font-size:var(--t-md);font-weight:700;color:var(--text);
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }
  .crawl-active-progress{font-size:13px;color:var(--text-2)}
  .crawl-active-btns{display:flex;gap:8px;align-items:center;justify-self:end}

  /* ── RALLY ──
     Its own row, full width, and the loudest thing on the bar. It is the
     control an organiser reaches for one-handed, in the dark, with a pint in
     the other hand, and it is the only one on this bar that does something for
     anybody other than the person pressing it. */
  .crawl-rally-btn{
    grid-column:1 / -1;
    width:100%;min-height:48px;
    display:flex;align-items:center;justify-content:center;gap:8px;
    background:linear-gradient(135deg,var(--accent-solid),var(--brand-600));
    border:none;border-radius:12px;color:var(--on-accent);
    font-family:var(--font-sans);
    font-size:var(--t-base);font-weight:700;letter-spacing:.2px;
    padding:10px 14px;cursor:pointer;
    transition:transform var(--dur-fast) ease, filter var(--dur-fast) ease;
  }
  .crawl-rally-btn:hover{filter:brightness(1.08)}
  .crawl-rally-btn:active{transform:scale(.98)}
  .crawl-rally-btn:focus-visible{outline:2px solid var(--text);outline-offset:2px}
  .crawl-rally-btn span{
    overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
  }
  /* On the last stop it is an announcement rather than an instruction, so it
     stops competing with Finish for the eye. */
  .crawl-rally-btn.is-final{
    background:var(--panel-2);
    border:1px solid var(--border-strong);
    color:var(--text);
  }
  @media (prefers-reduced-motion:reduce){
    .crawl-rally-btn{transition:none}
    .crawl-rally-btn:active{transform:none}
  }
  .crawl-next-btn{background:linear-gradient(90deg,var(--warn),var(--warn));border:none;border-radius:10px;color:var(--on-warn);font-family:var(--font-sans);font-size:14px;font-weight:700;padding:11px 16px;cursor:pointer;white-space:nowrap;min-height:44px}
  .crawl-end-btn{background:transparent;border:1px solid var(--border);border-radius:10px;color:var(--text-2);font-family:var(--font-sans);font-size:14px;padding:11px 12px;cursor:pointer;min-height:44px}
  .crawl-end-btn:hover{border-color:var(--danger);color:var(--danger)}
  .crawl-info-btn{background:rgba(var(--brand-rgb),0.15);border:1px solid rgba(var(--brand-rgb),0.4);border-radius:10px;color:var(--accent);font-family:var(--font-sans);font-size:14px;font-weight:600;padding:11px 12px;cursor:pointer;transition:all 0.2s;white-space:nowrap;min-height:44px}
  .crawl-info-btn:hover{background:rgba(var(--brand-rgb),0.25);border-color:var(--accent)}

  /* The group, from the active bar. Quiet, the loud control on this bar is
     Rally, but it carries the one piece of live state on the whole screen, so
     the dot has to be readable at a glance and from an angle. */
  .crawl-group-btn{
    position:relative;
    background:var(--panel-2);border:1px solid var(--border-strong);border-radius:10px;
    color:var(--text);font-family:var(--font-sans);
    font-size:14px;font-weight:600;padding:11px 12px;cursor:pointer;
    white-space:nowrap;min-height:44px;transition:all .2s;
  }
  .crawl-group-btn:hover{border-color:var(--accent);color:var(--accent)}
  .crawl-group-btn[hidden]{display:none}
  .crawl-group-dot{
    position:absolute;top:-5px;right:-5px;min-width:19px;height:19px;padding:0 5px;
    display:flex;align-items:center;justify-content:center;
    background:var(--live);color:#fff;border-radius:999px;
    font-size:11px;font-weight:700;line-height:1;
    box-shadow:0 0 0 2px var(--panel);
  }
  .crawl-group-dot[hidden]{display:none}

  /* The organiser's two buttons in the builder. A row of their own under Share
     and Send in chat, because they are about running the night rather than
     about telling people it exists. */
  .crawl-organise{display:flex;gap:8px}
  .crawl-organise[hidden]{display:none}
  .crawl-organise .crawl-share-btn{flex:1 1 0;min-width:0}

  /* The thread, from the builder. Full width and quietly accented: it is a
     count of real people who have said yes, which is the most encouraging
     thing this panel can tell you and the reason to keep sharing the link. */
  .crawl-group-open{
    width:100%;min-height:44px;
    display:flex;align-items:center;justify-content:center;gap:8px;
    background:var(--brand-ghost);border:1px solid var(--accent);border-radius:10px;
    color:var(--accent);font-family:var(--font-sans);
    font-size:14px;font-weight:700;padding:10px 12px;cursor:pointer;
    transition:background var(--dur-fast) ease;
  }
  .crawl-group-open:hover{background:var(--brand-ring)}
  .crawl-group-open[hidden]{display:none}

  /* ── THE GROUP THREAD ──
     A sheet over the map, not a page: the crawl is still running underneath and
     you are meant to be back out of here in ten seconds. Same geometry as the
     builder so the two feel like the same object seen from different ends. */
  .crawl-group{
    position:fixed;left:0;right:0;bottom:0;
    z-index:calc(var(--z-bar) + 1);
    display:flex;flex-direction:column;
    max-height:min(78vh, 640px);
    background:var(--panel);
    border-top:1px solid var(--border-strong);
    border-radius:18px 18px 0 0;
    box-shadow:0 -10px 40px rgba(0,0,0,.5);
    /* The keyboard: same measurement the builder uses, or the composer sits
       under it the moment anybody types. */
    padding-bottom:max(env(safe-area-inset-bottom), var(--kb-inset, 0px));
  }
  .crawl-group[hidden]{display:none}
  @media (min-width:760px){
    .crawl-group{left:auto;right:16px;width:380px;bottom:16px;border-radius:18px}
  }
  .crawl-group-head{
    display:flex;align-items:flex-start;justify-content:space-between;gap:10px;
    padding:14px 16px 10px;border-bottom:1px solid var(--border);
  }
  .crawl-group-title{font-family:var(--font-display);font-size:18px;font-weight:700;color:var(--text)}
  .crawl-group-sub{font-size:13px;color:var(--text-3);margin-top:2px}
  .crawl-group-close{
    flex-shrink:0;width:32px;height:32px;border-radius:50%;
    background:var(--panel-2);border:1px solid var(--border);
    color:var(--text-2);font-size:18px;line-height:1;cursor:pointer;
  }
  .crawl-group-close:hover{color:var(--text);border-color:var(--border-strong)}

  /* Who is here. A wrapping row of names rather than a list: it is context for
     the thread below it, not a screen of its own. */
  .crawl-group-people{
    display:flex;flex-wrap:wrap;gap:6px;padding:10px 16px;
    border-bottom:1px solid var(--border);
  }
  .crawl-group-people:empty{display:none}
  .crawl-group-person{
    font-size:12px;font-weight:600;padding:4px 9px;border-radius:999px;
    background:var(--panel-2);border:1px solid var(--border);color:var(--text-2);
  }
  .crawl-group-person.is-organiser{
    background:var(--brand-ghost);border-color:var(--accent);color:var(--accent);
  }

  .crawl-group-log{
    flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;
    padding:12px 16px;display:flex;flex-direction:column;gap:8px;
    min-height:120px;
  }
  .crawl-group-empty{font-size:13px;color:var(--text-3);line-height:1.5;margin:auto 0}
  .crawl-group-msg{
    max-width:82%;padding:8px 12px;border-radius:14px;
    font-size:14px;line-height:1.45;word-break:break-word;
  }
  .crawl-group-msg.mine{
    align-self:flex-end;background:var(--accent-solid);color:var(--on-accent);
    border-bottom-right-radius:4px;
  }
  .crawl-group-msg.theirs{
    align-self:flex-start;background:var(--panel-2);border:1px solid var(--border);
    color:var(--text);border-bottom-left-radius:4px;
  }
  .crawl-group-who{display:block;font-size:12px;font-weight:700;color:var(--accent);margin-bottom:1px}
  .crawl-group-body{display:block}
  .crawl-group-time{display:block;font-size:11px;opacity:.65;margin-top:2px;text-align:right}

  /* A rally is not a bubble. It is the organiser saying the group is moving,
     and it reads as an instruction on its own line rather than as one more
     opinion in a busy thread. */
  .crawl-group-notice{
    display:flex;align-items:baseline;gap:8px;
    padding:10px 12px;border-radius:12px;font-size:14px;line-height:1.45;
    background:var(--brand-ghost);border:1px solid var(--accent);color:var(--text);
  }
  .crawl-group-notice.is-reminder{
    background:rgba(var(--warn-rgb),.12);border-color:var(--warn);
  }
  .crawl-group-notice-ico{flex-shrink:0}
  .crawl-group-notice .crawl-group-time{margin-left:auto;text-align:right;white-space:nowrap}

  .crawl-group-compose{
    display:flex;gap:8px;align-items:center;
    padding:10px 16px 14px;border-top:1px solid var(--border);
  }
  .crawl-group-input{
    flex:1;min-width:0;min-height:44px;padding:10px 13px;
    background:var(--bg);color:var(--text);
    border:1px solid var(--border);border-radius:12px;
    font-family:var(--font-sans);
    font-size:16px; /* never below 16px: iOS zooms the viewport on focus */
  }
  .crawl-group-input:focus{outline:none;border-color:var(--accent)}
  .crawl-group-send{
    flex-shrink:0;width:44px;height:44px;border-radius:50%;
    background:var(--accent-solid);border:none;color:var(--on-accent);
    font-size:16px;cursor:pointer;
  }
  .crawl-group-send:hover{filter:brightness(1.08)}

  /* Current crawl stop pulses via icon swap, no CSS needed */

  /* On a phone the three controls get a row of their own and share it evenly.
     Side by side with the stop name they left it about 70px to render "The
     Skeff Late Night Bar" into. */
  @media(max-width:560px){
    .crawl-active-bar{grid-template-columns:1fr}
    .crawl-active-btns{justify-self:stretch}
    .crawl-active-btns > *{flex:1 1 0;min-width:0;padding-inline:8px}
    .crawl-info-btn{
      /* "Add Info" is the least urgent of the three and the first to give up
         its label when there is no room for all of it. */
      overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    }
  }

  @media(max-width:600px){
    .crawl-panel{
      top:auto;right:0;left:0;bottom:0;width:100%;
      /* 85dvh of what is left above the keyboard, not 85vh of a viewport that
         pretends the keyboard is not there. */
      height:min(85dvh, calc(100dvh - var(--kb-inset) - 24px));
      border-left:none;border-top:1px solid var(--border);border-radius:20px 20px 0 0;
      /* Plain 100%, and deliberately no var() in here.

         translateY(calc(100% + var(--kb-inset))) would also clear the screen,
         and it was the first fix written. `bottom` is the better place for the
         inset for two reasons that have nothing to do with arithmetic: the
         resting position of a CLOSED panel then depends on no variable at all,
         so it cannot be wrong however the keyboard is misread; and the panel
         stops re-animating its own resting position every time the keyboard
         opens, because `transform` is the transitioned property here and
         `bottom` is not. The inset only ever moves the panel that is open,
         which is the only one with a footer to keep off the keys. */
      transform:translateY(100%);
    }
    .crawl-panel.open{transform:translateY(0)}
    .crawl-panel-header{border-radius:20px 20px 0 0}
  }

  /* ── ROULETTE ── */
  .roulette-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.88);z-index:var(--z-dialog);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.3s ease}
  .roulette-backdrop.open{opacity:1;pointer-events:all}

  .roulette-card{
    background:linear-gradient(160deg,var(--bg),var(--panel),var(--panel-2));
    border:1px solid var(--border);border-radius:24px;
    padding:36px 32px 32px;width:320px;max-width:92vw;
    text-align:center;position:relative;
    box-shadow:0 40px 100px rgba(0,0,0,0.9),inset 0 1px 0 rgba(var(--brand-rgb),0.1);
  }
  .roulette-close{position:absolute;top:14px;right:16px;background:transparent;border:none;color:var(--text-2);font-size:22px;cursor:pointer;line-height:1;transition:color 0.2s}
  .roulette-close:hover{color:var(--text)}

  .roulette-title{font-family:var(--font-display);font-size:22px;font-weight:700;color:var(--text);margin-bottom:4px}
  .roulette-sub{font-size:13px;color:var(--text-2);margin-bottom:28px}

  /* The pint glass container */
  .roulette-glass-wrap{position:relative;width:100px;height:130px;margin:0 auto 28px;cursor:pointer}
  .roulette-glass-wrap:hover .roulette-glass{filter:drop-shadow(0 0 12px rgba(var(--brand-rgb),0.5))}

  /* SVG pint glass */
  .roulette-glass{width:100px;height:130px;transition:filter 0.2s}
  .roulette-glass.shaking{animation:glass-shake 0.12s ease-in-out}
  .roulette-glass.spinning{animation:glass-spin 0.5s ease-in-out infinite alternate}

  @keyframes glass-shake{
    0%{transform:rotate(0deg) scale(1)}
    15%{transform:rotate(-8deg) scale(1.05)}
    30%{transform:rotate(8deg) scale(1.08)}
    45%{transform:rotate(-10deg) scale(1.06)}
    60%{transform:rotate(10deg) scale(1.1)}
    75%{transform:rotate(-7deg) scale(1.07)}
    90%{transform:rotate(5deg) scale(1.04)}
    100%{transform:rotate(0deg) scale(1)}
  }
  @keyframes glass-spin{
    0%{transform:rotate(-12deg) scale(1.05) translateY(-3px)}
    100%{transform:rotate(12deg) scale(1.1) translateY(3px)}
  }

  /* Foam bubbles that appear during shake */
  .roulette-bubbles{position:absolute;top:10px;left:50%;transform:translateX(-50%);width:60px;height:30px;pointer-events:none;opacity:0;transition:opacity 0.3s}
  .roulette-bubbles.show{opacity:1}
  .bubble{position:absolute;border-radius:50%;background:rgba(255,255,255,0.7);animation:bubble-rise 0.8s ease-out forwards}
  @keyframes bubble-rise{0%{opacity:0.8;transform:translateY(0) scale(1)}100%{opacity:0;transform:translateY(-30px) scale(0.3)}}

  .roulette-tap-hint{font-size:13px;color:var(--text-2);margin-bottom:20px;transition:opacity 0.3s}
  .roulette-tap-hint.hide{opacity:0}

  /* Result area */
  .roulette-result{
    background:rgba(0,0,0,0.35);border:1px solid rgba(var(--brand-rgb),0.15);
    border-radius:16px;padding:16px;margin-bottom:18px;
    min-height:70px;display:flex;flex-direction:column;align-items:center;justify-content:center;
    transition:all 0.3s;
  }
  .roulette-result.reveal{border-color:rgba(var(--brand-rgb),0.4);box-shadow:0 0 20px rgba(var(--brand-rgb),0.1)}
  .roulette-pub-name{font-family:var(--font-display);font-size:20px;font-weight:700;color:var(--accent);margin-bottom:4px;opacity:0;transform:translateY(8px);transition:all 0.5s ease 0.1s}
  .roulette-pub-addr{font-size:13px;color:var(--text-2);opacity:0;transition:all 0.5s ease 0.2s}
  .roulette-pub-name.show,.roulette-pub-addr.show{opacity:1;transform:translateY(0)}
  .roulette-placeholder{font-size:14px;color:var(--border)}

  .roulette-go-btn{
    width:100%;background:linear-gradient(90deg,var(--brand-600),var(--accent));
    border:none;border-radius:12px;color:var(--on-accent);
    font-family:var(--font-sans);font-size:15px;font-weight:700;
    padding:12px;cursor:pointer;transition:all 0.2s;
    opacity:0;pointer-events:none;letter-spacing:0.3px;
  }
  .roulette-go-btn.show{opacity:1;pointer-events:all}
  .roulette-go-btn:hover{box-shadow:0 0 20px rgba(var(--brand-rgb),0.3);transform:translateY(-1px)}

  @media(max-width:600px){
    .roulette-card{padding:28px 20px 24px}
  }
  .filter-active-dot{width:7px;height:7px;border-radius:50%;background:var(--info);position:absolute;top:4px;right:4px;display:none}

  /* Panel */
  .filter-panel{
    position:fixed;top:0;left:0;bottom:0;width:300px;
    background:linear-gradient(160deg,var(--bg),var(--panel),var(--panel-2));
    border-right:1px solid var(--border);
    z-index:var(--z-sheet);display:flex;flex-direction:column;
    transform:translateX(-100%);
    transition:transform 0.35s cubic-bezier(0.22,1,0.36,1);
    box-shadow:8px 0 40px rgba(0,0,0,0.6);
  }
  .filter-panel.open{transform:translateX(0)}

  .filter-panel-header{
    background:linear-gradient(90deg,var(--info),var(--info) 50%,var(--info));
    padding:14px 18px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;
  }
  .filter-panel-title{font-size:13px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:var(--on-info)}
  .filter-panel-close{width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,0.25);border:none;color:var(--on-info);font-size:16px;font-weight:900;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s}
  .filter-panel-close:hover{background:rgba(0,0,0,0.5);color:var(--on-accent)}

  .filter-panel-body{
    flex:1;overflow-y:auto;
    padding:20px 16px max(20px, env(safe-area-inset-bottom));
    display:flex;flex-direction:column;gap:20px;
  }

  /* Tag grid */
  .filter-section-label{font-size:13px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.6px;font-weight:600;margin-bottom:10px}
  .filter-tags{display:flex;flex-wrap:wrap;gap:8px}
  .filter-tag{
    display:flex;align-items:center;gap:6px;
    background:rgba(0,0,0,0.3);border:1px solid var(--border);
    border-radius:20px;padding:7px 13px;
    font-size:14px;font-weight:500;color:var(--text-2);
    cursor:pointer;transition:all 0.2s;user-select:none;
  }
  .filter-tag:hover{border-color:var(--info);color:var(--info)}
  .filter-tag.active{background:rgba(14,116,144,0.25);border-color:var(--info);color:var(--info);box-shadow:0 0 8px rgba(34,211,238,0.15)}
  .filter-tag-emoji{font-size:15px}

  /* Visited filter */
  .filter-visited-row{display:flex;align-items:center;justify-content:space-between}
  .filter-visited-label{font-size:14px;color:var(--text);font-weight:500}
  .filter-visited-sub{font-size:13px;color:var(--text-2);margin-top:2px}
  .fv-toggle-track{width:40px;height:22px;background:var(--border);border-radius:11px;position:relative;transition:background 0.25s;cursor:pointer;flex-shrink:0}
  .fv-toggle-track.on{background:var(--green-dark)}
  .fv-toggle-thumb{width:16px;height:16px;background:var(--on-accent);border-radius:50%;position:absolute;top:3px;left:3px;transition:left 0.25s}
  .fv-toggle-track.on .fv-toggle-thumb{left:21px}

  /* Active filter summary bar on map. It reports state rather than demanding
     anything, so it stacks on top of the nav instead of replacing it. */
  .filter-bar{
    /* Full width and opaque, so at bottom:var(--tabbar-safe) it did not
       overlap the attribution, it replaced it. It steps over it like
       everything else that floats here. */
    position:fixed;bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px));left:0;right:0;
    background:linear-gradient(90deg,var(--info-soft),var(--info));
    border-top:1px solid var(--info);
    padding:8px 16px;z-index:calc(var(--z-tabbar) - 1);
    display:none;align-items:center;justify-content:space-between;gap:10px;
  }
  .filter-bar.show{display:flex}
  .filter-bar-label{font-size:13px;color:var(--info);font-weight:600}
  .filter-bar-tags{display:flex;gap:6px;flex-wrap:wrap;flex:1}
  .filter-bar-tag{font-size:13px;background:rgba(34,211,238,0.15);border:1px solid rgba(34,211,238,0.3);border-radius:10px;padding:2px 8px;color:var(--info)}
  .filter-bar-clear{background:transparent;border:1px solid rgba(34,211,238,0.4);border-radius:8px;color:var(--info);font-family:var(--font-sans);font-size:13px;padding:4px 10px;cursor:pointer;white-space:nowrap;transition:all 0.2s}
  .filter-bar-clear:hover{background:rgba(34,211,238,0.1)}

  /* Tag chips in popup */
  .tag-picker.show{display:flex}
  .tag-option.selected{background:rgba(14,116,144,0.2);border-color:var(--info);color:var(--info)}

  @media(max-width:600px){
    .filter-panel{top:0;left:0;right:0;bottom:auto;width:100%;height:auto;max-height:90vh;border-right:none;border-bottom:1px solid var(--border);border-radius:0 0 20px 20px;transform:translateY(-100%)}
    .filter-panel.open{transform:translateY(0)}
  }

  /* ── PRICE HISTORY CHART ── */
  .chart-backdrop{position:fixed;inset:0;background:rgba(0,0,0,0.82);z-index:var(--z-dialog);display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity 0.25s ease;padding:20px}
  .chart-backdrop.open{opacity:1;pointer-events:all}
  .chart-modal{
    background:linear-gradient(160deg,var(--bg),var(--panel),var(--panel-2));
    border:1px solid var(--border);border-radius:20px;
    width:480px;max-width:96vw;
    box-shadow:0 40px 100px rgba(0,0,0,0.9);
    overflow:hidden;
  }
  .chart-header{background:linear-gradient(90deg,var(--info),var(--info) 50%,var(--info));padding:13px 18px;display:flex;align-items:center;justify-content:space-between}
  .chart-header-title{font-size:13px;font-weight:800;letter-spacing:2px;text-transform:uppercase;color:var(--on-info)}
  .chart-close{width:26px;height:26px;border-radius:50%;background:rgba(0,0,0,0.25);border:none;color:var(--on-info);font-size:16px;font-weight:900;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all 0.2s}
  .chart-close:hover{background:rgba(0,0,0,0.5);color:var(--on-accent)}
  .chart-body{padding:18px}
  .chart-pub-name{font-family:var(--font-display);font-size:18px;font-weight:700;color:var(--text);margin-bottom:4px}
  .chart-sub{font-size:13px;color:var(--text-2);margin-bottom:14px}

  /* Drink filter pills */
  .chart-filters{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px}
  .chart-filter-pill{font-size:13px;font-weight:600;border-radius:16px;padding:4px 11px;cursor:pointer;border:1px solid var(--border);background:rgba(0,0,0,0.3);color:var(--text-2);transition:all 0.2s}
  .chart-filter-pill.active{background:rgba(34,211,238,0.15);border-color:var(--info);color:var(--info)}
  .chart-filter-pill:hover{border-color:var(--info);color:var(--info)}

  /* Canvas wrapper */
  .chart-canvas-wrap{position:relative;height:200px;margin-bottom:10px}
  canvas#price-chart{width:100%!important}

  /* No data state */
  .chart-no-data{text-align:center;padding:40px 20px;color:var(--text-2);font-size:14px}
  .chart-no-data-icon{font-size:32px;margin-bottom:8px}

  /* Chart button in popup */
  .chart-popup-btn{
    width:100%;background:rgba(14,116,144,0.15);
    border:1px solid rgba(34,211,238,0.25);border-radius:9px;
    color:var(--info);font-family:var(--font-sans);
    font-size:13px;font-weight:600;padding:7px;
    cursor:pointer;transition:all 0.2s;margin-top:6px;
  }
  .chart-popup-btn:hover{background:rgba(14,116,144,0.3);border-color:var(--info)}

  /* The hamburger and its drawer were deleted along with the priority+
     toolbar. One swipeable action rail replaced both. */

  /* ── OPENING HOURS ── */
  .opening-hours{background:rgba(0,0,0,0.25);border:1px solid var(--border);border-radius:10px;padding:9px 12px;margin-bottom:10px;font-size:13px}
  .opening-hours-label{font-size:12px;color:var(--text-2);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:4px;font-weight:600}
  .opening-hours-text{color:var(--text);line-height:1.5}
  .opening-hours-status{display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:600;margin-bottom:4px}
  .status-open{color:var(--accent)}.status-closed{color:var(--danger)}
  .status-dot{width:6px;height:6px;border-radius:50%;flex-shrink:0}
  .dot-open{background:var(--accent);box-shadow:0 0 6px rgba(var(--brand-rgb),0.6)}
  .dot-closed{background:var(--danger)}

  /* ── CITY SELECTOR ── */
  .city-selector{display:flex;align-items:center;gap:6px;background:rgba(0,0,0,0.35);border:1px solid var(--border);border-radius:22px;padding:5px 14px;transition:all 0.2s}
  .city-selector:hover{border-color:var(--accent)}
  .city-select{background:transparent;border:none;outline:none;color:var(--text);font-family:var(--font-sans);font-size:16px;font-weight:600;cursor:pointer;max-width:130px}
  .city-select option{background:var(--panel);color:var(--text)}

  /* ── CRAWL SHARING ── */
  .crawl-share-btn{
    width:100%;display:flex;align-items:center;justify-content:center;gap:8px;
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);
    border-radius:12px;color:var(--accent);
    font-family:var(--font-sans);font-size:14px;font-weight:600;
    padding:9px;cursor:pointer;transition:all 0.2s;
  }
  .crawl-share-btn:hover{background:rgba(var(--brand-rgb),0.22);border-color:var(--accent)}
  .crawl-share-btn:disabled{opacity:0.35;cursor:not-allowed}

  /* Shared crawl banner */
  .shared-crawl-banner{
    position:fixed;top:0;left:0;right:0;z-index:var(--z-float);
    background:linear-gradient(90deg,var(--brand-600),var(--accent) 50%,var(--brand-600));
    padding:12px 16px;display:flex;align-items:center;justify-content:space-between;
    gap:12px;box-shadow:0 4px 20px rgba(0,0,0,0.5);
    transform:translateY(-100%);transition:transform 0.4s cubic-bezier(0.22,1,0.36,1);
  }
  .shared-crawl-banner.show{transform:translateY(0)}
  .shared-crawl-banner-info{flex:1;min-width:0}
  .shared-crawl-banner-title{font-size:13px;font-weight:800;color:var(--on-accent);letter-spacing:0.5px;text-transform:uppercase}
  .shared-crawl-banner-stops{font-size:14px;font-weight:700;color:var(--on-accent);margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .shared-crawl-load-btn{
    background:rgba(0,0,0,0.2);border:none;border-radius:10px;
    color:var(--on-accent);font-family:var(--font-sans);
    font-size:13px;font-weight:700;padding:7px 14px;cursor:pointer;
    white-space:nowrap;transition:background 0.2s;flex-shrink:0;
  }
  .shared-crawl-load-btn:hover{background:rgba(0,0,0,0.35)}
  .shared-crawl-dismiss{background:transparent;border:none;color:var(--on-accent);font-size:20px;cursor:pointer;padding:0 2px;flex-shrink:0}
  @keyframes spin{to{transform:rotate(360deg)}}

  /* ── MOBILE PUB SHEET (Bootstrap offcanvas override) ── */
  #pub-offcanvas{
    background:linear-gradient(180deg,var(--bg) 0%,var(--panel) 100%) !important;
    border-top:1px solid var(--border) !important;
    border-radius:20px 20px 0 0 !important;
    max-height:82vh !important;
    padding-bottom:max(12px, env(safe-area-inset-bottom));
    z-index:calc(var(--z-sheet) + 1) !important;
  }
  #pub-offcanvas .offcanvas-header{
    border-bottom:1px solid var(--panel-2);
    padding:6px 18px 12px;
  }
  #pub-offcanvas .offcanvas-title{
    font-family:var(--font-display);
    font-size:20px;font-weight:700;color:var(--text);line-height:1.2;
  }
  /* invert() was unconditional, so this was inverted in light mode too. */
  #pub-offcanvas .btn-close{ opacity:.55 }
  :root[data-theme="dark"] #pub-offcanvas .btn-close{ filter:invert(1) }
  #pub-offcanvas .offcanvas-body{
    padding:14px 18px;color:var(--text-2);
  }
  .sheet-handle{width:40px;height:4px;background:var(--border);border-radius:2px;margin:10px auto 2px}
  .sheet-addr{font-size:13px;color:var(--text-2);margin-top:2px}
  .sheet-status-open{color:var(--accent);font-size:13px;font-weight:600}
  .sheet-status-closed{color:var(--danger);font-size:13px;font-weight:600}
  /* ── THE ACTION ROW ──
     Four secondary actions on one line instead of two stacked pairs. They
     were costing ~120px of the first screenful between the price bar and
     everything that explains the pub, which pushed the hours, the events and
     the community prices below the fold on a phone. None of them is the
     reason anyone opened the sheet, so none of them earns a full-width row. */
  /* Was repeat(4,1fr). Follow makes five, and five fixed columns on a 320px
     screen is five 54px buttons with the labels clipped. auto-fit lets the row
     break to 3+2 down there and stay one row of five everywhere else, which is
     the degradation people already understand from every other wrapping row in
     this app. */
  .sheet-actions{
    display:grid;grid-template-columns:repeat(auto-fit,minmax(62px,1fr));
    gap:8px;margin-bottom:12px;
  }
  .sheet-actions button{
    display:flex;flex-direction:column;align-items:center;justify-content:center;
    gap:3px;padding:8px 2px;min-height:56px;line-height:1.1;
  }
  .sa-ico{font-size:19px;line-height:1}
  .sa-lab{font-size:12px;font-weight:600;letter-spacing:0.01em}
  .sheet-rating-row{
    display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;
  }
  /* Visited button */
  .btn-visited{
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);
    color:var(--accent);border-radius:12px;font-weight:600;font-size:14px;
    padding:10px 8px;width:100%;transition:all 0.2s;
  }
  .btn-visited.active-visited{background:rgba(var(--brand-rgb),0.3);border-color:var(--accent);}
  .btn-visited:hover{background:rgba(var(--brand-rgb),0.22);color:var(--accent);}
  /* Crawl button */
  .btn-crawl{
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);
    color:var(--accent);border-radius:12px;font-weight:600;font-size:14px;
    padding:10px 8px;width:100%;transition:all 0.2s;
  }
  .btn-crawl:hover{background:rgba(var(--brand-rgb),0.22);color:var(--accent);}
  /* ── THE TILE YOU CAME HERE TO PRESS ──
     Only while the map is being used to pick a crawl stop. Four tiles that all
     look like offers is the right row for a sheet somebody opened to read about
     a pub; it is the wrong one when the app already knows what the tap was for.
     Filled rather than outlined, because nothing else in the row is. */
  .btn-crawl.is-target{
    background:var(--accent-solid);border-color:var(--accent-solid);color:var(--on-accent);
    box-shadow:0 0 0 3px var(--brand-ring);
  }
  .btn-crawl.is-target:hover{background:var(--brand-500);color:var(--on-accent);}
  /* Already a stop: still readable, but not an invitation to add it twice. */
  .btn-crawl.is-on-crawl{
    background:rgba(var(--brand-rgb),0.28);border-color:var(--accent);color:var(--accent);
  }
  :root[data-theme="light"] .btn-crawl.is-target{
    background:var(--brand-600);border-color:var(--brand-600);color:var(--on-accent);
  }
  :root[data-theme="light"] .btn-crawl.is-on-crawl{
    background:var(--brand-ghost);border-color:var(--brand-600);color:var(--brand-600);
  }
  /* Favourite button */
  .btn-fave{
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);
    color:var(--accent);border-radius:12px;font-weight:600;font-size:14px;
    padding:10px 8px;width:100%;transition:all 0.2s;
  }
  /* The heart used to swap between an outline emoji and a filled one. It is a
     single drawn icon now, and the set is monoline throughout, so the fill
     BEHIND it is what says which way this is set, the way it already did.
     Visited can change its glyph because a ring and a tick are two shapes;
     a heart only has the one, and inventing a filled exception for it would
     cost the set the property that makes it a set. */
  .btn-fave.active-fave{background:rgba(var(--brand-rgb),0.3);border-color:var(--accent);}

  /* ── FOLLOW ──
     A subscription, not a favourite. It borrows the favourite's shape because
     they sit side by side and a different shape would imply a different kind
     of control, and it borrows brass rather than green because green is the
     price ramp's cheap end and means something else three inches above this. */
  .btn-follow{
    /* Transparent at rest, not brass-tinted. A filled tile beside four others
       reads as already on, and this one is off far more often than it is on,
       the brass is the state, so the state is the only thing that gets it. */
    background:transparent;
    border:1px solid var(--border);
    color:var(--text-2);
    border-radius:10px;cursor:pointer;font-family:var(--font-sans);
    transition:background var(--dur-fast) ease,border-color var(--dur-fast) ease,color var(--dur-fast) ease;
  }
  .btn-follow:hover{background:rgba(var(--warn-rgb),.2);border-color:var(--warn)}
  .btn-follow.active-follow{
    background:var(--warn);border-color:var(--warn);color:var(--on-warn);
  }
  .btn-follow.active-follow:hover{background:var(--warn);border-color:var(--warn)}
  .btn-fave:hover{background:rgba(var(--brand-rgb),0.22);}
  /* Price / Who buttons */
  .btn-sheet-action{
    border-radius:12px;font-size:14px;font-weight:600;padding:10px 8px;
    width:100%;transition:all 0.2s;
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);color:var(--accent);
  }
  .btn-price{
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);color:var(--accent);
  }
  .btn-price:hover{background:rgba(var(--brand-rgb),0.22);}
  .btn-who{
    background:rgba(var(--brand-rgb),0.12);border:1px solid rgba(var(--brand-rgb),0.3);color:var(--accent);
  }
  .btn-who:hover{background:rgba(var(--brand-rgb),0.22);}
  /* Light mode overrides, dark green outline */
  :root[data-theme="light"] .btn-visited,
  :root[data-theme="light"] .btn-crawl,
  :root[data-theme="light"] .btn-fave,
  :root[data-theme="light"] .btn-sheet-action,
  :root[data-theme="light"] .btn-price,
  :root[data-theme="light"] .btn-who{
    background:var(--on-accent);border-color:var(--brand-600);color:var(--brand-600);
  }
  :root[data-theme="light"] .btn-visited:hover,
  :root[data-theme="light"] .btn-crawl:hover,
  :root[data-theme="light"] .btn-fave:hover,
  :root[data-theme="light"] .btn-sheet-action:hover,
  :root[data-theme="light"] .btn-price:hover,
  :root[data-theme="light"] .btn-who:hover{background:var(--brand-ghost);}
  :root[data-theme="light"] .btn-visited.active-visited,
  :root[data-theme="light"] .btn-fave.active-fave{background:var(--brand-600);color:var(--on-accent);border-color:var(--brand-600);}
  /* Hours pill */
  .sheet-hours{
    background:rgba(0,0,0,0.3);border:1px solid var(--panel-2);
    border-radius:10px;padding:10px 12px;font-size:13px;color:var(--text-2);
    margin-bottom:4px;
  }
  /* Stars */
  .sheet-stars span{font-size:24px;cursor:pointer;line-height:1;color:var(--warn);}
  /* Section label */
  .sheet-section-label{
    font-size:13px;color:var(--text-2);font-weight:600;
    text-transform:uppercase;letter-spacing:0.6px;margin-bottom:6px;display:block;
  }
  /* Divider */
  .sheet-divider{border-color:var(--panel-2);margin:12px 0;}

  /* ── LOCATE ME BUTTON ── */
  /* ── MAP BUTTON ROW (suggest + locate) ── */
  .map-btn-row{
    position:fixed;
    bottom:24px;
    right:12px;
    z-index:var(--z-map);
    display:flex;
    flex-direction:row;
    align-items:center;
    gap:8px;
    pointer-events:all;
  }
  .locate-btn{
    width:40px;height:40px;border-radius:50%;
    background:var(--panel);border:1px solid var(--border);
    color:var(--green);font-size:18px;cursor:pointer;
    display:flex;align-items:center;justify-content:center;
    box-shadow:0 2px 12px rgba(0,0,0,0.25);
    transition:all 0.2s;flex-shrink:0;
  }
  .locate-btn:hover{background:var(--panel2);border-color:var(--green);box-shadow:0 0 12px rgba(var(--brand-rgb),0.3)}
  .locate-btn:active{transform:scale(0.92)}
  .locate-btn.locating{animation:spin 1s linear infinite;pointer-events:none;opacity:0.7}

  /* ── Z-INDEX FIXES ── */
  .offcanvas-backdrop{z-index:var(--z-sheet) !important}
  .offcanvas{z-index:calc(var(--z-sheet) + 1) !important}

  /* ── SUGGEST A PUB ── */
  .suggest-backdrop{
    position:fixed;inset:0;background:rgba(0,0,0,0.85);
    z-index:var(--z-dialog);display:flex;align-items:center;justify-content:center;
    opacity:0;pointer-events:none;transition:opacity 0.3s;padding:20px;
  }
  .suggest-backdrop.open{opacity:1;pointer-events:all}
  .suggest-panel{
    background:linear-gradient(180deg,var(--bg) 0%,var(--panel) 100%);
    border:1px solid var(--border);border-radius:20px;
    width:100%;max-width:500px;max-height:90vh;overflow-y:auto;
    padding:28px;position:relative;
    box-shadow:0 24px 80px rgba(0,0,0,0.7);
  }
  .suggest-close{
    position:absolute;top:16px;right:16px;background:rgba(255,255,255,0.06);
    border:none;color:var(--text-2);font-size:18px;width:32px;height:32px;
    border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;
    transition:background 0.2s;
  }
  .suggest-close:hover{background:rgba(255,255,255,0.12);color:var(--text)}
  .suggest-title{font-family:var(--font-display);font-size:24px;font-weight:700;color:var(--text);margin:0 0 4px}
  .suggest-subtitle{font-size:14px;color:var(--text-2);margin:0 0 24px}
  /* Pin drop zone */
  .suggest-map-wrap{
    border-radius:14px;overflow:hidden;border:1px solid var(--border);
    height:200px;margin-bottom:8px;position:relative;cursor:crosshair;
  }
  .suggest-map-hint{
    position:absolute;top:10px;left:50%;transform:translateX(-50%);
    background:rgba(12,21,16,0.85);border:1px solid var(--border);
    color:var(--text-2);font-size:13px;padding:5px 12px;border-radius:20px;
    pointer-events:none;z-index:10;white-space:nowrap;
    transition:opacity 0.3s;
  }
  .suggest-coords{
    font-size:13px;color:var(--text-3);text-align:center;margin-bottom:16px;
    min-height:16px;
  }
  /* Form fields */
  .suggest-field{margin-bottom:16px}
  .suggest-label{
    display:block;font-size:13px;color:var(--text-2);font-weight:600;
    text-transform:uppercase;letter-spacing:0.6px;margin-bottom:6px;
  }
  /* --panel-2, not rgba(0,0,0,.3). The literal was written for the dark theme,
     where a 30% black wash over a dark panel is a slightly recessed field. In
     the light theme the same wash over white is a mid-grey block with
     near-black text on it, every field in every one of these panels, sitting
     there looking like a disabled control or a stuck selection highlight.

     The token is the same recessed grey in dark and the right one in light,
     which is the whole reason the palette has tokens. Same for the focus
     state, which was a heavier wash and went the same way. */
  .suggest-input{
    width:100%;background:var(--panel-2);border:1px solid var(--border);
    border-radius:10px;padding:11px 14px;color:var(--text);
    font-family:var(--font-sans);font-size:15px;
    outline:none;transition:border-color 0.2s, box-shadow 0.2s;box-sizing:border-box;
  }
  .suggest-input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--brand-ring)}
  .suggest-input::placeholder{color:var(--text-3)}
  .suggest-input option{background:var(--bg);color:var(--text)}
  textarea.suggest-input{resize:vertical;min-height:72px}
  .suggest-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
  .suggest-btn{
    width:100%;background:linear-gradient(135deg,var(--accent-solid),var(--brand-600));
    color:var(--on-accent);border:none;border-radius:12px;padding:13px;
    font-family:var(--font-sans);font-size:15px;font-weight:700;
    cursor:pointer;transition:opacity 0.2s;margin-top:4px;letter-spacing:0.3px;
  }
  .suggest-btn:hover{opacity:0.88}
  .suggest-btn:disabled{opacity:0.45;cursor:not-allowed}
  .suggest-success-wrap{text-align:center;padding:32px 0}
  .suggest-success-icon{font-size:48px;margin-bottom:12px}
  .suggest-success-title{font-family:var(--font-display);font-size:22px;color:var(--text);margin:0 0 8px}
  .suggest-success-msg{font-size:15px;color:var(--text-2)}
  /* Floating suggest button on map */
  .suggest-map-btn{
    background:var(--panel);border:1px solid var(--border);
    color:var(--green);font-size:13px;font-weight:600;
    padding:8px 14px;border-radius:20px;cursor:pointer;
    font-family:var(--font-sans);
    box-shadow:0 2px 12px rgba(0,0,0,0.25);
    display:flex;align-items:center;gap:6px;
    transition:all 0.2s;white-space:nowrap;height:40px;box-sizing:border-box;
  }
  .suggest-map-btn:hover{background:var(--panel2);border-color:var(--green);box-shadow:0 0 12px rgba(var(--brand-rgb),0.3)}

  /* ── FRIENDS PANEL ── */
  .friends-panel{
    position:fixed;inset:0;z-index:var(--z-sheet);display:flex;align-items:flex-end;
    background:rgba(0,0,0,0.55);backdrop-filter:blur(4px);
  }
  .friends-panel-inner{
    background:var(--panel);border:1px solid var(--border);
    border-radius:20px 20px 0 0;width:100%;max-width:500px;
    margin:0 auto;padding:20px 20px 32px;
    max-height:85vh;overflow-y:auto;
    box-shadow:0 -8px 40px rgba(0,0,0,0.5);
  }
  .friends-panel-title{
    font-family:var(--font-display);
    font-size:18px;font-weight:700;color:var(--text);
    margin-bottom:16px;display:flex;justify-content:space-between;align-items:center;
  }
  .friends-add-row{display:flex;gap:8px;margin-bottom:18px}
  .friends-add-row input{
    flex:1;background:var(--panel2);border:1px solid var(--border);
    color:var(--text);border-radius:10px;padding:9px 12px;font-size:15px;
    font-family:var(--font-sans);outline:none;
  }
  .friends-add-row input:focus{border-color:var(--green)}
  .friends-add-btn{
    background:var(--green-dark);border:none;color:var(--on-accent);
    border-radius:10px;padding:9px 16px;font-weight:600;font-size:15px;
    cursor:pointer;white-space:nowrap;transition:opacity 0.2s;
  }
  .friends-add-btn:hover{opacity:0.85}
  .friend-row{
    display:flex;align-items:center;gap:12px;
    padding:10px 0;border-bottom:1px solid var(--border);
  }
  .friend-avatar{
    width:36px;height:36px;border-radius:50%;
    background:var(--green-dark);display:flex;align-items:center;
    justify-content:center;font-size:15px;font-weight:700;color:var(--on-accent);
    flex-shrink:0;overflow:hidden;
  }
  .friend-avatar img{width:100%;height:100%;object-fit:cover;border-radius:50%}
  .friend-info{flex:1}
  .friend-name{font-size:15px;font-weight:600;color:var(--text)}
  .friend-stats{font-size:13px;color:var(--muted)}
  .friend-remove{
    background:none;border:1px solid var(--border);border-radius:8px;
    color:var(--muted);font-size:13px;padding:4px 8px;cursor:pointer;
  }
  .friend-remove:hover{color:var(--danger);border-color:var(--danger)}
  .friends-empty{text-align:center;color:var(--muted);font-size:15px;padding:20px 0}
  .friends-find-lab{
    display:block;margin-bottom:6px;
    font-size:13px;font-weight:600;color:var(--text-2);
    text-transform:uppercase;letter-spacing:.6px;
  }
  .friends-find-hint{
    margin:8px 0 14px;font-size:13px;line-height:1.5;color:var(--text-3);
  }

  /* ══════════════════════════════════════════════════════════════════════
     NUDGES: consent, install, and anything else that interrupts
     ══════════════════════════════════════════════════════════════════════

     All three of these used to be full-bleed bars pinned to bottom:0. The
     cookie bar sat at z-index 9500, which is exactly the tab bar's, so it
     lost the tie on source order and rendered *underneath* the navigation,
     with its own Accept button unreachable. The two install bars sat at 9600
     and won the same fight the other way, covering the navigation instead.
     Neither is a layering accident that can be fixed by picking a third
     number: a full-width bar and a full-width nav both want the same edge.

     So they are cards now. They float above the tab bar rather than fighting
     it, they are inset from the screen edges so it is visible that something
     is on top of the app rather than part of it, and every one of them has a
     close button in the same corner. ══════════════════════════════════════ */
  .nudge{
    position:fixed;left:12px;right:12px;
    bottom:calc(var(--tab-h) + env(safe-area-inset-bottom) + 12px);
    z-index:var(--z-float);
    max-width:440px;margin-inline:auto;
    background:var(--panel);
    border:1px solid var(--border-strong);
    border-radius:var(--r-lg);
    box-shadow:var(--shadow-3);
    padding:15px 52px 15px 16px;
    transform:translateY(14px);opacity:0;pointer-events:none;
    transition:transform var(--dur) var(--ease-out), opacity var(--dur) ease;
  }
  .nudge.show{transform:none;opacity:1;pointer-events:auto}
  /* The banner offering to install the app shows the icon that install
   puts on the home screen, rather than a generic pint. It is the one
   place where the picture IS the thing being offered. */
.nudge-mark{width:22px;height:22px;border-radius:6px;vertical-align:-5px;flex-shrink:0}
.nudge-title{
    font-family:var(--font-sans);
    font-size:var(--t-md);font-weight:700;color:var(--text);
    display:flex;align-items:center;gap:8px;margin-bottom:3px;
  }
  .nudge-text{font-size:var(--t-base);color:var(--text-2);line-height:1.5}
  .nudge-text a{color:var(--accent)}
  /* Always the same size, always the same corner, on every nudge. */
  .nudge-close{
    position:absolute;top:9px;right:9px;
    width:36px;height:36px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:var(--panel-2);border:1px solid var(--border);
    color:var(--text-2);font-size:17px;line-height:1;cursor:pointer;
    transition:background var(--dur-fast) ease,color var(--dur-fast) ease;
  }
  .nudge-close:hover{background:var(--hover);color:var(--text)}
  .nudge-close:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .nudge-btns{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}
  .nudge-steps{
    background:var(--panel-2);border:1px solid var(--border);
    border-radius:var(--r-sm);padding:11px 13px;margin-top:11px;
    font-size:var(--t-base);color:var(--text-2);line-height:1.6;
  }

  .cookie-accept{
    background:linear-gradient(135deg,var(--accent-solid),var(--brand-600));
    color:var(--on-accent);border:none;border-radius:10px;
    padding:9px 18px;min-height:40px;font-family:var(--font-sans);
    font-size:15px;font-weight:700;cursor:pointer;
  }
  .cookie-decline{
    background:transparent;border:1px solid var(--border);
    color:var(--text-2);border-radius:10px;
    padding:9px 14px;min-height:40px;font-family:var(--font-sans);
    font-size:15px;cursor:pointer;
  }
  .cookie-decline:hover{border-color:var(--border-strong);color:var(--text)}

  /* Hide overlapping elements during crawl */
  .crawl-active .map-btn-row{opacity:0.3;pointer-events:none}
  .crawl-active .suggest-map-btn{pointer-events:none;opacity:0}

  /* Footer links.

     Three things wanted the same strip at the foot of the map and all three
     were drawing in it: this, the button row, and the attribution. On a phone
     these links landed UNDER the button row, which repeats one of them, the
     pill directly on top of the words "Suggest a pub" saying "Suggest a pub",
     and across the attribution, in var(--border) grey. What that looked like
     was smudged text behind a button.

     So it clears the attribution too, and on a phone it stands down entirely:
     there is no room for a third row of chrome over the map, Privacy and Terms
     are in the launcher's footer one tap away on Home, and the button it was
     hiding behind is the same suggestion form. */
  .app-footer{
    /* Left, not centred. The button row is anchored right:12px, so a centred
       footer runs into it as soon as the window is narrow enough for the two
       to meet, measured at 620px, where "Suggest a pub" crosses the links.
       Anchored to the opposite corner they cannot collide at any width. */
    position:fixed;bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px) + 4px);left:12px;
    z-index:var(--z-map);display:flex;gap:14px;
    /* --text-3, not --border. These are the privacy policy and the terms, and
       they were painted in the colour this app draws hairlines in: #333333 on
       a dark basemap, #E2E2E2 on a light one, which is about 1.5:1 against the
       tiles either way. Not quiet, absent. --text-3 is the colour the
       attribution beside them already uses and clears 6:1 on both themes.

       And the same glass behind it, for the same reason the attribution has
       it: a map is not a background, it is a photograph of streets, and small
       text over one is legible in some places and not others. */
    font-size:12px;color:var(--text-3);
    background:var(--glass);
    -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);
    border-radius:var(--r-sm);padding:2px 8px;
    pointer-events:all;
    transition:opacity 0.2s;
  }
  .crawl-active .app-footer{opacity:0;pointer-events:none}
  .app-footer a{color:var(--text-3);text-decoration:none;transition:color 0.2s}
  .app-footer a:hover{color:var(--text)}

  /* ── OVERALL PASSPORT ── */
  .overall-passport-backdrop{
    position:fixed;inset:0;background:rgba(0,0,0,0.85);
    z-index:var(--z-dialog);display:flex;align-items:center;justify-content:center;
    opacity:0;pointer-events:none;transition:opacity 0.3s;padding:20px;
    overflow-y:auto;-webkit-overflow-scrolling:touch;
  }
  .overall-passport-backdrop.open{opacity:1;pointer-events:all}
  /* This panel is the tallest thing in the app, around 1070px of content, more
     as you visit more cities. It fits unscrolled on exactly one device we test
     (an iPad in portrait). Everywhere else it used to overflow and the BACKDROP
     scrolled, which worked but dragged the header and its × off the top of the
     screen along with everything else.
     Cap it to the viewport and scroll the body instead, so the title and the
     close button stay where you left them. */
  .overall-passport-panel{
    background:linear-gradient(180deg,var(--bg) 0%,var(--panel) 100%);
    border:1px solid var(--border);border-radius:20px;
    width:100%;max-width:500px;
    padding:0;position:relative;
    box-shadow:0 24px 80px rgba(0,0,0,0.7);
    margin:auto;
    max-height:calc(100dvh - 40px);
    display:flex;flex-direction:column;
  }
  .overall-passport-header{flex:0 0 auto}
  .overall-passport-body{
    overflow-y:auto;-webkit-overflow-scrolling:touch;
    overscroll-behavior:contain;min-height:0;
  }
  .overall-passport-header{
    background:linear-gradient(135deg,var(--panel-2),var(--border));
    border-radius:20px 20px 0 0;padding:24px 24px 20px;
    border-bottom:1px solid var(--border);
    display:flex;align-items:center;justify-content:space-between;
  }
  .overall-passport-title{
    font-family:var(--font-display);
    font-size:22px;font-weight:700;color:var(--text);
  }
  .overall-passport-close{
    background:rgba(255,255,255,0.07);border:none;color:var(--text-2);
    font-size:20px;width:34px;height:34px;border-radius:50%;
    cursor:pointer;display:flex;align-items:center;justify-content:center;
  }
  .overall-passport-body{padding:20px 24px 28px}
  /* The sub-line under a panel title. The All-Ireland panel wrote its own as
     an inline style; the badges panel needs the same one, and two copies of a
     colour is how they stop matching. */
  .overall-passport-sub{
    font-size:13px;color:var(--text-3);margin-top:2px;
  }
  .pp-badge-note{
    font-size:13px;color:var(--text-3);line-height:1.5;
    margin:16px 0 0;text-align:center;
  }
  /* The two ways further in, Badges and Every town. Both were chips in the
     banner, written inline, 25px tall and one of them clipped. */
  .pp-more-row{
    display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:16px;
  }
  .pp-more-btn{
    display:flex;align-items:center;justify-content:center;gap:6px;
    min-height:48px;padding:10px 8px;
    /* Filled rather than outlined. These are the two ways further into the
       passport and they sat under it as two faint outlines, the quietest
       things on a card whose every other row is a number in accent green. */
    background:rgba(var(--brand-rgb),0.16);
    border:1px solid var(--accent);
    color:var(--accent);border-radius:12px;
    font-family:var(--font-sans);font-size:14px;font-weight:700;
    cursor:pointer;white-space:nowrap;
    transition:background var(--dur-fast) ease,color var(--dur-fast) ease;
  }
  .pp-more-btn:hover{background:var(--accent);color:var(--on-accent)}
  .pp-more-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .pp-more-ico{font-size:17px;line-height:1}
  .pp-more-label{letter-spacing:.1px}
  /* The count is the reason to press it. "Badges" is a place; "Badges 3/10"
     is a thing you are three-tenths of the way through, which is the whole
     mechanic, and it means neither button needs a longer label to say what
     is behind it. */
  .pp-more-count{
    font-family:var(--font-num);font-size:12px;font-weight:700;
    padding:2px 6px;border-radius:6px;
    background:rgba(0,0,0,.28);
  }
  .pp-more-count:empty{display:none}
  .pp-more-btn:hover .pp-more-count{background:rgba(0,0,0,.22)}

  .op-total-bar{
    background:rgba(0,0,0,0.3);border:1px solid var(--panel-2);
    border-radius:14px;padding:16px;margin-bottom:16px;text-align:center;
  }
  .op-total-num{
    font-family:var(--font-num);
    font-size:48px;font-weight:700;color:var(--accent);line-height:1;
  }
  .op-total-label{font-size:14px;color:var(--text-2);margin-top:4px}
  .op-city-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:16px}
  .op-city-card{
    background:rgba(0,0,0,0.25);border:1px solid var(--panel-2);
    border-radius:12px;padding:12px 14px;
    display:flex;align-items:center;gap:10px;
  }
  .op-city-card.has-visits{border-color:rgba(var(--brand-rgb),0.3);background:rgba(var(--brand-rgb),0.05)}
  .op-city-emoji{font-size:20px;flex-shrink:0}
  .op-city-info{flex:1;min-width:0}
  .op-city-name{font-size:14px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .op-city-count{font-size:13px;color:var(--text-2);margin-top:2px}
  .op-city-bar{height:3px;background:var(--panel-2);border-radius:2px;margin-top:6px;overflow:hidden}
  .op-city-bar-fill{height:100%;background:linear-gradient(90deg,var(--accent-solid),var(--accent));border-radius:2px;transition:width 0.6s ease}
  .op-overall-progress{margin-top:4px}
  .op-progress-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}
  .op-progress-label{font-size:13px;color:var(--text-2);font-weight:600;text-transform:uppercase;letter-spacing:0.5px}
  .op-progress-pct{font-size:14px;color:var(--accent);font-weight:700}
  .op-progress-track{height:8px;background:var(--panel-2);border-radius:4px;overflow:hidden}
  .op-progress-fill{height:100%;background:linear-gradient(90deg,var(--accent-solid),var(--accent));border-radius:4px;transition:width 0.6s ease}

  /* ── TOUCH DRINK UI ── */
  .touch-drink-ui{padding:4px 0}
  .touch-drink-logged{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--panel-2);font-size:14px;color:var(--text)}
  .touch-drink-logged:last-of-type{border-bottom:none}
  .touch-drink-name{font-weight:600;color:var(--accent)}
  .touch-drink-price{color:var(--text-2)}
  .touch-drink-del{background:transparent;border:none;font-size:16px;cursor:pointer;padding:4px 8px;opacity:0.6}
  /* The add-a-drink form that used to sit here is gone. The price bar at the
     top of the sheet is the one place a price is entered, .touch-add-section,
     .touch-drink-select, .touch-price-row, .touch-price-input and
     .touch-confirm-btn went with it. */

  /* ── PWA SAFE AREA FIX ── */
  @supports (padding: env(safe-area-inset-bottom)) {
    body {
      padding-top: env(safe-area-inset-top);
      padding-bottom: env(safe-area-inset-bottom);
      padding-left: env(safe-area-inset-left);
      padding-right: env(safe-area-inset-right);
    }
  }
  /* ── RESPONSIVE / MOBILE ── */

  /* On mobile the popup should slide up from the bottom like a sheet */
  @media (max-width: 600px) {

    /* ── MOBILE HEADER ── */
    header{padding:8px 12px;flex-wrap:nowrap;gap:10px;align-items:center}
    .logo svg{width:28px;height:28px}
    .logo-mark{width:30px;height:30px;border-radius:8px}

    /* Sign-in used to be hidden on phones and reachable only through the
       hamburger. With the hamburger gone it stays in the header, where it is
       the only thing a logged-out visitor needs from this row. */
    .header-right{gap:8px;align-items:center;flex-shrink:0}
    .counter{display:none!important}

    /* Map fills remaining height */
    /* touch-action:none, not `pan-x pan-y pinch-zoom`. Leaflet's own
       stylesheet sets none on the container for exactly this reason, and
       an id selector was quietly overriding it: `pan-x pan-y pinch-zoom`
       hands the gesture to the BROWSER, which then pans and zooms the page
       instead of letting the map have it. On a phone that is a pinch that
       fights the map and a drag that arrives late, on the one platform
       where the map is nothing but touch. */
    #map{touch-action:none}
    /* Prevent iOS/Android zoom on input focus */
    input, select, textarea { font-size:16px !important; }

    /* Map fills remaining height */
    /* touch-action:none, not `pan-x pan-y pinch-zoom`. Leaflet's own
       stylesheet sets none on the container for exactly this reason, and
       an id selector was quietly overriding it: `pan-x pan-y pinch-zoom`
       hands the gesture to the BROWSER, which then pans and zooms the page
       instead of letting the map have it. On a phone that is a pinch that
       fights the map and a drag that arrives late, on the one platform
       where the map is nothing but touch. */
    #map{touch-action:none}
    /* Prevent iOS zoom on input focus */
    input, select, textarea {
      font-size:16px !important;
    }

    /* The only Leaflet popup left is the one on an event marker, and scoping
       these to it is the point. They were written for a pub popup that has
       since been deleted, buildPopup() was never called from anywhere, so
       unscoped they were turning a two-line event card into a full-width
       bottom sheet with 72vh of empty space under it. */
    /* It sits above the nav rather than under it. It cannot cover it: Leaflet
       gives .leaflet-popup-pane a z-index of its own, which makes it a
       stacking context, so nothing inside a popup can ever outrank something
       outside the map no matter what z-index it is given. Moving it up is the
       only fix that works. */
    .ev-popup{
      position:fixed!important;
      bottom:var(--tabbar-safe)!important;left:0!important;right:0!important;top:auto!important;
      width:100%!important;transform:none!important;margin:0!important;
    }
    .ev-popup .leaflet-popup-content-wrapper{
      border-radius:18px 18px 0 0!important;
      width:100%!important;min-width:unset!important;
      max-height:72vh;overflow-y:auto;
      -webkit-overflow-scrolling:touch;overscroll-behavior:contain;
    }
    .ev-popup .leaflet-popup-content{width:100%!important;max-width:100%!important}
    .ev-popup .leaflet-popup-tip-container{display:none!important}

    /* Auth modal fills screen on mobile */
    .auth-backdrop{align-items:flex-start;padding-top:16px}
    .modal{
      width:100%;
      max-width:100%;
      border-radius:18px 18px 0 0;
      padding:24px 20px 32px;
      max-height:92vh;
      overflow-y:auto;
    }
    .modal-field input{font-size:16px;padding:11px 12px} /* prevents iOS zoom */
    .modal-submit{padding:13px;font-size:15px}
  }

  /* ── TABLET ──
     An iPad in portrait is 768 or 834 CSS px: too wide to be treated as a
     phone, too narrow to hold a 340px side panel and still show the map the
     panel is about. Panels get proportional widths rather than the fixed ones
     a laptop can afford. */
  @media (min-width:601px) and (max-width:900px) {
    .logo p{display:none}
    .leaflet-popup-content-wrapper{min-width:270px}
    .crawl-panel{width:min(400px, 52vw)}
    .filter-panel{width:min(360px, 46vw)}
    .cheapest-panel{width:min(440px, 54vw)}
  }

  /* ── LAPTOP AND UP ──
     Room to stop stretching things. A 480px pub sheet on a 1512px MacBook is
     a column of text with 500px of dead map either side of it; a modal that
     sits in the middle of the screen at a comfortable measure reads better
     than one pinned to the bottom edge of a very wide window. */
  @media (min-width:1100px) {
    .crawl-panel{width:380px}
    .cheapest-panel{width:min(460px, 34vw)}
  }

  /* The pub sheet's own responsive rules used to live here, and a second copy
     lived a few lines below. Both are gone: the whole story is now told once,
     in the PUB SHEET section further down. See the note there for why. */

  /* Make sure touch scroll works inside popup */
  .leaflet-popup-content-wrapper{-webkit-overflow-scrolling:touch}

/* ══════════════════════════════════════════════════════════════
   PRICE-LED MAP PINS
   The pin is the price. Everything else on a marker is secondary.
   ══════════════════════════════════════════════════════════════ */
.pin-wrap{background:none!important;border:none!important}

/* The tap target. It is bigger than the bead it holds, so the bead is centred
   inside rather than pinned to a corner, and it carries no paint of its own,
   what a person sees is unchanged, what a thumb hits is 44px. */
.pin-hit{
  width:100%;height:100%;
  display:flex;align-items:center;justify-content:center;
  background:none;border:0;padding:0;
  -webkit-tap-highlight-color:transparent;
}
.pin-hit:focus-visible{
  outline:3px solid var(--accent-solid);
  outline-offset:2px;
  border-radius:50%;
}

.pin{
  position:relative;display:flex;align-items:center;justify-content:center;gap:3px;
  font-family:var(--font-sans);font-weight:700;
  border-radius:7px;white-space:nowrap;line-height:1;
  box-shadow:0 2px 6px rgba(0,0,0,.4),0 0 0 .5px rgba(0,0,0,.25);
  cursor:pointer;
}
/* The transition exists for the hover lift below it, and there is no hover on
   a touch screen. Declaring it unconditionally left several hundred pins on a
   phone carrying a transition nothing would ever start, and every one of them
   is a property the compositor has to keep watching. */
@media (hover:hover){
  .pin{transition:transform .14s ease,box-shadow .14s ease}
}
.pin-priced.pin-cheap{background:var(--price-cheap);color:var(--price-cheap-ink);border:1.5px solid var(--price-cheap-edge)}
.pin-priced.pin-mid  {background:var(--price-mid);  color:var(--price-mid-ink);  border:1.5px solid var(--price-mid-edge)}
.pin-priced.pin-dear {background:var(--price-dear); color:var(--price-dear-ink); border:1.5px solid var(--price-dear-edge)}
/* Priced but the city has too few prices to band yet */
.pin-priced:not(.pin-cheap):not(.pin-mid):not(.pin-dear){
  background:var(--price-mid);color:var(--price-mid-ink);border:1.5px solid var(--price-mid-edge)
}
.pin-priced:hover{transform:translateY(-2px) scale(1.06);box-shadow:0 6px 14px rgba(0,0,0,.5);z-index:600}

/* ── AN ESTIMATED FIGURE ──
   Roughly what a pint costs in this part of this city, drawn only where nobody
   has logged a real one. See the guide-price block in shared.js.

   Styled exactly like a reported price, same fill, same band colours, same
   weight, because a dashed hollow tag reads as a placeholder and makes a
   finished map look unfinished. The band comes from computeGuideBands(),
   which is terciles of the estimates in this town rather than of the real
   prices, so the two ladders never mix.

   The pin carries no marker of its own. The disclosure lives one tap in, on
   the pub sheet, which is where somebody actually decides to go somewhere,
   the map is an overview, and the cheapest-pint list (the one surface that
   turns a number into a recommendation) never sees these at all. */
.pin-guide{
  background:var(--price-mid);
  color:var(--price-mid-ink);
  border:1.5px solid var(--price-mid-edge);
}
.pin-guide.pin-cheap{background:var(--price-cheap);color:var(--price-cheap-ink);border-color:var(--price-cheap-edge)}
.pin-guide.pin-mid  {background:var(--price-mid);  color:var(--price-mid-ink);  border-color:var(--price-mid-edge)}
.pin-guide.pin-dear {background:var(--price-dear); color:var(--price-dear-ink); border-color:var(--price-dear-edge)}
.pin-guide:hover{transform:translateY(-2px) scale(1.06);box-shadow:0 6px 14px rgba(0,0,0,.5);z-index:600}

/* ── A PRICE NOBODY HAS CONFIRMED IN FOUR MONTHS ──
   Still shown, it is still the best we know, and hiding it would make the map
   emptier rather than more honest. What it loses is the confidence: a dashed
   edge and a lighter face, so it reads as "last we heard" instead of "this is
   what it costs". The colour band stays, because the band is still roughly
   right even when the number has drifted. */
.pin-priced.pin-stale{
  border-style:dashed;
  /* Not opacity: .pin-priced runs the pinIn animation with fill mode `both`,
     and an animated property beats a plain declaration, so an opacity here is
     silently thrown away by the last keyframe. Saturation is not animated,
     and desaturating keeps the band's hue, still roughly right, while
     taking away the confidence the full colour claims. */
  filter:saturate(.5);
  box-shadow:0 1px 3px rgba(0,0,0,.28);
}
.pin-priced.pin-stale .pin-price{ font-weight:600 }
/* The bead form has no text to soften, so it fades instead. Beads are not
   animated, so opacity reaches them. */
.pin-bead.pin-stale{ opacity:.55 }

/* The little stem that points at the actual coordinate */
.pin-stem{
  position:absolute;bottom:-5px;left:50%;transform:translateX(-50%);
  width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid currentColor;opacity:.9;
}
/* ── WHAT'S ON MODE PINS ──
   Green for tonight, amber for later this week, and a small grey bead for a
   pub with nothing on. The colours are deliberately the *same* green and
   amber the rest of the app uses for live and standing-by, they can be,
   because in this mode the price bands are not on the map at all. */
.pin-ev{
  display:flex;align-items:center;justify-content:center;
  border-radius:50% 50% 50% 50%;
  position:relative;
  box-shadow:0 3px 10px rgba(0,0,0,.45);
  transition:transform var(--dur-fast) ease;
  /* Sized here for the same reason the price tags are: these used to carry
     width:28px;height:28px;font-size:14px in their own markup, so every zoom
     rebuilt every one of them - in the mode where the pins are biggest. */
  --d:max(24px, calc(28px * var(--pin-s, 1)));
  width:var(--d);height:var(--d);font-size:calc(var(--d) * .5);
}
.pin-ev-tonight{
  background:var(--accent-solid);border:2px solid var(--price-cheap-edge);
}
/* Tonight is the only thing on this map that moves. It is also the only thing
   that is happening right now, so it is allowed. */
.pin-ev-tonight{animation:pin-ev-pulse 2.4s ease-in-out infinite}
@keyframes pin-ev-pulse{
  0%,100%{box-shadow:0 3px 10px rgba(0,0,0,.45), 0 0 0 0 rgba(var(--brand-rgb),.45)}
  50%    {box-shadow:0 3px 10px rgba(0,0,0,.45), 0 0 0 7px rgba(var(--brand-rgb),0)}
}
.pin-ev-week{
  background:var(--amber);border:2px solid var(--price-dear-edge);
}
.pin-ev-none{
  background:var(--muted);border:1.5px solid var(--border-strong);
  opacity:.5;border-radius:50%;box-shadow:none;
  --d:max(10px, calc(11px * var(--pin-s, 1)));
}
.pin-ev-none.pin-visited{
  opacity:.75;border-color:var(--accent);
  --d:max(10px, calc(13px * var(--pin-s, 1)));
}
.pin-ev-glyph{line-height:1;filter:drop-shadow(0 1px 1px rgba(0,0,0,.4))}
/* A pub running three things this week says so rather than showing one of
   them and hiding the rest behind a tap. */
.pin-ev-count{
  position:absolute;top:-5px;right:-5px;
  min-width:16px;height:16px;padding:0 3px;
  display:flex;align-items:center;justify-content:center;
  background:var(--bg);border:1.5px solid currentColor;border-radius:99px;
  font-size:11px;font-weight:800;color:var(--text);
  font-family:var(--font-sans);line-height:1;
}
.pin-ev-tonight .pin-stem{border-top-color:var(--accent-solid)}
.pin-ev-week .pin-stem{border-top-color:var(--amber)}
@media (hover:hover){
  .pin-ev:hover{transform:scale(1.12)}
}
@media (prefers-reduced-motion:reduce){
  .pin-ev{transition:none}
  .pin-ev-tonight{animation:none}
}

.pin-cheap .pin-stem{border-top-color:var(--price-cheap)}
.pin-mid   .pin-stem{border-top-color:var(--price-mid)}
.pin-dear  .pin-stem{border-top-color:var(--price-dear)}

.pin-price{font-variant-numeric:tabular-nums;letter-spacing:-.01em}

/* == A TAG IS SIZED HERE, NOT IN ITS HTML ================================
   font-size, padding, width and height were all computed from the zoom scale
   in JavaScript and written into every tag's markup, which meant a zoom
   changed the HTML of every priced pin on screen and Leaflet tore all of them
   out of the DOM and built them again. They are multiples of --pin-s here, so
   the markup is the same string at every zoom and a zoom costs nothing.
   The max() floors mirror the ones the old arithmetic carried: a tag never
   shrinks below legibility however far out you are. */
.pin-priced,.pin-guide{
  font-size:max(10px, calc(12px * var(--pin-s, 1)));
  padding:calc(4px * var(--pin-s, 1)) calc(4px * var(--pin-s, 1) + 3px);
}

/* == THE TIP OF THE STEM IS THE PUB ======================================

   A stemmed pin's marker element is a ZERO-SIZE POINT sitting exactly on the
   pub's coordinate - see tagIcon() in index.js. Everything visible hangs off
   that point from here.

   bottom:5px is the height of .pin-stem, which hangs 5px below the tag it is
   attached to. So the tag's bottom edge sits 5px above the coordinate and the
   stem's tip lands on it - at every zoom, on every screen, whatever the tag
   turns out to measure. The arithmetic this replaced guessed the tag's height
   from its font size and was one to four pixels out, differently at every
   zoom, which is what marker drift looks like from the outside. */
.pin-tagwrap{overflow:visible}
.pin-tagwrap > .pin-hit{
  position:absolute;
  left:0;bottom:5px;
  /* max-content, not auto. An absolutely positioned box with `left` set and no
     `right` gets its available width from the containing block, and the
     containing block here is the zero-size point itself - so `auto` resolves
     against zero. The content is `white-space:nowrap` throughout, which makes
     min-content and max-content the same number and hides the problem, but a
     single wrappable thing added to a tag one day would collapse it to a
     column one character wide. Saying max-content says what is meant. */
  width:max-content;height:auto;
  display:block;
  transform:translateX(-50%);
}
/* The 44px a thumb needs, without a 44px box a mouse could use to swallow
   clicks meant for the pin behind. It is invisible and it sits behind the pin
   it belongs to, so it only ever catches what would otherwise have missed. */
.pin-tagwrap > .pin-hit::before{
  content:'';position:absolute;left:50%;top:50%;
  width:max(100%, var(--pin-tap, 44px));
  height:max(100%, var(--pin-tap, 44px));
  transform:translate(-50%,-50%);
}

/* == THE NAME, WHEN YOU ARE CLOSE ENOUGH FOR IT TO MEAN SOMETHING ========
   Under the pin rather than inside it: folded into the tag, a long pub name
   would stretch the tag sideways and drag the price away from the coordinate
   the stem is pointing at. It never takes a click - the pin above it is the
   target, and a caption that ate taps would be worse than no caption. */
.pin-caption{
  position:absolute;top:calc(100% + 7px);left:50%;
  transform:translateX(-50%);
  width:max-content;
  max-width:136px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
  font-family:var(--font-sans);font-size:11px;font-weight:700;line-height:1.2;
  color:var(--text);pointer-events:none;
  text-shadow:0 1px 2px var(--bg),0 0 3px var(--bg),0 0 6px var(--bg),0 0 10px var(--bg);
}

/* == THE ONE THE SHEET IS ABOUT =========================================
   The map used to have no idea which pin you had opened: you tapped one of
   forty and the sheet came up describing a pub you could no longer pick out
   on screen. It is a class in the icon's own markup rather than one poked
   onto the element, because setIcon replaces the element - see
   setSelectedPub() in index.js. */
.pin-selected{
  box-shadow:0 0 0 3px var(--accent-solid),0 8px 22px rgba(0,0,0,.55)!important;
}
.pin-priced.pin-selected,.pin-guide.pin-selected{transform:scale(1.1)}
.pin-bead.pin-selected{transform:scale(1.6)}
.pin-dot.pin-selected{transform:scale(1.45)}

/* == SHUT RIGHT NOW =====================================================
   Only ever when the answer is known and it is no: isOpenNow() returns null
   for every pub with no opening hours on file, and dimming those would be the
   map telling you they are closed when it has no idea. */
.pin-shut{filter:grayscale(.6) brightness(.82)}
/* filter does not accumulate across rules - a later declaration replaces the
   earlier one outright - so a pin that is both stale and shut has to state
   both in one place or it silently loses one of them. */
.pin-priced.pin-stale.pin-shut{filter:saturate(.5) grayscale(.6) brightness(.82)}
.pin-bead.pin-shut,.pin-dot.pin-shut{opacity:.45}

/* ══════════════════════════════════════════════════════════════════════════
   CLUSTERS

   Zoomed out over Dublin the map was 774 beads in an area that can legibly
   hold perhaps forty, which is not a map of anything - it is a texture. Below
   the zoom where price tags appear, pubs within about 60 screen pixels of each
   other are drawn as one bubble.

   The bubble is NOT a grey count. It carries the median price of the pubs
   inside it and wears the same three band colours the pins do, so zooming out
   stops showing you every price and goes on showing you where the town is
   cheap - which is the thing the map is for. A cluster that threw the price
   away would make zooming out a downgrade rather than a summary.
   ══════════════════════════════════════════════════════════════════════════ */
/* The key has to explain the two states the pins gained, or it is a key that
   is wrong rather than merely short. The swatches are samples of the real
   thing: the same dimming, and a bubble at legend scale. */
.legend-swatch.lg-shut{
  background:var(--pin-none);border:1px solid var(--pin-none-ring);
  border-radius:50%;width:11px;height:11px;
  filter:grayscale(.6) brightness(.82);
}
.legend-cluster{
  display:inline-flex;align-items:center;justify-content:center;flex-shrink:0;
  width:18px;height:18px;border-radius:50%;
  background:var(--price-mid);border:1.5px solid var(--price-mid-edge);
  color:var(--price-mid-ink);font-size:10px;font-weight:800;line-height:1;
}
.pin-clusterwrap{overflow:visible}
.pin-clusterwrap > .pin-cluster{
  position:absolute;left:0;top:0;
  transform:translate(-50%,-50%);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  border-radius:50%;
  font-family:var(--font-sans);line-height:1;
  font-variant-numeric:tabular-nums;font-feature-settings:"tnum" 1;
  /* == WHY THERE IS NO HALO HERE ==
     There was: a 6px ring of translucent brand green around every bubble. It
     added twelve pixels to the footprint of a circle that is at most 54 across,
     so two bubbles a comfortable distance apart still read as one blob, and
     three of them read as a smear. What separates a bubble from what is behind
     it is a two-pixel ring in the page's own ground - the same knock-out a map
     label uses - plus a shadow with somewhere to fall. It costs nothing in
     size, which is the point. */
  box-shadow:0 0 0 2px var(--bg), 0 2px 9px rgba(0,0,0,.30);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
@media (hover:hover){
  .pin-clusterwrap > .pin-cluster{transition:transform var(--dur-fast) ease, box-shadow var(--dur-fast) ease}
  .pin-clusterwrap > .pin-cluster:hover{
    transform:translate(-50%,-50%) scale(1.09);
    box-shadow:0 0 0 2px var(--bg), 0 6px 16px rgba(0,0,0,.38);
  }
}
.pin-clusterwrap > .pin-cluster:focus-visible{outline:3px solid var(--accent-solid);outline-offset:3px}

/* Three sizes, and the radii these come to are the ones clusterRadius() in
   index.js uses to keep two bubbles from ever touching. Change a diameter here
   and change it there, or the collision pass is measuring a circle that is not
   the one on screen. */
.pc-sm{width:38px;height:38px}
.pc-md{width:46px;height:46px}
.pc-lg{width:54px;height:54px}

/* The count is the answer to "how many pubs is that" and it is what somebody
   is reading, so it gets the weight. The price underneath is the same figure
   the tags carry, quieter, because at this zoom it is a characterisation of an
   area rather than a claim about a pub. */
.pc-n{font-weight:800;letter-spacing:-.02em}
.pc-sm .pc-n{font-size:15px}
.pc-md .pc-n{font-size:16px}
.pc-lg .pc-n{font-size:17px}
.pc-p{font-weight:600;opacity:.75;margin-top:2px;letter-spacing:-.01em}
.pc-sm .pc-p{font-size:10px}
.pc-md .pc-p{font-size:10px}
.pc-lg .pc-p{font-size:11px}

/* The three price bands, which are the same three the pins wear - so zooming
   out summarises the price map rather than throwing it away. */
.pin-cluster.pin-cheap{background:var(--price-cheap);border:1.5px solid var(--price-cheap-edge);color:var(--price-cheap-ink)}
.pin-cluster.pin-mid  {background:var(--price-mid);  border:1.5px solid var(--price-mid-edge);  color:var(--price-mid-ink)}
.pin-cluster.pin-dear {background:var(--price-dear); border:1.5px solid var(--price-dear-edge); color:var(--price-dear-ink)}
/* Nobody in this group has reported a price, so the bubble does not wear one
   of the three bands - it says how many pubs are here and nothing else. */
/* The same grey the unpriced PIN wears, not a white disc. On a light basemap
   a white circle with a hairline border is a hole in the map - it reads as
   something that failed to load rather than as an answer. The legend calls
   this colour "no price yet" a few lines up; the bubble has to be a sample of
   the thing the legend names, or the key is describing something that is not
   on screen. */
.pin-cluster.pc-none{
  background:var(--pin-none);border:1.5px solid var(--pin-none-ring);color:#fff;
}

/* ══════════════════════════════════════════════════════════════════════════
   YOU ARE HERE

   Was a circleMarker that deleted itself after four seconds, so the answer to
   "where am I" was gone before you had finished reading the pubs around it.
   It is a layer that stays and follows the next fix, and it never takes a
   pointer event - a dot that swallowed taps meant for the pub underneath
   would be worse than no dot at all.
   ══════════════════════════════════════════════════════════════════════════ */
.me-wrap{overflow:visible;pointer-events:none}
.me-wrap > .me-dot{
  position:absolute;left:0;top:0;
  width:16px;height:16px;margin:-8px 0 0 -8px;
  border-radius:50%;
  background:var(--accent-solid);
  border:2.5px solid var(--bg);
  box-shadow:0 1px 6px rgba(0,0,0,.55);
}
.me-pulse{
  position:absolute;inset:-2.5px;border-radius:50%;
  box-shadow:0 0 0 0 rgba(var(--brand-rgb),.55);
  animation:mePulse 2.6s ease-out infinite;
}
@keyframes mePulse{
  0%  {box-shadow:0 0 0 0 rgba(var(--brand-rgb),.5)}
  70% {box-shadow:0 0 0 18px rgba(var(--brand-rgb),0)}
  100%{box-shadow:0 0 0 0 rgba(var(--brand-rgb),0)}
}
/* The halo, only drawn when the fix is loose enough for it to mean something.
   A presentation attribute loses to a stylesheet rule, so Leaflet's own
   fill/stroke defaults give way to these without needing !important. */
.me-acc{fill:var(--accent-solid);fill-opacity:.12;stroke:none}
@media (prefers-reduced-motion:reduce){
  .me-pulse{animation:none}
}
.pin-tick{font-size:.8em;opacity:.75;font-weight:800}

/* The dot form, whatever its colour. Two kinds of pin end up here: a pub with
   no price at all, and a pub with a guide figure, seen from further out than
   the zoom at which figures are drawn. They are the same dot and this is where
   they are the same size. font-size is set alongside the diameter so the
   visited tick inside can be a fraction of it in `em`. */
.pin-dot{
  --d:max(12px, calc(14px * var(--pin-s, 1)));
  width:var(--d);height:var(--d);font-size:var(--d);
  /* The same guard .pin-bead carries, and for the same reason. A pub with a
     guide figure, seen from below the zoom at which figures are drawn, is
     `pin-guide pin-dot` - and .pin-guide takes its padding from --pin-s along
     with the price tags now. Everything here is border-box, so that padding
     does not make the dot bigger; it eats the dot from the inside, and at
     zoom 18 there is nothing left of the 12px circle for the visited tick to
     be drawn in. */
  padding:0!important;
}
.pin-dot.pin-visited{ --d:max(12px, calc(18px * var(--pin-s, 1))) }

/* Unpriced venues, quiet, but clearly tappable, and never green. */
.pin-unpriced{
  border-radius:50%;
  background:var(--pin-none);
  border:1.5px solid var(--pin-none-ring);
  box-shadow:0 1px 4px rgba(0,0,0,.45);
}
/* Visited keeps the grey fill: "I have been here" is a fact about you, and
   painting it green would put a personal note on the price scale. The brand
   ring and the tick say it instead. */
.pin-unpriced.pin-visited{border-color:var(--accent);border-width:2.5px}
.pin-unpriced:hover{transform:scale(1.25)}
.pin-dot-tick{color:#151719;font-weight:800;line-height:1;font-size:.6em}

/* Live-right-now marker */
.pin-live-dot{
  position:absolute;top:-4px;right:-4px;width:9px;height:9px;border-radius:50%;
  background:var(--live);border:1.5px solid var(--bg);
  box-shadow:0 0 0 0 var(--live-glow);animation:livePulse 2.2s infinite;
}
@keyframes livePulse{
  0%  {box-shadow:0 0 0 0 var(--live-glow)}
  70% {box-shadow:0 0 0 7px rgba(0,0,0,0)}
  100%{box-shadow:0 0 0 0 rgba(0,0,0,0)}
}
@media (prefers-reduced-motion:reduce){
  .pin-live-dot{animation:none}
  .pin{transition:none}
}

/* ══════════════════════════════════════════════════════════════
   CONFIDENCE: how much a community price can be trusted
   ══════════════════════════════════════════════════════════════ */
.conf{display:inline-flex;align-items:center;gap:2px;vertical-align:middle;margin-left:5px;cursor:help}
.conf-dot{width:4px;height:4px;border-radius:50%;background:var(--border)}
.conf-dot.on{background:var(--muted)}
.conf-high  .conf-dot.on{background:var(--green)}
.conf-medium .conf-dot.on{background:var(--amber)}
.conf-low   .conf-dot.on{background:var(--muted)}
.conf-stale .conf-dot.on{background:var(--red)}

/* The movement chip is defined in shared.css, because the feed draws it too
   and feed.html does not load this file. In a list row it sits in a meta line
   of 12px grey, so it steps down to match rather than shouting over the pub's
   name. */
.cheapest-meta .price-move{padding:2px 6px;font-size:11.5px}
.cheapest-meta .price-move .ic{font-size:12px}

.price-hero{display:flex;align-items:baseline;gap:8px;flex-wrap:wrap}
.price-hero-num{
  font-family:var(--font-num);font-size:30px;font-weight:700;
  color:var(--green);font-variant-numeric:tabular-nums;line-height:1;
}
.price-hero-label{font-size:13px;color:var(--muted);text-transform:uppercase;letter-spacing:.07em;font-weight:600}
.price-hero-meta{font-size:13px;color:var(--muted);width:100%;margin-top:3px}
.price-band-chip{
  display:inline-block;font-size:12px;font-weight:700;text-transform:uppercase;
  letter-spacing:.06em;padding:2px 7px;border-radius:4px;vertical-align:middle;
}
.price-band-chip.cheap{background:rgba(var(--brand-rgb),.18);color:var(--green)}
.price-band-chip.mid  {background:rgba(107,158,118,.18);color:var(--muted)}
.price-band-chip.dear {background:rgba(240,168,104,.2);color:var(--price-dear)}


/* Per-drink community price table inside the sheet */
.cprice-list{display:flex;flex-direction:column;gap:1px;margin-top:8px}
.cprice-row{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  padding:7px 10px;background:var(--panel2);border-radius:6px;
}
.cprice-name{font-size:14px;color:var(--text);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cprice-val{font-size:14px;font-weight:700;color:var(--green);font-variant-numeric:tabular-nums;white-space:nowrap}
/* `flex:0 0 auto` on both of the middle columns, so the row has exactly one
   element that gives way under pressure, the drink name, which has an
   ellipsis and can afford to. Without it a long name shrinks "12 reports" and
   the price column instead, and the right edge stops lining up. */
.cprice-n{flex:0 0 auto;font-size:12px;color:var(--muted);white-space:nowrap}
.cprice-val{flex:0 0 auto}

/* ── THE SCORE ON A DRINK ──
   a star and 8.4, read-only. It was a button with the scale folded under it, which
   put a control on all eight rows of a list nobody scrolls to, reported as
   "it will become too clogged up with buttons on the pub card". Reading and
   writing are different jobs and only one of them belongs in a list.

   `flex:0 0 auto`, this column does not absorb slack. The drink name does,
   so the prices stay on one axis down the right edge whatever is in here.
   `min-width` so an unscored drink still holds the column open and the prices
   do not shuffle left and right down the list. */
.cprice-score{
  flex:0 0 auto;display:inline-flex;align-items:center;gap:3px;
  min-width:38px;justify-content:flex-end;
  font-size:13px;font-weight:700;color:var(--accent);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
.cprice-score-star{font-size:11px;line-height:1}

/* ── THE ONE WAY IN ──
   Under the pub's own stars, because that is where somebody looks to rate
   something. Full width and quiet: it is an invitation, not the primary
   action on the card, the price bar is. */
.btn-rate-pint{
  display:flex;align-items:center;justify-content:center;gap:7px;
  width:100%;min-height:42px;margin:0 0 12px;padding:9px 12px;
  border:1px dashed var(--border-strong);border-radius:12px;
  background:transparent;color:var(--text-2);
  font-family:inherit;font-size:14px;font-weight:600;
  cursor:pointer;touch-action:manipulation;
  transition:background .18s ease, border-color .18s ease, color .18s ease;
}
@media (hover:hover){
  .btn-rate-pint:hover{
    background:rgba(var(--brand-rgb),.09);
    border-color:var(--accent);color:var(--accent);
  }
}
.btn-rate-pint:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){ .btn-rate-pint{transition:none} }

/* ── THE PANEL ──
   Sits on .suggest-backdrop / .suggest-panel, the same furniture the closed-pub
   report uses. Only the two pieces it adds are here. */
.drate-results{display:flex;flex-direction:column;gap:4px;margin-top:8px}
/* Same trap as .drate-scale below: a class selector with a display beats the
   user agent's [hidden]{display:none}, so hiding the list after a pick would
   set an attribute and change nothing. */
.drate-results[hidden]{display:none}
.drate-hit{
  display:flex;align-items:center;gap:10px;width:100%;
  min-height:44px;padding:9px 12px;
  border:1px solid var(--border);border-radius:10px;
  background:var(--panel-2);color:var(--text);
  font-family:inherit;font-size:15px;text-align:left;
  cursor:pointer;touch-action:manipulation;
  transition:border-color .15s ease, background .15s ease;
}
.drate-hit-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.drate-hit-mine{flex:0 0 auto;font-size:12px;color:var(--muted);white-space:nowrap}
.drate-hit-score{
  flex:0 0 auto;font-size:13px;font-weight:700;color:var(--accent);
  font-variant-numeric:tabular-nums;white-space:nowrap;
}
@media (hover:hover){ .drate-hit:hover{border-color:var(--accent)} }
.drate-hit.is-picked{
  border-color:var(--accent);background:rgba(var(--brand-rgb),.12);
}
.drate-hit:focus-visible{outline:2px solid var(--accent);outline-offset:1px}

/* Wider than the other panels on a screen that has the room. This one carries
   a ten-button row where the closed-pub report carries a form, and 500px was
   chosen for the form. Nothing is gained by cramping the strip on a laptop. */
#drate-panel{max-width:560px}
/* And less padding on a phone, where 28px each side is 56px of a 320px screen
   spent on nothing. Scoped to this panel so the panels that were laid out
   against the old figure keep it. */
@media (max-width:400px){
  #drate-panel{padding:22px 16px}
}

.drate-scale{display:flex;flex-direction:column;gap:8px;margin-top:14px}
/* Wraps rather than truncates. "How was the Kopparberg Strawberry & Lime?" is
   a real drink in DRINKS and the ellipsis version asks about a
   "Kopparberg Strawberry & L…", which is a different, sillier question. */
.drate-scale .pint-score-label{white-space:normal;overflow:visible;line-height:1.35}
/* A class selector with a display beats the user agent's [hidden]{display:none},
   so `scale.hidden = true` would set an attribute and change nothing on screen,
   the panel would open with the ten buttons already showing under an empty
   search. test/hidden-attribute.test.mjs exists for exactly this and caught it. */
.drate-scale[hidden]{display:none}
.pint-score-label{
  font-size:12px;color:var(--muted);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.pint-score-dots{display:flex;gap:4px;align-items:stretch}
.pint-score{
  /* `min-width:0`, and it is the whole reason this row cannot break. A flex
     item defaults to min-width:auto, which refuses to shrink past its content,
     ten of those plus nine gaps overflowed a 320px panel by a few pixels,
     measured, before this line existed. With a zero floor the ten buttons
     divide whatever the row has and the strip is exactly as wide as its
     parent at every screen size there is.

     The touch target is carried by the height instead: 36-38px tall and about
     22px wide on the narrowest phone, which is comfortably hittable in a row
     of adjacent numbers in a way a 22px square would not be. */
  flex:1 1 0;min-width:0;max-width:52px;
  height:36px;padding:0;border-radius:8px;
  border:1px solid var(--border);background:var(--panel2);
  color:var(--muted);font-size:12px;font-weight:700;line-height:1;
  font-variant-numeric:tabular-nums;cursor:pointer;
  touch-action:manipulation;                 /* no 300ms wait, no double-tap zoom */
  transition:background .15s ease, border-color .15s ease, color .15s ease;
}
/* Everything below the score you picked is tinted, so the strip reads as a
   scale filling up rather than as ten unrelated buttons with one of them on.
   It is the difference between "I chose 8" and "it is an 8". */
.pint-score.under{
  background:rgba(var(--brand-rgb),.14);
  border-color:rgba(var(--brand-rgb),.28);
  color:var(--accent);
}
.pint-score.on{
  background:var(--accent);border-color:var(--accent);color:var(--on-accent);
}
/* Hover is a pointer thing. On a touch screen it sticks after the tap and
   leaves a button looking half-selected, which on a scale is a lie. */
@media (hover:hover){
  .pint-score:hover{border-color:var(--accent);color:var(--accent)}
  .pint-score.on:hover{color:var(--on-accent)}
}
.pint-score:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px;
}
@media (prefers-reduced-motion:reduce){
  .pint-score{transition:none}
}

/* Narrow phones: 320px of viewport is 284px of sheet, which is 24.8px per
   button once the gaps are out. The height carries the touch target instead,
   a 25×38 strip of adjacent numbers is comfortably hittable in a way a 25×25
   one is not, and the gap tightens so the numerals keep their own room.

   The word after the report count goes at the same width. "12 reports" and
   "12" are the same fact, and four columns on a 284px sheet is the difference
   between a drink name and "Guinne…". */
@media (max-width:400px){
  .pint-score-dots{gap:3px}
  .pint-score{height:38px;font-size:11px;border-radius:7px}
  .cprice-n-word{display:none}
  .cprice-score{min-width:34px;font-size:12px}
  /* 10px of gap times three is thirty pixels of a 284px row spent on nothing.
     Six still reads as four separate columns and gives the drink name back
     twelve characters. */
  .cprice-row{gap:6px}
  .drate-hit{padding:9px 10px;font-size:14px;gap:8px}
}
/* Laptop and up the sheet is a 760px dialog. Uncapped, the buttons would
   become slabs; capped, they settle into a row of chips with the label above
   and room around it. */
@media (min-width:1100px){
  .pint-score{height:38px;font-size:13px}
  .pint-score-label{font-size:13px}
}

/* ── THE PUB'S SCORE, ON THE TITLE ──
   4.6, five stars and (23), on the line under the pub name, which is where a rating
   goes in every product that has one. One line, no label, and nothing at all
   when nobody has rated the pub, `hidden` is set in JS rather than left to an
   empty element, because an empty element still has a margin and a 6px gap
   under the name of every unrated pub is how a layout starts looking
   accidental.

   Wraps rather than truncates: "(1,204)" is the part that grows and a second
   line is cheaper than an ellipsis through a number. */
.sheet-score{
  display:flex;align-items:baseline;gap:5px;flex-wrap:wrap;
  margin-top:4px;font-size:13px;line-height:1.2;
}
.sheet-score-val{font-weight:800;color:var(--text);font-variant-numeric:tabular-nums}
.sheet-score-stars{color:var(--accent);font-size:12px;line-height:1}
.sheet-score-n{color:var(--text-3);font-size:12px}

/* ══════════════════════════════════════════════════════════════
   LIVE BUZZ
   ══════════════════════════════════════════════════════════════ */
.buzz-badge{
  display:inline-flex;align-items:center;gap:5px;font-size:13px;font-weight:700;
  padding:3px 8px;border-radius:20px;background:rgba(var(--live-rgb),.14);
  color:var(--live);white-space:nowrap;vertical-align:middle;
}
.buzz-dot{width:6px;height:6px;border-radius:50%;background:var(--live);animation:livePulse 2.2s infinite}
.buzz-hopping{background:rgba(var(--live-rgb),.2)}
.buzz-busy{background:rgba(var(--warn-rgb),.16);color:var(--amber)}
.buzz-busy .buzz-dot{background:var(--amber)}
.buzz-steady{background:rgba(var(--brand-rgb),.14);color:var(--green)}
.buzz-steady .buzz-dot{background:var(--green)}

.buzz-panel{background:var(--panel2);border:1px solid var(--border);border-radius:10px;padding:12px 14px;margin:8px 0}
.buzz-panel-head{
  display:flex;align-items:center;justify-content:space-between;gap:8px;
  font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;
  color:var(--muted);margin-bottom:8px;
}
.buzz-panel-head span{text-transform:none;letter-spacing:0;font-size:14px;color:var(--live)}
.buzz-panel.buzz-busy    .buzz-panel-head span{color:var(--amber)}
.buzz-panel.buzz-steady  .buzz-panel-head span{color:var(--green)}
.buzz-meter{height:7px;background:var(--bg);border-radius:99px;overflow:hidden}
.buzz-meter-fill{height:100%;border-radius:99px;background:linear-gradient(90deg,var(--green),var(--live));transition:width .5s ease}
.buzz-panel-foot{font-size:13px;color:var(--muted);margin-top:7px}
.buzz-panel-foot strong{color:var(--text);font-weight:700}
.buzz-panel-empty p{font-size:13px;color:var(--muted);line-height:1.55;margin:0}

/* ══════════════════════════════════════════════════════════════
   CITY PRICE BAR: the headline number, above the map
   ══════════════════════════════════════════════════════════════ */
.price-stats-bar{
  display:none;align-items:stretch;gap:0;flex-shrink:0;
  background:var(--panel);border-bottom:1px solid var(--border);
  padding:0 10px;overflow-x:auto;scrollbar-width:none;
  /* One height, four states. The bar has a loading state, a failed state, an
     empty state and a loaded one, and they used to be four different heights:
     the map under it stepped down as the answer arrived and stepped again if
     the town changed. --psb-h is the tallest of them, the loaded bar, whose
     cheapest-pint stat is three lines, so nothing below it ever moves. */
  --psb-h:66px;
  min-height:var(--psb-h);
}
.price-stats-bar::-webkit-scrollbar{display:none}
.price-stats-bar.show{display:flex}
.psb-stat{
  display:flex;flex-direction:column;align-items:flex-start;justify-content:center;gap:1px;
  padding:7px 14px;background:none;border:none;cursor:pointer;text-align:left;
  font-family:var(--font-sans);color:inherit;flex-shrink:0;
  border-radius:6px;transition:background .15s;
}
.psb-stat:hover{background:var(--panel2)}
.psb-coverage{cursor:help}
.psb-coverage:hover{background:none}
.psb-label{font-size:12px;text-transform:uppercase;letter-spacing:.09em;color:var(--muted);font-weight:600;white-space:nowrap}
.psb-value{
  font-family:var(--font-num);font-size:17px;font-weight:700;
  color:var(--text);font-variant-numeric:tabular-nums;line-height:1.1;
}
.psb-best .psb-value{color:var(--green)}
.psb-sub{font-size:12px;color:var(--muted);max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.psb-sep{width:1px;background:var(--border);margin:8px 0;flex-shrink:0}

/* ── THE EMPTY STATE, AND THE LENGTH OF A TOWN'S NAME ──
   Two children, and the whole rule is which of them gives way. The button is
   the thing being asked for, so it never shrinks and never wraps; the sentence
   is the thing that can be read from half of itself, so it is the one that
   truncates. Without `min-width:0` a flex child refuses to shrink below its
   content and the button wraps instead, which is exactly what "Add prices to
   Crossmolina" did on a 390px screen. */
.psb-empty{
  display:flex;align-items:center;gap:10px;padding:9px 6px;
  font-size:13px;color:var(--muted);white-space:nowrap;
  flex:1 1 auto;min-width:0;
}
.psb-empty-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.psb-empty strong{color:var(--text)}
.psb-cta{
  background:var(--green-dark);color:var(--on-accent);border:none;border-radius:6px;
  padding:5px 11px;font-size:13px;font-weight:600;cursor:pointer;
  font-family:var(--font-sans);white-space:nowrap;flex-shrink:0;
}
.psb-cta:hover{background:var(--green-dim)}
.psb-cta:disabled{opacity:.6;cursor:default}

/* Two bars that are not the "this city needs prices" bar: one while the answer
   is still coming, one when it never did. Both are the same height as the real
   thing so the map does not step up and down underneath as it resolves. */
.psb-loading{gap:12px}
.psb-skel{
  display:block;height:11px;width:120px;border-radius:5px;
  background:linear-gradient(90deg,var(--panel2) 25%,var(--border) 37%,var(--panel2) 63%);
  background-size:400% 100%;
  animation:psb-shimmer 1.3s ease infinite;
}
.psb-skel-sm{width:64px}
@keyframes psb-shimmer{0%{background-position:100% 0}100%{background-position:0 0}}
@media (prefers-reduced-motion: reduce){
  .psb-skel{animation:none;background:var(--panel2)}
}
.psb-failed{color:var(--text-2)}

/* ══════════════════════════════════════════════════════════════
   CHEAPEST PINT PANEL
   ══════════════════════════════════════════════════════════════ */
.cheapest-backdrop{
  position:fixed;inset:0;background:rgba(0,0,0,.55);z-index:var(--z-sheet);
  opacity:0;pointer-events:none;transition:opacity .22s ease;
}
.cheapest-backdrop.open{opacity:1;pointer-events:auto}

.cheapest-panel{
  position:fixed;top:0;right:0;bottom:0;width:min(420px,100vw);z-index:calc(var(--z-sheet) + 1);
  background:var(--panel);border-left:1px solid var(--border);
  display:flex;flex-direction:column;
  transform:translateX(100%);transition:transform .28s cubic-bezier(.32,.72,0,1);
  box-shadow:-8px 0 32px rgba(0,0,0,.4);
}
.cheapest-panel.open{transform:translateX(0)}

.cheapest-head{padding:16px 18px 12px;border-bottom:1px solid var(--border);flex-shrink:0}
.cheapest-title-row{display:flex;align-items:center;justify-content:space-between;gap:10px}
.cheapest-title{font-family:var(--font-display);font-size:23px;font-weight:700;color:var(--text)}
.cheapest-close{
  background:none;border:none;color:var(--muted);font-size:26px;line-height:1;
  cursor:pointer;padding:0 4px;border-radius:6px;
}
.cheapest-close:hover{color:var(--text);background:var(--panel2)}
.cheapest-sub{font-size:13px;color:var(--muted);margin-top:2px}
.cheapest-sorts{display:flex;gap:6px;margin-top:12px}
.cheapest-sort-btn{
  flex:1;padding:6px 10px;border-radius:7px;border:1px solid var(--border);
  background:transparent;color:var(--muted);font-size:13px;font-weight:600;
  cursor:pointer;font-family:var(--font-sans);transition:all .15s;
}
.cheapest-sort-btn:hover{color:var(--text);border-color:var(--green-dim)}
.cheapest-sort-btn.active{background:var(--green-dark);border-color:var(--green-dark);color:var(--on-accent)}

.cheapest-body{
  flex:1;min-height:0;overflow-y:auto;
  padding:8px 12px max(12px, env(safe-area-inset-bottom));
  display:flex;flex-direction:column;gap:4px;
}

.cheapest-row{
  display:flex;align-items:center;gap:11px;width:100%;
  padding:10px 12px;border-radius:10px;border:1px solid transparent;
  background:var(--panel2);cursor:pointer;text-align:left;
  font-family:var(--font-sans);transition:all .15s;
}
.cheapest-row:hover{border-color:var(--green-dim);transform:translateX(-2px)}
.cheapest-rank{
  flex-shrink:0;width:22px;height:22px;border-radius:50%;
  background:var(--bg);color:var(--muted);font-size:13px;font-weight:700;
  display:flex;align-items:center;justify-content:center;font-variant-numeric:tabular-nums;
}
.cheapest-row:nth-child(1) .cheapest-rank{background:var(--green-dark);color:var(--on-accent)}
.cheapest-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:3px}
.cheapest-name{
  font-size:15px;font-weight:600;color:var(--text);
  display:flex;align-items:center;gap:6px;flex-wrap:wrap;
}
.cheapest-meta{display:flex;gap:8px;font-size:12px;color:var(--muted);flex-wrap:wrap}
.cheapest-meta span::after{content:"·";margin-left:8px;opacity:.5}
.cheapest-meta span:last-child::after{content:""}
.cheapest-price-col{display:flex;flex-direction:column;align-items:flex-end;gap:2px;flex-shrink:0}
.cheapest-price{
  font-family:var(--font-num);font-size:19px;font-weight:700;
  font-variant-numeric:tabular-nums;line-height:1;
}
.cheapest-price.price-cheap{color:var(--green)}
/* Same idea in the list: the row is not demoted, it is annotated. Somebody
   deciding where to walk gets to weigh "cheapest" against "nobody has checked
   this since February" in the same glance. */
.cheapest-row.is-stale .cheapest-price{opacity:.75}
.cheapest-stale{color:var(--amber);font-weight:600}
.cheapest-price.price-mid{color:var(--text)}
.cheapest-price.price-dear{color:var(--price-dear)}
.cheapest-diff{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
.cheapest-diff.best{color:var(--green);font-weight:700;text-transform:uppercase;letter-spacing:.05em}

.cheapest-empty{padding:36px 20px;text-align:center;display:flex;flex-direction:column;gap:10px;align-items:center}
.cheapest-empty-icon{font-size:38px;opacity:.7}
.cheapest-empty p{font-size:14px;color:var(--muted);line-height:1.55;max-width:34ch}
.cheapest-empty strong{color:var(--text)}
.cheapest-empty-cta{
  margin-top:6px;background:var(--green-dark);color:var(--on-accent);border:none;border-radius:8px;
  padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;font-family:var(--font-sans);
}
.cheapest-empty-cta:hover{background:var(--green-dim)}

.cheapest-foot{
  flex-shrink:0;padding:11px 18px;border-top:1px solid var(--border);
  font-size:13px;color:var(--muted);display:flex;align-items:center;
  justify-content:space-between;gap:10px;
}
.cheapest-foot-link{
  background:none;border:none;color:var(--green);font-size:13px;font-weight:600;
  cursor:pointer;font-family:var(--font-sans);white-space:nowrap;
}

@media (max-width:600px){
  .cheapest-panel{
    top:auto;width:100vw;height:82vh;border-left:none;
    border-top-left-radius:18px;border-top-right-radius:18px;
    border-top:1px solid var(--border);
    transform:translateY(100%);
  }
  .cheapest-panel.open{transform:translateY(0)}
  .psb-stat{padding:6px 11px}
  .psb-value{font-size:15px}
}
@media (prefers-reduced-motion:reduce){
  .cheapest-panel,.cheapest-backdrop,.cheapest-row{transition:none}
}

/* ══════════════════════════════════════════════════════════════
   PRICE HERO: the headline block at the top of a pub sheet
   ══════════════════════════════════════════════════════════════ */
.price-hero-block{
  background:var(--panel2);border:1px solid var(--border);border-radius:12px;
  padding:14px 16px;margin-bottom:14px;
}
.price-hero-block[hidden]{display:none}

/* Why the signup form opened, see showPendingPriceNote(). */
.modal-pending{
  display:flex;gap:9px;align-items:flex-start;
  background:var(--brand-ghost);border:1px solid var(--brand-ring);
  border-radius:10px;padding:11px 13px;margin-bottom:14px;
  font-size:14px;line-height:1.45;color:var(--text-2);
}
.modal-pending strong{color:var(--accent);font-variant-numeric:tabular-nums}
.modal-pending-ico{flex:0 0 auto;font-size:17px;line-height:1.2}
.modal-pending[hidden]{display:none}

/* ══════════════════════════════════════════════════════════════
   THE PRICE BAR: the one action the product depends on
   Directly under the hero, above everything else in the sheet,
   sized for a thumb holding a pint in the other hand.
   ══════════════════════════════════════════════════════════════ */
.pbar{
  background:var(--brand-ghost);
  border:1px solid var(--brand-ring);
  border-radius:12px;
  padding:13px 14px 12px;
  margin-bottom:14px;
}
.pbar-q{
  font-size:15px;color:var(--text);line-height:1.35;
  margin-bottom:10px;
}
.pbar-q strong{color:var(--accent);font-weight:700;font-variant-numeric:tabular-nums}

/* ── One-tap confirmation: the volume driver ── */
.pbar-confirm{display:flex;gap:8px;align-items:stretch}
.pbar-yes{
  flex:1 1 auto;min-height:46px;padding:11px 14px;
  background:var(--accent-solid);color:var(--on-accent);
  border:1px solid var(--accent-solid);border-radius:10px;
  font-size:15px;font-weight:700;letter-spacing:-.005em;
  font-variant-numeric:tabular-nums;cursor:pointer;
  transition:background-color var(--dur-fast) ease, transform var(--dur-fast) var(--ease-out);
}
.pbar-yes:hover{background:var(--brand-500)}
.pbar-yes:active{transform:scale(.97)}
.pbar-no{
  flex:0 0 auto;min-height:46px;padding:11px 14px;
  background:transparent;color:var(--text-2);
  border:1px solid var(--border);border-radius:10px;
  font-size:14px;font-weight:600;cursor:pointer;white-space:nowrap;
  transition:color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.pbar-no:hover{color:var(--text);border-color:var(--border-strong)}

/* ── Typing the price in ──
   The only way a price is entered now. The bar used to lead with five preset
   chips and keep this behind an "Other" button; the chips covered 80c of a
   five-euro range, so the honest answer was off the strip more often than on
   it, and they were the widest thing on the card. One row: symbol, field,
   button. */
.pbar-keypad{display:flex;align-items:center;gap:8px}
.pbar-cur{
  font-size:19px;font-weight:700;color:var(--text-3);
  font-family:var(--font-num);
}
.pbar-input{
  flex:1 1 auto;min-width:0;min-height:46px;padding:11px 13px;
  background:var(--panel-2);color:var(--text);
  border:1px solid var(--border);border-radius:10px;
  font-size:17px; /* never below 16px: iOS zooms the viewport on focus */
  font-weight:700;font-variant-numeric:tabular-nums;
}
.pbar-input::placeholder{color:var(--text-3);font-weight:500}
/* No spinner. type=number is here for the numeric keypad it brings up on a
   phone, not for the two arrows it draws on a desktop, which are 20px of
   chrome inside the field, step by 10c, and are the slowest possible way to
   enter a price. Both vendor spellings, because Chrome and Firefox disagree
   about which one to honour. */
.pbar-input::-webkit-outer-spin-button,
.pbar-input::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}
.pbar-input{-moz-appearance:textfield;appearance:textfield}
.pbar-input:focus{outline:none;border-color:var(--accent);box-shadow:0 0 0 3px var(--brand-ring)}
.pbar-save{
  flex:0 0 auto;min-height:46px;padding:11px 18px;
  background:var(--accent-solid);color:var(--on-accent);
  border:1px solid var(--accent-solid);border-radius:10px;
  font-size:15px;font-weight:700;cursor:pointer;white-space:nowrap;
}
.pbar-save:active{transform:scale(.97)}

/* ── Which drink, and how confident the current number is ── */
.pbar-drinks{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}
.pbar-drink{
  padding:5px 10px;border-radius:var(--r-full);
  background:transparent;color:var(--text-3);
  border:1px solid var(--border);
  font-size:13px;font-weight:600;cursor:pointer;
  transition:all var(--dur-fast) ease;
}
.pbar-drink:hover{color:var(--text-2);border-color:var(--border-strong)}
.pbar-drink.on{
  background:var(--panel-2);color:var(--text);border-color:var(--border-strong);
}
.pbar-drink-more{border-style:dashed}
.pbar-select{
  width:100%;min-height:38px;padding:8px 10px;
  background:var(--panel-2);color:var(--text);
  border:1px solid var(--border);border-radius:9px;
  font-size:14px;font-weight:600;
}
.pbar-meta{font-size:13px;color:var(--text-3);margin-top:9px}
.pbar-meta.is-stale{color:var(--warn)}

/* ── Receipt state ── */
.pbar-thanks{
  display:flex;align-items:center;gap:9px;flex-wrap:wrap;
  font-size:14px;color:var(--text-2);line-height:1.4;
}
.pbar-thanks-ico{
  flex:0 0 auto;
  width:22px;height:22px;border-radius:50%;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--accent-solid);color:var(--on-accent);
  font-size:14px;font-weight:700;
}
.pbar-again{
  margin-left:auto;padding:6px 11px;border-radius:var(--r-full);
  background:transparent;color:var(--accent);
  border:1px solid var(--brand-ring);
  font-size:13px;font-weight:700;cursor:pointer;
}
.pbar-again:hover{background:var(--brand-ghost)}

/* The bar while the price is crossing the network. Same height as the row it
   replaces, so the sheet does not jump under the thumb that just tapped. */
.pbar-saving{
  display:flex;align-items:center;gap:10px;min-height:42px;
  font-size:14px;color:var(--text-2);
}
.pbar-spinner{
  flex:0 0 auto;width:18px;height:18px;border-radius:50%;
  border:2px solid var(--border);border-top-color:var(--accent-solid);
  animation:spin 0.8s linear infinite;
}
@media (prefers-reduced-motion: reduce){
  .pbar-spinner{animation-duration:2.4s}
}

@media (prefers-reduced-motion:reduce){
  .pbar-yes,.pbar-save{transition:none}
  .pbar-yes:active,.pbar-save:active{transform:none}
}

/* ══════════════════════════════════════════════════════════════
   CONTRIBUTION RECEIPT
   ══════════════════════════════════════════════════════════════ */
.contrib-receipt{
  /* Above the PWA install banner (z 9600) and the pub sheet, a receipt the
     user cannot see is not a receipt. */
  position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(18px);
  z-index:var(--z-float);display:flex;align-items:center;gap:12px;
  max-width:min(420px,calc(100vw - 32px));
  padding:13px 17px;border-radius:13px;
  background:var(--panel);border:1px solid var(--green-dim);
  box-shadow:0 10px 34px rgba(0,0,0,.45);
  opacity:0;pointer-events:none;transition:opacity .25s ease,transform .25s cubic-bezier(.32,.72,0,1);
}
.contrib-receipt.show{opacity:1;transform:translateX(-50%) translateY(0)}
.contrib-icon{font-size:26px;flex-shrink:0;line-height:1}
.contrib-text{display:flex;flex-direction:column;gap:2px;min-width:0}
.contrib-text strong{font-size:14px;font-weight:700;color:var(--green)}
.contrib-text span{font-size:13px;color:var(--muted);line-height:1.45}
@media (prefers-reduced-motion:reduce){.contrib-receipt{transition:opacity .2s}}

/* Check-in button */
.btn-checkin{
  display:flex;align-items:center;justify-content:center;gap:6px;
  padding:11px;border-radius:10px;cursor:pointer;
  font-family:var(--font-sans);font-size:14px;font-weight:600;
  background:rgba(var(--live-rgb),.1);border:1px solid rgba(var(--live-rgb),.32);color:var(--live);
  transition:all .16s;
}
.btn-checkin:hover{background:rgba(var(--live-rgb),.19);border-color:var(--live)}
/* Stood down while the map is picking a crawl stop. The pink is the loudest
   thing in the action row, and during a pick it is loud about the wrong tile. */
.btn-checkin.is-quiet{
  background:transparent;border-color:var(--border);color:var(--text-2);
}
.btn-checkin.is-quiet:hover{background:var(--hover);border-color:var(--border-strong)}

/* Zoomed-out bead: keeps the price heatmap readable without labels colliding.

   The diameter is 13px times --pin-s, the zoom scale the map container
   carries, see applyPinScale() in index.js. It used to be an inline
   `width:17px` written into the pin's HTML, which meant every zoom changed
   the HTML of every pin and Leaflet rebuilt all 774 of them. */
.pin-bead{
  --d:max(11px, calc(13px * var(--pin-s, 1)));
  width:var(--d);height:var(--d);
  border-radius:50%;padding:0!important;border-width:2px;
  box-shadow:0 1px 5px rgba(0,0,0,.5);
}
.pin-bead .pin-stem{display:none}
.pin-bead:hover{transform:scale(1.35)}

/* ══════════════════════════════════════════════════════════════
   PRICE FILTERS
   ══════════════════════════════════════════════════════════════ */
.fp-bands{display:flex;flex-direction:column;gap:6px;margin-bottom:14px}
.fp-band{
  display:flex;align-items:center;gap:9px;width:100%;padding:9px 12px;
  border-radius:10px;border:1px solid var(--border);background:transparent;
  color:var(--muted);font-family:var(--font-sans);font-size:14px;
  font-weight:500;cursor:pointer;text-align:left;transition:all .15s;
}
.fp-band:hover{color:var(--text);border-color:var(--green-dim)}
.fp-band.active{background:var(--panel2);color:var(--text);border-color:var(--green)}
.fp-band-swatch{width:12px;height:12px;border-radius:3px;flex-shrink:0}
.fp-cheap .fp-band-swatch{background:var(--price-cheap)}
.fp-mid   .fp-band-swatch{background:var(--price-mid);border:1px solid var(--price-mid-edge)}
.fp-dear  .fp-band-swatch{background:var(--price-dear)}

.fp-max-row{display:flex;align-items:baseline;justify-content:space-between;gap:8px;margin-bottom:6px}
.fp-max-label{font-size:13px;color:var(--muted)}
.fp-max-out{
  font-family:var(--font-num);font-size:16px;font-weight:700;
  color:var(--green);font-variant-numeric:tabular-nums;
}
.fp-max-slider{
  width:100%;height:5px;border-radius:99px;appearance:none;-webkit-appearance:none;
  background:var(--border);outline:none;margin-bottom:14px;cursor:pointer;
}
.fp-max-slider::-webkit-slider-thumb{
  -webkit-appearance:none;width:18px;height:18px;border-radius:50%;
  background:var(--green);border:2px solid var(--panel);cursor:pointer;
  box-shadow:0 1px 4px rgba(0,0,0,.4);
}
.fp-max-slider::-moz-range-thumb{
  width:18px;height:18px;border-radius:50%;background:var(--green);
  border:2px solid var(--panel);cursor:pointer;
}
.fp-max-slider:focus-visible{outline:2px solid var(--green);outline-offset:3px}

.fp-toggles{display:flex;flex-wrap:wrap;gap:6px}
.fp-chip{
  padding:7px 12px;border-radius:20px;border:1px solid var(--border);
  background:transparent;color:var(--muted);font-size:13px;font-weight:500;
  cursor:pointer;font-family:var(--font-sans);transition:all .15s;
}
.fp-chip:hover{color:var(--text);border-color:var(--green-dim)}
.fp-chip.active{background:var(--green-dark);border-color:var(--green-dark);color:var(--on-accent)}

/* Visible keyboard focus everywhere, this was missing entirely */
button:focus-visible, a:focus-visible, input:focus-visible,
select:focus-visible, textarea:focus-visible, [tabindex]:focus-visible{
  outline:2px solid var(--green);outline-offset:2px;border-radius:4px;
}

/* ══════════════════════════════════════════════════════════════
   EVENTS ON THE MAP
   ══════════════════════════════════════════════════════════════ */
.ev-pin{
  display:flex;align-items:center;justify-content:center;
  width:26px;height:26px;border-radius:50% 50% 50% 0;transform:rotate(-45deg);
  background:var(--panel);border:2px solid var(--amber);
  box-shadow:0 2px 7px rgba(0,0,0,.5);cursor:pointer;transition:transform .15s;
}
.ev-pin .ev-pin-icon{transform:rotate(45deg);font-size:13px;line-height:1}
.ev-pin:hover{transform:rotate(-45deg) scale(1.18)}
.ev-tonight{border-color:var(--live);box-shadow:0 2px 10px var(--live-glow)}

.ev-popup .leaflet-popup-content-wrapper{
  background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;
}
.ev-popup .leaflet-popup-tip{background:var(--panel);border:1px solid var(--border)}
.ev-popup-body{display:flex;flex-direction:column;gap:3px;min-width:170px;padding:2px}
.ev-popup-type{font-size:12px;text-transform:uppercase;letter-spacing:.08em;color:var(--muted);font-weight:700}
.ev-popup-title{font-size:15px;font-weight:700;color:var(--text);line-height:1.3}
.ev-popup-meta{font-size:13px;color:var(--muted)}
.ev-popup-cover{font-size:13px;font-weight:700;color:var(--amber);margin-top:2px}
.ev-popup-cover.free{color:var(--green)}
.ev-popup-link{
  margin-top:6px;font-size:13px;font-weight:600;color:var(--green);
  text-decoration:none;border-top:1px solid var(--border);padding-top:6px;
}
.ev-popup-link:hover{text-decoration:underline}

/* ══════════════════════════════════════════════════════════════════════════
   MAP MODE SWITCH
   Centred at the top of the map under the header, because it changes what the
   whole map means and that is not a corner-of-the-screen decision.
   ══════════════════════════════════════════════════════════════════════════ */
.map-mode{
  /* The fallback, for the moment before seatMapMode() runs and for the case
     where the map never comes up at all. --header-h is a nominal 56px and the
     header is followed by the brass rail, the price bar and the knot rule, so
     this is only ever approximately right, which is the whole reason for the
     rule below. */
  position:fixed;top:calc(var(--header-h) + 10px);left:50%;
  transform:translateX(-50%);
  z-index:var(--z-map);
  display:flex;gap:2px;padding:3px;
  background:color-mix(in srgb, var(--panel) 92%, transparent);
  border:1px solid var(--border);border-radius:var(--r-full);
  box-shadow:var(--shadow-2, 0 4px 16px rgba(0,0,0,.3));
}
/* Seated inside the Leaflet container by seatMapMode(), where the legend
   already lives, so "10px down" means 10px from the top of the map rather than
   a guess at how tall the banner above it happens to be. */
.map-mode.is-seated{position:absolute;top:10px}

@supports (backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px)){
  .map-mode{-webkit-backdrop-filter:saturate(180%) blur(14px);backdrop-filter:saturate(180%) blur(14px)}
}
.map-mode-btn{
  display:flex;align-items:center;gap:6px;
  min-height:38px;padding:0 15px;
  background:transparent;border:none;border-radius:var(--r-full);
  color:var(--text-2);font-family:var(--font-sans);
  font-size:var(--t-base);font-weight:600;white-space:nowrap;cursor:pointer;
  transition:background var(--dur-fast) ease, color var(--dur-fast) ease;
}
.map-mode-btn.active{background:var(--accent-solid);color:var(--on-accent);font-weight:700}
@media (hover:hover){
  .map-mode-btn:not(.active):hover{background:var(--hover);color:var(--text)}
}
.map-mode-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){ .map-mode-btn{transition:none} }

/* On a narrow phone the switch shares the row with Leaflet's zoom controls
   (top left) and the legend (top right), so it gives up its emoji first. */
@media (max-width:420px){
  .map-mode-btn{padding:0 12px;font-size:var(--t-sm)}
  .map-mode-btn span{display:none}
}
/* During a crawl the map is a route, not a browse, the switch would only be
   another thing between the user and the next pub. */
.crawl-active .map-mode{opacity:0;pointer-events:none}

/* ── REPORT A CLOSURE ──
   Square, quiet, and beside the ＋ that adds a pub, because it is the same
   sentence in the other direction. Amber rather than red: this is a report
   about a pub, not a warning to the person pressing it, and red on a map that
   uses red for "dear pint" would be a third meaning for the colour. */
.closed-map-btn{
  width:38px;height:38px;border-radius:10px;flex-shrink:0;
  background:rgba(var(--warn-rgb),.10);
  border:1px solid var(--amber);color:var(--amber);
  font-size:16px;line-height:1;cursor:pointer;transition:filter .16s;
  display:flex;align-items:center;justify-content:center;
}
@media (hover:hover){
  .closed-map-btn:hover{filter:brightness(1.25)}
}
.closed-map-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
@media (prefers-reduced-motion:reduce){
  .closed-map-btn{transition:none}
}

/* ── THE PUB SEARCH INSIDE THE CLOSURE REPORT ──
   A list under a text field, not a datalist: the address is what tells you
   which Murphy's you have got, and a datalist option cannot carry two lines.
   Capped in height because "The" matches four hundred pubs. */
.closed-results{
  margin-top:6px;max-height:210px;overflow-y:auto;
  border:1px solid var(--border);border-radius:10px;background:var(--panel-2);
  -webkit-overflow-scrolling:touch;
}
.closed-hit{
  display:flex;flex-direction:column;gap:1px;width:100%;
  padding:9px 12px;background:none;border:none;border-bottom:1px solid var(--border);
  text-align:left;cursor:pointer;font-family:var(--font-sans);
}
.closed-hit:last-child{border-bottom:none}
.closed-hit:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
@media (hover:hover){ .closed-hit:hover{background:var(--hover)} }
.closed-hit-name{font-size:var(--t-base);font-weight:600;color:var(--text)}
.closed-hit-where{font-size:var(--t-sm);color:var(--text-3)}
.closed-none{margin:0;padding:11px 12px;font-size:var(--t-sm);color:var(--text-3);line-height:1.45}
.closed-picked{
  margin-top:8px;padding:8px 11px;border-radius:10px;
  background:rgba(var(--brand-rgb),.10);border:1px solid rgba(var(--brand-rgb),.30);
  color:var(--accent);font-size:var(--t-sm);font-weight:600;line-height:1.4;
}
.closed-done-btn{
  margin-top:20px;background:rgba(var(--brand-rgb),0.15);
  border:1px solid rgba(var(--brand-rgb),0.3);color:var(--accent);
  border-radius:12px;padding:10px 24px;
  font-family:var(--font-sans);font-size:15px;font-weight:600;cursor:pointer;
}

/* Events listed inside a pub sheet */
.sheet-events{display:flex;flex-direction:column;gap:5px;margin-bottom:6px}
.sheet-events-empty{margin-bottom:10px}
.sheet-events-empty p{
  margin:4px 0 0;font-size:var(--t-base);line-height:1.5;color:var(--text-3);
}
.sheet-events-empty a{color:var(--accent);text-decoration:none;font-weight:600}
.sheet-events-empty a:hover{text-decoration:underline}
.sheet-event-row{
  display:flex;align-items:center;gap:10px;padding:9px 11px;border-radius:9px;
  background:var(--panel2);text-decoration:none;transition:all .15s;
  border:1px solid transparent;
}
.sheet-event-row:hover{border-color:var(--amber)}
.sheet-event-icon{font-size:17px;flex-shrink:0}
.sheet-event-info{display:flex;flex-direction:column;gap:1px;flex:1;min-width:0}
.sheet-event-title{font-size:14px;font-weight:600;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.sheet-event-when{font-size:13px;color:var(--muted)}
.sheet-event-arrow{color:var(--muted);font-size:17px;flex-shrink:0}

/* When the install banner is up, the receipt sits above it rather than under */
body:has(#pwa-banner.show) .contrib-receipt,
body:has(#pwa-banner[style*="display: flex"]) .contrib-receipt{ bottom:calc(var(--tabbar-safe) + 104px); }

/* ══════════════════════════════════════════════════════════════════════════
   MOTION SYSTEM
   One set of curves used everywhere, so the whole app moves like one object.
   --ease-out is the iOS sheet curve: fast start, long settle. --spring
   overshoots slightly and is reserved for things that should feel physical.
   ══════════════════════════════════════════════════════════════════════════ */
/* ══════════════════════════════════════════════════════════════════════════
   BOTTOM NAV: map furniture only

   The bar itself now lives in shared.css. It had to: the launcher needs the
   same nav and does not load this file, and a second copy of it in home.css
   is how two navigations drift apart.

   What stays here is the map's own furniture getting out of its way.
   ══════════════════════════════════════════════════════════════════════════ */

/* Floating map furniture clears the bar. --tabbar-safe rather than --tab-h so
   the same rule is right on a page that has no bottom nav.

   AND IT CLEARS THE ATTRIBUTION.

   The other thing at the foot of the map is the line that says whose map it
   is, Leaflet, OpenFreeMap, OpenMapTiles, OpenStreetMap. Leaflet draws it at
   the map's own bottom edge, which is the top of the tab bar, so everything
   here was measuring from a floor that already had something standing on it.
   At 12px the button row's lower 9 pixels sat across the top of the letters;
   at 320px, where the line wraps to two, it covered nearly all of it.

   That line is a condition of using the tiles, not decoration. --map-attrib-h
   is the box's measured height, published by trackMapAttribution() in
   index.js, and it is measured rather than assumed because the wrap point
   depends on the width, the font and the theme. It falls back to 0px, so a
   page where the script has not run yet is exactly where it was. */
/* 6px, not 12. The clearance that has to be there is the attribution's own
   height; the gap on top of it is taste, and at 12 the row sat noticeably high
   on Android, where the tab bar, the credit line and the system gesture bar
   are already three things stacked under it. Six still reads as a gap. */
.map-btn-row{bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px) + 6px)!important}
.contrib-receipt{bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px) + 14px)}
/* A nudge card occupies the same strip. Whichever one is up, the receipt goes
   above it rather than under it, it is the one confirming something the user
   just did, so it is the one that must be readable. */
body:has(.nudge.show) .contrib-receipt{
  bottom:calc(var(--tabbar-safe) + 150px);
}
/* The filter summary bar takes the strip directly above the nav when it is up,
   so everything that floats there steps over it rather than onto it. */
body:has(.filter-bar.show) .map-btn-row{bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px) + 52px)!important}
body:has(.filter-bar.show) .contrib-receipt{bottom:calc(var(--tabbar-safe) + var(--map-attrib-h, 0px) + 54px)}

/* ══════════════════════════════════════════════════════════════════════════
   MOBILE REFINEMENTS
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width:600px){

  header{padding:calc(6px + env(safe-area-inset-top)) 12px 6px;gap:10px}

  /* Price bar, the most valuable strip on a phone */
  .price-stats-bar{padding:0 8px;scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;--psb-h:69px}
  .psb-stat{scroll-snap-align:start;padding:8px 12px;min-height:44px}
  .psb-value{font-size:17px}
  .psb-label{font-size:13px}
  .psb-sub{max-width:88px}
  /* `white-space:normal` was here so a long sentence could wrap. It is gone:
     a bar that changes height as the sentence changes is the bug. */
  .psb-empty{font-size:14px;padding:9px 8px}
  .psb-cta{min-height:36px}

  /* Pins, bigger tap area, same visual weight */
  .pin-wrap{touch-action:manipulation}
  .pin-priced{min-height:26px}
  .pin:active{transform:scale(.92)}

  /* Cheapest panel as a true bottom sheet. It now outranks the nav rather than
     sitting under it, so it slides over the bar instead of reserving 58px of
     empty space above one it could not cover. */
  .cheapest-panel{
    height:86dvh;
    padding-bottom:max(12px, env(safe-area-inset-bottom));
    box-shadow:0 -8px 40px rgba(0,0,0,.5);
  }
  .cheapest-head{padding:6px 16px 12px}
  .cheapest-row{padding:12px;min-height:60px}
  .cheapest-name{font-size:15px}
  .cheapest-price{font-size:20px}
  .cheapest-sort-btn{min-height:44px}

  /* Pub sheet, bigger type, bigger targets */
  .price-hero-num{font-size:34px}
  .price-hero-block{padding:15px 16px}
  .btn-visited,.btn-crawl,.btn-fave,.btn-checkin,.btn-sheet-action{min-height:48px;font-size:15px}
  .sheet-stars span{font-size:26px;padding:2px}
  .sheet-event-row{min-height:52px}
  .cprice-row{padding:10px 12px;min-height:44px}

  /* Filters */
  .fp-band{min-height:48px;font-size:15px}
  .fp-chip{min-height:44px;padding:9px 14px;font-size:14px}
  .fp-max-slider{height:6px;margin-bottom:18px}
  .fp-max-slider::-webkit-slider-thumb{width:26px;height:26px}
  .fp-max-slider::-moz-range-thumb{width:26px;height:26px}

}

/* ══════════════════════════════════════════════════════════════════════════
   SHEET DRAG HANDLES
   ══════════════════════════════════════════════════════════════════════════ */
.sheet-grab{
  flex-shrink:0;display:none;padding:9px 0 3px;cursor:grab;
  touch-action:none;
}
.sheet-grab::before{
  content:"";display:block;width:38px;height:4px;margin:0 auto;
  background:var(--border);border-radius:99px;
  transition:background var(--dur-fast) ease,width var(--dur-fast) ease;
}
.sheet-grab:active::before{background:var(--muted);width:48px}
@media (max-width:600px){ .sheet-grab{display:block} }

/* While a finger is on the sheet, kill the transition so it tracks 1:1 */
.dragging{transition:none!important}

/* ══════════════════════════════════════════════════════════════════════════
   INTERACTION POLISH: all sizes
   ══════════════════════════════════════════════════════════════════════════ */
button,a,.tab,.pin,.cheapest-row,.filter-tag,.fp-band,.fp-chip{
  -webkit-tap-highlight-color:transparent;
}

button{
  transition:transform var(--dur-fast) var(--ease-out),
             background-color var(--dur-fast) ease,
             border-color var(--dur-fast) ease,
             color var(--dur-fast) ease;
}
button:active:not(:disabled){transform:scale(.975)}

/* Staggered list entrance, --i is set inline by the renderer */
@keyframes rowIn{
  from{opacity:0;transform:translateY(9px)}
  to  {opacity:1;transform:none}
}
.cheapest-row{
  animation:rowIn var(--dur) var(--ease-out) both;
  animation-delay:calc(var(--i,0) * 22ms);
}
.cheapest-row:active{transform:scale(.985)}

.cheapest-panel{transition:transform var(--dur-slow) var(--ease-out)}
.cheapest-backdrop{transition:opacity var(--dur) ease}

/* A number that changed should look like it changed */
@keyframes tick{
  0%  {transform:translateY(0)}
  40% {transform:translateY(-4px);opacity:.55}
  100%{transform:translateY(0)}
}
.psb-value.updated,.price-hero-num.updated{animation:tick var(--dur-slow) var(--ease-out)}

/* == NEW PINS LAND RATHER THAN APPEAR ==
   Scoped to .pin-fresh, which addPubMarker puts on a marker the one time it is
   created. Unscoped, this replayed on every setIcon - so crossing the price
   label threshold, which continuous zooming does repeatedly while you hover on
   it, re-ran a 250ms fade-and-drop on every priced pin on screen at once. That
   is the flicker. The class cannot survive a rebuild (setIcon replaces the
   element), which is exactly the property that makes it the right hook. */
@keyframes pinIn{
  from{opacity:0;transform:translateY(-6px) scale(.8)}
  to  {opacity:1;transform:none}
}
.pin-fresh .pin-priced{animation:pinIn var(--dur) var(--ease-out) both}

/* ── SKELETONS ─────────────────────────────────────────────────────────────
   A price map that pops in from blank reads as broken; one that resolves from
   placeholders reads as fast. */
@keyframes shimmer{ from{background-position:200% 0} to{background-position:-200% 0} }
.skeleton{
  background:linear-gradient(90deg,
    var(--panel2) 25%,
    var(--border) 37%,
    var(--panel2) 63%);
  background-size:200% 100%;
  animation:shimmer 1.3s linear infinite;
  border-radius:8px;
}
.sk-row{display:flex;align-items:center;gap:11px;padding:11px 12px}
.sk-dot{width:22px;height:22px;border-radius:50%;flex-shrink:0}
.sk-lines{flex:1;display:flex;flex-direction:column;gap:6px}
.sk-line{height:11px}
.sk-line.short{width:45%}
.sk-price{width:54px;height:19px;flex-shrink:0}

@media (prefers-reduced-motion:reduce){
  *,*::before,*::after{
    animation-duration:.01ms!important;
    animation-iteration-count:1!important;
    transition-duration:.01ms!important;
    scroll-behavior:auto!important;
  }
  button:active:not(:disabled){transform:none}
}

/* ══════════════════════════════════════════════════════════════════════════
   PUB SHEET
   Was a block of hardcoded dark hex values in a style attribute, so it stayed
   dark in light mode. Now token-driven and themed correctly in both.

   ── Why this is all in one place now ──
   The sheet's geometry used to be set in three separate blocks scattered
   through this file: a `min-width:1100px` dialog block up top, a
   `min-width:600px` `#pub-sheet-wrap` block right after it, and the base rules
   down here. All three set `transform`, and the two up top fought the ones down
   here on source order rather than on intent.

   The laptop lost that fight. The 1100px block set `top:50%` and centred the
   sheet with `translate(-50%,-50%)`; the base `.pub-sheet-wrap` transform below
   it is later in the file at equal specificity, so it won and replaced the
   whole transform with `translateX(-50%) translateY(0)`. The `-50%` that pulls
   the sheet back up by half its own height was simply dropped, so on every
   window wider than 1100px the sheet's TOP edge sat on the vertical centre of
   the screen and the bottom 60-200px of it hung off the end of the display,
   with no scroll container to reach it. That is why the pub popup looked wrong
   on a laptop and right on a phone.

   So: one transform declaration, driven by two custom properties, and each
   breakpoint sets only the properties it cares about. A breakpoint can no
   longer half-overwrite the transform, because no breakpoint writes `transform`
   at all. `transform` still animates normally, it is the substituted value
   that transitions, not the variable.

   Three sizes, deliberately:
     < 600px   full-width bottom sheet   (phone: thumb reaches the bottom edge)
     600-1099  centred bottom sheet      (tablet: still a sheet, not full-bleed)
     >= 1100   centred dialog            (laptop: "slides up from the bottom"
                                          means nothing on a 1440px window)
   ══════════════════════════════════════════════════════════════════════════ */
.pub-sheet-backdrop{
  display:none;position:fixed;inset:0;z-index:var(--z-sheet);
  background:rgba(0,0,0,.55);opacity:0;pointer-events:none;
  transition:opacity var(--dur) ease;
}
.pub-sheet-backdrop.open{opacity:1}

.pub-sheet-wrap{
  /* --sx: horizontal centring offset. --sy: vertical offset when closed.
     --sy-open: vertical offset when open. Only these ever change per size. */
  --sx: 0;
  --sy: 100%;
  --sy-open: 0px;
  --sc: 1;
  position:fixed;bottom:0;left:0;right:0;z-index:calc(var(--z-sheet) + 1);width:100%;
  background:linear-gradient(180deg, var(--bg), var(--panel));
  border-top:1px solid var(--border);
  border-radius:22px 22px 0 0;
  max-height:80dvh;overflow-y:auto;-webkit-overflow-scrolling:touch;
  overscroll-behavior:contain;
  transform:translate(var(--sx), var(--sy)) scale(var(--sc));
  /* ── Closed means untouchable, not just invisible ──
     On a laptop this panel closes to opacity:0 while staying exactly where it
     was: a 540x530 box, position:fixed, over the middle of the map, at z-index
     3401. It hid from the eye and from nothing else. Every click on a pin under
     it went to the sheet instead, so the sheet never opened, and because the
     body still holds the last pub's markup, a click that happened to land on
     the invisible "Crawl" button silently added THAT pub to the crawl. Which is
     what it looked like from outside: pins stopped opening and started
     collecting themselves into a crawl.
     visibility also takes it out of the tab order, and its 0s transition is
     delayed by the length of the close so the fade still plays on the way out.
     Both are needed at every width, not just here: for the 400ms of the close
     animation the sheet is over the map at any size. */
  pointer-events:none;visibility:hidden;
  transition:transform var(--dur-slow) var(--ease-out), opacity var(--dur) ease,
             visibility 0s linear var(--dur-slow);
  padding-bottom:max(12px, env(safe-area-inset-bottom));
  box-shadow:0 -8px 40px rgba(0,0,0,.35);
}
.pub-sheet-wrap.open{
  transform:translate(var(--sx), var(--sy-open)) scale(var(--sc));
  pointer-events:auto;visibility:visible;transition-delay:0s;
}

/* ── TABLET: still a bottom sheet, but centred rather than full-bleed ── */
@media (min-width:600px){
  .pub-sheet-wrap{
    --sx:-50%;
    left:50%;right:auto;width:min(560px, 92vw);
    max-height:90dvh;
  }
  .pub-sheet-backdrop{background:rgba(0,0,0,.65)}
}

/* ── LAPTOP AND UP: a centred dialog, not a sheet ──
   `--sy-open:-50%` is the rule whose loss broke this at every width over
   1100px. It pulls the box up by half its own height to sit against top:50%. */
@media (min-width:1100px){
  .pub-sheet-wrap{
    --sx:-50%;
    --sy:calc(-50% + 14px);
    --sy-open:-50%;
    /* Starts noticeably small and lands at full size. .97 was a nudge nobody
       could see; .94 with a slight overshoot on the way out is what makes this
       read as a bubble popping rather than a panel fading in. */
    --sc:.94;
    top:50%;bottom:auto;
    /* Roomy rather than cramped. min() with a vw cap so a 1100px window, the
       exact width this breakpoint starts at, still gets margins either side
       instead of a card wedged against both edges. */
    width:min(760px, 88vw);
    max-height:min(920px, 90dvh);
    border-radius:24px;
    border:1px solid var(--border);
    box-shadow:0 24px 70px rgba(0,0,0,.5);
    opacity:0;
    /* Re-stating the transition here re-states the visibility delay with it,
       drop it and the panel would vanish instantly instead of fading out.
       The transform gets its own longer, overshooting curve; opacity keeps the
       plain one, because a fade that overshoots is a flicker. */
    transition:transform var(--dur-slow) var(--spring), opacity var(--dur) ease,
               visibility 0s linear var(--dur-slow);
  }
  .pub-sheet-wrap.open{--sc:1;opacity:1}
  /* More width means the content needs more gutter, or the text runs edge to
     edge and the extra room reads as an accident.
     Qualified with the wrapper for the same reason `.pub-sheet-grab` below is:
     the base rules for both of these sit LATER in this file at equal
     specificity, so an unqualified selector here would lose on source order. */
  .pub-sheet-wrap .pub-sheet-head{padding:10px 26px 14px}
  .pub-sheet-wrap #pub-sheet-body{padding:18px 26px 22px}
  /* Centred, it no longer needs the drag handle that says "pull me down", a
     pill on a dialog floating in the middle of a 1440px window reads as a
     leftover, and there is an × two inches to the right of it.
     Written as a descendant selector on purpose: the `.pub-sheet-grab{display:
     block}` that keeps the pill on phones and tablets sits LATER in this file,
     so an equal-specificity rule here would lose on source order and the pill
     would stay visible on desktop. Two classes beats one at any position. */
  .pub-sheet-wrap .pub-sheet-grab{display:none}
}
@media (min-width:1100px) and (prefers-reduced-motion:reduce){
  .pub-sheet-wrap{--sy:-50%;--sc:1;transition:opacity var(--dur) ease, visibility 0s linear var(--dur)}
}

.pub-sheet-head{
  display:flex;align-items:flex-start;justify-content:space-between;
  padding:8px 18px 12px;border-bottom:1px solid var(--border);
}
#pub-sheet-body{padding:14px 18px}
/* This sheet keeps its handle on desktop too, it is the affordance that says
   the panel can be dismissed. */
.pub-sheet-grab{display:block}

/* 599.98, not 600. `min-width:600px` above and `max-width:600px` here both
   match at exactly 600px, an iPad mini in portrait is 768, but a resized
   laptop window lands on 600 exactly often enough, and there the sheet got the
   centred-modal transform and the full-width max-height at the same time. */
@media (max-width:599.98px){
  /* Near enough the whole screen. The sliver of map left above it is doing a
     job, it is what says this is a sheet you can pull down, and not a page you
     have navigated to and have to find your way back from. */
  .pub-sheet-wrap{max-height:96dvh}
}

/* ══════════════════════════════════════════════════════════════════════════
   LANDSCAPE PHONES
   390x844 turns into 844x390, and a sheet at 88dvh of 390px is 343px of panel
   over 47px of map. At that height a bottom sheet is not a sheet, it is a
   page, so it gives back half the screen and scrolls instead.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-height:520px) and (orientation:landscape){
  .pub-sheet-wrap{max-height:92dvh}
  .cheapest-panel{height:100dvh}
  .crawl-panel{height:calc(100dvh - var(--kb-inset))}
  /* The onboarding and roulette dialogs are the two tallest things in the app
     and neither fits a 390px-high viewport without scrolling. */
  .roulette-card,
  .suggest-panel{max-height:calc(100dvh - 16px)}
  /* The auth modal's backdrop keeps its 20px inset (it is the one dialog you
     type into, and a hairline gap to the screen edge reads as cramped), so its
     cap has to subtract that inset and not the 8px the other two use. */
  .auth-modal-box{max-height:calc(100dvh - 40px);padding:20px}
  .map-mode.is-seated{top:4px}

  /* The passport is 510px of card. Scrolling the whole backdrop would work, but
     it would take the banner, and the ×, off the top of the screen with it.
     Pin the banner, scroll only the body. The tap nozzle is pure decoration and
     is the first thing to go when the screen is 390px tall. */
  .passport-backdrop{padding:8px}
  .tap-nozzle,.tap-stream{display:none}
  .passport-card{
    max-height:calc(100dvh - 16px);
    display:flex;flex-direction:column;
  }
  .passport-banner{flex:0 0 auto}
  .passport-inner{overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain;min-height:0}
  /* Two columns of stats rather than one tall column: in landscape the screen
     is short and wide, which is exactly the shape the grid should follow. */
  .passport-stats{grid-template-columns:repeat(4,1fr)}
  .pp-stat.wide{grid-column:1/-1}

  /* The all-cities panel already pins its header and scrolls its body at every
     size (see the OVERALL PASSPORT block); here it just needs the tighter
     margin, because 40px of padding is 10% of a 390px-tall screen. */
  .overall-passport-backdrop{padding:8px}
  .overall-passport-panel{max-height:calc(100dvh - 16px)}
  .overall-passport-header{padding:14px 18px}
  .overall-passport-body{padding:14px 18px 18px}
}

/* ══════════════════════════════════════════════════════════════════════════
   LEGEND: now explains the price bands, and collapses on phones
   ══════════════════════════════════════════════════════════════════════════ */
/* Top right, not bottom left. The bottom of the map already carries the
   Suggest / events / locate row, the contribution receipt and any nudge card;
   the legend was the fourth thing wanting that edge, and collapsed it was
   still a 185px labelled pill sitting over the middle of the pin cluster.
   Nothing else uses the top right, Leaflet's zoom controls are top left. */
.legend{
  position:absolute;top:10px;right:10px;z-index:900;
  background:var(--panel);border:1px solid var(--border);border-radius:12px;
  padding:0;font-size:13px;color:var(--muted);overflow:hidden;
  box-shadow:0 4px 16px rgba(0,0,0,.28);
  max-width:210px;
}
.legend-toggle{
  display:flex;align-items:center;justify-content:space-between;gap:10px;
  width:100%;padding:8px 12px;background:none;border:none;cursor:pointer;
  color:var(--text);font-family:var(--font-sans);
  font-size:13px;font-weight:700;text-transform:uppercase;letter-spacing:.09em;
  min-height:36px;
}
/* Collapsed, it is a 44px button showing three bars in the pins' own colours,
   a sample of the thing it explains rather than the word "PRICE" and a chevron.
   Expanded, the per-row swatches say it better, so the mini key hides. */
.legend-mini{display:none;flex-direction:column;gap:2px}
.legend-mini i{display:block;width:15px;height:3px;border-radius:2px}
.legend.collapsed .legend-mini{display:flex}
.legend.collapsed .legend-key,
.legend.collapsed .legend-chev{display:none}
/* An explicit width, not `auto`. The body is collapsed with max-height:0, which
   removes its height and not its intrinsic width, "Bands are relative to the
   city you're viewing" was still setting the box to 210px wide with nothing
   visible in it. overflow:hidden on .legend clips the rest. */
.legend.collapsed{width:44px;max-width:44px}
.legend.collapsed .legend-toggle{
  width:44px;height:44px;min-height:44px;padding:0;justify-content:center;
}
.legend-toggle:hover{background:var(--panel2)}
.legend-chev{
  font-size:13px;color:var(--muted);
  transition:transform var(--dur) var(--ease-out);
}
.legend.collapsed .legend-chev{transform:rotate(-90deg)}

.legend-body{
  display:flex;flex-direction:column;gap:6px;
  padding:2px 12px 11px;
  overflow:hidden;
  transition:max-height var(--dur) var(--ease-out),
             opacity var(--dur-fast) ease,
             padding var(--dur) var(--ease-out);
  /* The cap exists only so the collapse has something to animate to. It was
     200px, and the price key measures 240, four rows and a two-line note,
     so overflow:hidden was quietly cutting the last line of the note in half
     at every width. 400 is well clear of anything either key can grow to
     (that is nine more rows of headroom), and the transition is time-based,
     so the number costs nothing when it is not reached. */
  max-height:400px;opacity:1;
}
.legend.collapsed .legend-body{max-height:0;opacity:0;padding-top:0;padding-bottom:0}

.legend-row{display:flex;align-items:center;gap:8px;font-size:13px;line-height:1.3}
.legend-swatch{
  width:14px;height:11px;border-radius:3px;flex-shrink:0;
}
.lg-cheap{background:var(--price-cheap);border:1px solid var(--price-cheap-edge)}
.lg-mid  {background:var(--price-mid);  border:1px solid var(--price-mid-edge)}
.lg-dear {background:var(--price-dear); border:1px solid var(--price-dear-edge)}
/* Same grey as the pin it explains, and the same round shape, the key has to
   be a sample, not an approximation. */
.lg-none {
  background:var(--pin-none);border:1px solid var(--pin-none-ring);
  border-radius:50%;width:11px;height:11px;
}

/* ── THE TWO KEYS ──
   Only one is ever on screen: the whole point of having a map mode is that a
   colour means exactly one thing at a time, and a legend showing both keys at
   once would give away the thing the mode was introduced to protect. */
.legend-set{display:flex;flex-direction:column;gap:7px}
.legend-set-events{display:none}
.legend.legend-events .legend-set-price{display:none}
/* flex, not block: the gap above only applies to a flex box, so shown as a
   block the What's On key drew its three rows tighter than the price key's. */
.legend.legend-events .legend-set-events{display:flex}

.lg-ev-tonight{
  background:var(--accent-solid);border:1px solid var(--price-cheap-edge);
  border-radius:50%;width:12px;height:12px;
}
.lg-ev-week{
  background:var(--amber);border:1px solid var(--price-dear-edge);
  border-radius:50%;width:12px;height:12px;
}
.lg-ev-none{
  background:var(--muted);border:1px solid var(--border-strong);
  border-radius:50%;width:10px;height:10px;opacity:.5;
}
/* The collapsed button samples whichever key is live, so the corner chip is
   never explaining the other mode. */
.legend.legend-events .legend-mini i.lg-cheap{background:var(--accent-solid)}
.legend.legend-events .legend-mini i.lg-mid{background:var(--amber)}
.legend.legend-events .legend-mini i.lg-dear{background:var(--muted);opacity:.5}

.legend-note{
  font-size:12px;line-height:1.4;color:var(--muted);opacity:.75;
  margin-top:2px;max-width:24ch;
}

@media (max-width:600px){
  .legend{max-width:calc(100vw - 20px)}
  .legend-body{padding:2px 10px 10px}
  /* Open, the key is 210px wide and the mode switch is centred across the same
     row, and the switch sits a layer above it, so the top of the key was
     simply not there. Collapsed it is a 44px chip and fits in the corner
     beside the switch; open, it drops underneath it. 62px clears the switch
     (10px down, 46px tall) with room to spare, and the short-screen rule that
     moves the switch to top:4px only ever increases that gap. */
  .legend:not(.collapsed){top:62px}
}
/* Not a jump: the legend slides down as it opens, so the two moves read as one
   gesture. */
.legend{transition:top var(--dur) var(--ease-out)}


/* The header toolbar that used to live here is gone: its seven buttons are
   now chips on the action rail. ~44 rules removed, none of them reachable
   from any page after the markup changed. */

/* ═══════════════════════════════════════════════════════════════════════════
   HEADER SHELL
   ═══════════════════════════════════════════════════════════════════════════ */
header{
  background:var(--panel);
  border-bottom:1px solid var(--border);
  gap:var(--s-2);
}
.logo h1{
  font-family:var(--font-display);
  font-size:var(--t-xl);font-weight:700;letter-spacing:-.015em;
  color:var(--text);
}
.logo p{font-size:var(--t-xs);color:var(--text-3);font-weight:400}

.city-selector{
  background:var(--panel-2);
  border:1px solid var(--border);
  border-radius:var(--r-md);
  transition:border-color var(--dur-fast) ease;
}
.city-selector:hover{border-color:var(--border-strong)}
.city-select{
  background:transparent;border:none;color:var(--text);
  font-family:var(--font-sans);font-size:var(--t-base);font-weight:600;
  cursor:pointer;
}

/* The header progress ring duplicated the Passport panel and was removed. */

.auth-btn{
  border-radius:var(--r-md);font-family:var(--font-sans);
  font-size:var(--t-sm);font-weight:600;padding:8px 14px;min-height:38px;
  border:1px solid var(--border);
  transition:all var(--dur-fast) ease;
}
.btn-login{background:transparent;color:var(--text-2)}
.btn-login:hover{color:var(--text);border-color:var(--border-strong);background:var(--hover)}
.btn-signup{background:var(--accent-solid);border-color:var(--accent-solid);color:var(--on-accent);font-weight:700}
.btn-signup:hover{background:var(--brand-500);border-color:var(--brand-500)}

.theme-toggle{
  width:38px;height:38px;border-radius:var(--r-md);
  background:var(--panel-2);border:1px solid var(--border);
  display:flex;align-items:center;justify-content:center;font-size:15px;
  cursor:pointer;transition:all var(--dur-fast) ease;
}
.theme-toggle:hover{background:var(--hover);border-color:var(--border-strong)}

.user-chip{
  background:var(--panel-2);border:1px solid var(--border);
  border-radius:var(--r-full);padding:5px 12px;
  font-size:var(--t-sm);color:var(--text-2);
}
.user-chip:hover{border-color:var(--border-strong)}
.user-avatar{background:var(--accent-solid);color:var(--on-accent)}

/* ═══════════════════════════════════════════════════════════════════════════
   SEARCH
   ═══════════════════════════════════════════════════════════════════════════ */
.map-search-wrap{background:var(--panel);border-bottom:1px solid var(--border)}
.map-search-inner{
  background:var(--panel-2);border:1px solid var(--border);
  border-radius:var(--r-md);
  transition:border-color var(--dur-fast) ease, box-shadow var(--dur-fast) ease;
}
.map-search-inner:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--brand-ring)}
.map-search-input{background:transparent;border:none;color:var(--text);font-size:15px}
.map-search-input::placeholder{color:var(--text-3)}
.map-search-results{
  background:var(--panel);border:1px solid var(--border);
  border-radius:var(--r-md);box-shadow:var(--shadow-3);
}


/* ══════════════════════════════════════════════════════════════════════════
   MAP FURNITURE: mobile layout
   The legend used to be stacked above this row because both wanted the bottom
   of the screen. It has moved to the top right, so this row gets the whole
   bottom edge to itself.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width:600px){
  .map-btn-row{
    left:10px;right:10px;
    display:flex;align-items:center;gap:8px;
    bottom:calc(var(--tab-h) + env(safe-area-inset-bottom) + var(--map-attrib-h, 0px) + 6px)!important;
  }
  /* No room for a third row of chrome over the map, see .app-footer above. */
  .app-footer{display:none}
  .suggest-map-btn{flex:1;min-height:44px;justify-content:center}
  .locate-btn,.closed-map-btn{width:44px;height:44px;flex-shrink:0}
}


/* ══════════════════════════════════════════════════════════════════════════
   HEADER SHRINK ORDER
   These live here rather than in shared.css because index.css loads last and
   the base .counter / .logo rules are defined in this file, a display:none
   in shared.css lost to them on source order.

   Order of sacrifice as the window narrows: the tagline, then the visit
   counter (it is repeated in the passport), then the wordmark. The search
   field never gives up room, it is the only control on the row with a job.
   ══════════════════════════════════════════════════════════════════════════ */
@media (max-width:1240px){
  .logo p{display:none}
}
@media (max-width:1180px){
  .counter{display:none!important}
}
@media (max-width:820px){
  .logo h1{font-size:var(--t-base)}
}


/* ═══════════════════════════════════════════════════════════════════════════
   HEADER
   ═══════════════════════════════════════════════════════════════════════════

   One row, three things:

       [ logo ]  [ ──── search ──── ]  [ account ]

   The row before this one had five, and the two that are gone, a city pill
   and a light/dark toggle, were the two that made it impossible. Between them
   they took ~250px of fixed width, which on any phone left search too little
   room to be usable, so search wrapped to a bar of its own. That bar cost 46px
   of a 844px screen, permanently, to work around two controls that are touched
   about once a month each.

   The city pill's job moved into search (type a town name). The toggle's job
   moved behind the account button. Search now fits beside the logo at 360px,
   and the app is one bar shorter everywhere.
   ═══════════════════════════════════════════════════════════════════════════ */

/* The z-index is above the nav's deliberately. This header is `relative` in a
   flex column, so it never overlaps the map, but it is the stacking context
   the account menu and the search suggestions drop out of, and a context below
   the nav means a long dropdown on a landscape phone gets its last two rows
   cut off by a bar at the other end of the screen. */
header{
  display:flex;align-items:center;gap:var(--s-3);
  padding:9px 14px;
  position:relative;z-index:calc(var(--z-tabbar) + 10);
}
.logo{
  display:flex;align-items:center;gap:10px;flex-shrink:0;min-width:0;
  text-decoration:none;color:inherit;
}
.logo svg{flex-shrink:0}
/* The logo is a raster mark now. Explicit box and a radius matching the
   tile inside it, or a 192px icon drawn at 38 shows the PNG's own square
   corners against the header. */
.logo-mark{width:38px;height:38px;flex-shrink:0;border-radius:10px;display:block}
.logo:hover h1{color:var(--accent)}
.logo h1{transition:color var(--dur-fast) ease}

/* The city <select> survives as state, not as chrome. An earlier
   `.city-selector{display:flex}` outranks the UA stylesheet's [hidden] rule,
   so hiding it takes a selector of its own. */
.city-selector[hidden]{display:none}

.header-right{
  display:flex;align-items:center;gap:8px;flex-shrink:0;
}
.auth-guest{display:flex;gap:8px}
.auth-guest[hidden]{display:none}

/* ── The account button ────────────────────────────────────────────────────
   Deliberately the same 38px square as every other control that used to sit
   here, so the row's rhythm survived losing two of them. */
.avatar-btn{
  width:38px;height:38px;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  background:var(--panel-2);border:1px solid var(--border);
  border-radius:var(--r-full);cursor:pointer;padding:0;
  transition:border-color var(--dur-fast) ease, background var(--dur-fast) ease;
}
.avatar-btn:hover{background:var(--hover);border-color:var(--border-strong)}
.avatar-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.user-avatar{
  width:28px;height:28px;border-radius:50%;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  font-size:14px;font-weight:700;position:relative;overflow:hidden;
  /* Signed out this is a quiet dot, not a fake profile bubble. */
  background:transparent;color:var(--text-3);
}
.user-avatar.is-signed-in{background:var(--accent-solid);color:var(--on-accent)}
/* A photo covers the whole circle, so it must not sit on the accent fill. */
.user-avatar.has-photo{background:transparent}


@media (max-width:600px){
  header{padding:8px 12px;gap:10px}
  /* Sign up is the first item inside the menu, and on a phone the standalone
     button costs search about 90px, a quarter of the row, to say the same
     thing twice. The row is logo, search, you. */
  .auth-guest{display:none}
}

/* ── Search, in the header ──────────────────────────────────────────────────
   It used to occupy a full-width strip of its own between the header and the
   map. Moving it into the header's empty middle gives roughly 60px of the
   viewport back to the thing people came for.

   No flex-wrap any more: with the city pill and the theme toggle gone there is
   nothing left to wrap, and allowing it meant a 40-character placeholder could
   still push search onto a second row on a narrow phone. It shares the row at
   360px now, or it is a bug. */
.map-search-wrap{
  flex:1 1 240px;min-width:0;
  padding:0!important;
  background:transparent!important;
  border-bottom:none!important;
  position:relative;z-index:900;
}
.map-search-inner{
  display:flex;align-items:center;gap:9px;
  padding:0 13px;height:38px;
  border-radius:var(--r-full);
}
.map-search-input{flex:1;min-width:0;height:100%;font-size:var(--t-base)}
.map-search-icon{flex-shrink:0}
.map-search-results{
  position:absolute;top:calc(100% + 6px);left:0;right:0;z-index:1500;
  max-height:min(52vh,420px);overflow-y:auto;
}

@media (max-width:600px){
  /* The tagline and the wordmark both go: at this width the pin mark alone is
     the brand, and every pixel it gives back goes to the search field, which is
     the only thing on this row anyone is trying to use. */
  .logo h1{display:none}
  .logo{gap:0}
}


/* ── Trimming the chrome above the map ─────────────────────────────────────
   Four stacked bars pushed the map down to 70% of the viewport. Two of them
   are now gone, search joined the header, and the chip rail was deleted as
   duplication of the bottom bar. The map is the product; everything above it
   is overhead and is sized accordingly. */
.logo svg{width:34px;height:34px}
.logo h1{line-height:1.15}
header{padding:8px 14px}
.knot-rule{height:8px;-webkit-mask-size:28px 8px;mask-size:28px 8px}

/* ── Pin contrast ──────────────────────────────────────────────────────────
   Unpriced venues were drawn at 45-50% opacity. On the parchment basemap that
   put them at roughly 1.8:1 against the ground, invisible on a phone held
   outdoors, which is exactly where this app gets used. Solid fill and a real
   halo instead; the colour itself now comes from --pin-none, which is defined
   per theme in shared.css. This block only supplies the separation. */
.pin-unpriced{
  border-width:2px;
  box-shadow:0 1px 3px rgba(0,0,0,.5);
}
:root[data-theme="light"] .pin-unpriced{
  box-shadow:0 1px 3px rgba(20,40,24,.45);
}

/* Panel-open state. The crawl, filter and passport code toggles .active on
   these controls by their original ids, the ids moved onto the tabs, so the
   tab bar is what renders that state now. */
.tab.active{color:var(--accent)}


/* ── ALL IRELAND PASSPORT: everywhere you have not been yet ──
   The collection is the grid above. This is the rest of the country, and it
   is a long list, so it opens on request and grows sixty towns at a time. */
.op-more-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:12px;
  flex-wrap:wrap;margin:22px 0 10px;
}
.op-more-head[hidden]{display:none}
.op-more-title{
  font-size:var(--t-2xs);font-weight:800;letter-spacing:.09em;
  text-transform:uppercase;color:var(--text-3);
}
.op-more-note{font-size:var(--t-2xs);color:var(--text-3);opacity:.8;
  font-variant-numeric:tabular-nums}
.op-more-btn{
  display:block;width:100%;max-width:300px;margin:16px auto 4px;
  min-height:44px;padding:0 18px;
  background:transparent;border:1px solid var(--border);border-radius:var(--r-full);
  color:var(--text-2);font:inherit;font-weight:700;cursor:pointer;
  transition:border-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.op-more-btn:hover{border-color:var(--accent);color:var(--text)}
.op-more-btn[hidden]{display:none}

/* A grid item's automatic minimum is its min-content, so a long town name
   pushed both passport grids wider than the panel and the whole thing scrolled
   sideways on a phone, "Carrick-on-Suir" and "0 / 774 pubs" are wider than
   half of a 358px card row. The name already ellipsises; this is what lets it. */
.op-city-card{min-width:0}
.op-city-count{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

  /* ── FRIENDS HERE NOW (028) ──────────────────────────────────────────────
     Above the buzz panel in the pub sheet, because a named friend outranks an
     anonymous count: "12 people checked in" tells you to expect a queue,
     "Aoife and Cian are here" tells you to go. */
  .sheet-live:empty{display:none}
  .fh-panel{
    display:flex;align-items:center;gap:12px;
    padding:12px 13px;border-radius:14px;
    background:var(--brand-ghost);
    border:1px solid var(--accent);
  }
  .fh-faces{display:flex;flex-shrink:0}
  .fh-face{
    width:30px;height:30px;border-radius:50%;
    display:flex;align-items:center;justify-content:center;
    background:var(--panel-2);
    /* The ring is the page's own ground, so a stack of faces reads as a stack
       rather than as one blurred shape. */
    border:2px solid var(--bg);
    font-size:11px;font-weight:700;color:var(--text-2);
    overflow:hidden;
  }
  .fh-face + .fh-face{margin-left:-9px}
  .fh-face img{width:100%;height:100%;object-fit:cover}
  .fh-text{font-size:14px;color:var(--text);line-height:1.35;min-width:0}
  .fh-text strong{color:var(--accent)}
  .fh-sub{display:block;font-size:12px;color:var(--text-3);margin-top:2px}

  /* ── THE SWITCH ──────────────────────────────────────────────────────────
     Under the action row, beside the button it governs, because this is a
     decision people change by situation rather than once in Settings. */
  .fh-toggle{
    display:flex;align-items:center;gap:10px;width:100%;
    min-height:44px;padding:8px 12px;margin-top:8px;
    background:none;border:1px solid var(--border);border-radius:12px;
    color:var(--text-2);font-family:var(--font-sans);
    font-size:13px;font-weight:600;text-align:left;cursor:pointer;
    transition:border-color var(--dur-fast) ease,color var(--dur-fast) ease;
  }
  .fh-toggle:hover{border-color:var(--border-strong);color:var(--text)}
  .fh-toggle:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  .fh-toggle-track{
    flex:none;width:36px;height:20px;border-radius:10px;
    background:var(--panel-2);border:1px solid var(--border-strong);
    position:relative;transition:background var(--dur-fast) ease,
                                 border-color var(--dur-fast) ease;
  }
  .fh-toggle-knob{
    position:absolute;top:2px;left:2px;width:14px;height:14px;border-radius:50%;
    background:var(--text-3);
    transition:transform var(--dur-fast) ease,background var(--dur-fast) ease;
  }
  .fh-toggle.is-on{border-color:var(--accent);color:var(--accent)}
  .fh-toggle.is-on .fh-toggle-track{background:var(--accent);border-color:var(--accent)}
  .fh-toggle.is-on .fh-toggle-knob{transform:translateX(16px);background:var(--on-accent)}

  @media (prefers-reduced-motion:reduce){
    .fh-toggle,.fh-toggle-track,.fh-toggle-knob{transition:none}
  }

/* ══════════════════════════════════════════════════════════════════════════
   THE MAP'S OWN CONTROLS

   Three buttons shared this strip and were styled as though the wrong two were
   a pair. "Suggest a pub" was a full-width bar, flex:1 on a phone, so it took
   whatever the other two left, while report-closed and locate were matched
   circles either side of nothing. That put the loudest control on the rarest
   action, and split the two buttons that actually belong together with an
   unrelated one wedged between them.

   What is true about them: two are contributions, this pin is missing, this
   pin is gone, and one is navigation. So the two contributions are one
   grouped control with a hairline between them, and locate is its own button.
   Structure carries the meaning; nothing here is decoration.

   Hierarchy comes from MATERIAL rather than size, which is the part worth
   keeping if this is ever reworked. The contribution group is glass, the same
   translucent, blurred surface the attribution uses, so it sits in the map and
   lets the streets through. Locate is solid, on the app's own panel colour with
   the accent ring. The frequent control reads as an object on top of the map;
   the rare ones recede into it. That is also, incidentally, the whole of the
   fix for "the suggest button doesn't need to be so big".
   ══════════════════════════════════════════════════════════════════════════ */

/* Right-aligned, content-width. `left` is unset so the row cannot stretch: the
   bottom-left of a map is where the eye goes to read the place names. */
.map-btn-row{
  left:auto;
  gap:10px;
}

/* The grouped pair. One surface, one border, one shadow, the divider between
   the halves is the inset box-shadow on the second child, so there is no extra
   element and no double hairline where the two meet. */
.map-contrib-group{
  display:flex;align-items:stretch;
  background:var(--glass);
  -webkit-backdrop-filter:blur(10px) saturate(1.1);
  backdrop-filter:blur(10px) saturate(1.1);
  border:1px solid var(--border);
  border-radius:var(--r-lg);
  overflow:hidden;
  box-shadow:0 2px 14px rgba(0,0,0,.28);
}

.suggest-map-btn,
.closed-map-btn{
  background:transparent;border:0;border-radius:0;
  color:var(--text-2);
  font-family:var(--font-sans);font-size:var(--t-sm);font-weight:600;
  height:38px;padding:0 13px;
  display:flex;align-items:center;justify-content:center;gap:7px;
  cursor:pointer;white-space:nowrap;
  transition:background var(--dur-fast) ease,color var(--dur-fast) ease;
}
/* The hairline, drawn by the button that follows one. */
.map-contrib-group > * + *{box-shadow:inset 1px 0 0 var(--border)}

/* Sized off the icon rather than off the label. `.ic` is 1em, and 1em here is
   the 13px of the button text, at which a pin with a plus inside it is a
   smudge. These two carry meaning the text does not repeat, so they get their
   own size. */
.suggest-map-btn .ic,
.closed-map-btn .ic{width:17px;height:17px}

.closed-map-btn{width:40px;padding:0;font-size:var(--t-base)}

@media (hover:hover){
  .suggest-map-btn:hover,
  .closed-map-btn:hover{background:var(--hover);color:var(--text)}
}
.suggest-map-btn:active,
.closed-map-btn:active{background:var(--hover)}
.suggest-map-btn:focus-visible,
.closed-map-btn:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}

/* The one accented control on the map, because it is the one people press.
   Solid, not glass: it belongs on top of the map rather than in it. */
.locate-btn{
  width:40px;height:40px;border-radius:50%;
  background:var(--panel);
  border:1px solid var(--brand-ring);
  color:var(--accent);
  font-size:var(--t-lg);line-height:1;cursor:pointer;
  display:flex;align-items:center;justify-content:center;flex-shrink:0;
  box-shadow:0 2px 14px rgba(0,0,0,.28);
  transition:transform var(--dur-fast) ease,border-color var(--dur-fast) ease,
             box-shadow var(--dur-fast) ease;
}
@media (hover:hover){
  .locate-btn:hover{
    border-color:var(--accent);
    box-shadow:0 2px 14px rgba(0,0,0,.28),0 0 0 4px var(--brand-ghost);
  }
}
.locate-btn:active{transform:scale(.93)}
.locate-btn:focus-visible{outline:2px solid var(--accent);outline-offset:2px}

@media (prefers-reduced-motion:reduce){
  .suggest-map-btn,.closed-map-btn,.locate-btn{transition:none}
  .locate-btn:active{transform:none}
}

/* On a phone the row keeps its shape and its right edge, it does not stretch,
   and the label does not disappear. A control that grows to fill the screen is
   the thing that made this look unconsidered. Touch targets stay at 44. */
@media (max-width:600px){
  .map-btn-row{left:auto;right:10px;gap:10px}
  .suggest-map-btn{flex:0 0 auto;height:44px;padding:0 15px}
  .closed-map-btn{width:46px;height:44px}
  .locate-btn{width:44px;height:44px}
}

/* The narrowest phones still sold. The label is the first thing to go, not the
   button: the pair keeps its shape and the icons keep their meaning. */
@media (max-width:340px){
  .suggest-map-btn .suggest-map-label{display:none}
  .suggest-map-btn{width:46px;padding:0}
  /* With the label gone the icon is the only thing saying what this does, so
     it gets the room the words were using. */
  .suggest-map-btn .ic{width:21px;height:21px}
}

/* ══════════════════════════════════════════════════════════════════════════
   THE INSTALL NUDGE STANDS DOWN FOR A DIALOG

   --z-float is 9700 and --z-dialog is 5500, so the install banner, the iOS
   banner and the contribution receipt all drew on top of every modal in the
   app. On a phone, which is the only place the install nudge appears at all,
   it landed squarely on the Send button of the suggestion form, the closure
   report and the feedback form. The form looked broken rather than covered.

   Raising the dialogs instead would be the wrong way round: a receipt saying
   "your price is live" genuinely does belong over the map, and the nudge
   genuinely does belong over the map. Neither belongs over a form somebody is
   in the middle of filling in.

   display:none rather than opacity, because the nudge is shown by an inline
   style from JavaScript and this has to outrank it. The inline style is left
   alone, so it comes straight back when the dialog closes.
   ══════════════════════════════════════════════════════════════════════════ */
body:has(.suggest-backdrop.open) .nudge,
body:has(.auth-backdrop.open) .nudge,
body:has(.cheapest-backdrop.open) .nudge,
body:has(.overall-passport-backdrop.open) .nudge,
body:has(.passport-backdrop.open) .nudge,
body:has(.roulette-backdrop.open) .nudge,
body:has(.chart-backdrop.open) .nudge,
body:has(.suggest-backdrop.open) .contrib-receipt,
body:has(.auth-backdrop.open) .contrib-receipt{
  display:none!important;
}
