/* Shared app styles for the lookup (index.html) and constituency (pcon/) pages.
 *
 * These two pages had byte-identical <style> blocks – 120 lines, duplicated in
 * full – so every fix to the tiles, verdict badge, league modal or sauna list
 * had to be made twice, with nothing enforcing that it was. Extracted verbatim
 * on 14 Jul 2026; no rule was changed in the move.
 *
 * Colours and type come from tokens.css. Do not put hexes here – add a token.
 */

  *{box-sizing:border-box}
  body{margin:0;background:var(--bg);color:var(--ink);-webkit-font-smoothing:antialiased;
       font:var(--fs-ui)/1.5 var(--font)}
  /* The app was built as a fixed 480px phone column, which stranded it in a sea
     of black on anything wider. The single linear layout is deliberate (the
     result reads top to bottom as one story), so responsiveness here is one
     fluid column that grows to a comfortable reading width – no reflow into
     grids. 640px keeps body copy around 66 characters a line; the block still
     collapses to the viewport on phones. Map height and the full-screen league
     modal get their own desktop tuning below. */
  .wrap{max-width:640px;margin:0 auto;padding:26px 18px 60px}
  h1{font-size:var(--fs-xl);font-weight:700;letter-spacing:-1px;line-height:1.05;margin:0 0 14px}
  .sub{font-size:var(--fs-body);color:var(--muted);line-height:1.5;margin:0}
  .ast{color:var(--muted);text-decoration:none}

  /* position:relative anchors the place-search dropdown to the whole row –
     anchored to .field it inherits the field's mobile squeeze (~180px once
     Locate/dice/Look up take their share) and the rows can't breathe. */
  .search{display:flex;gap:10px;margin:20px 0 0;position:relative}
  .field{flex:1;display:flex;align-items:center;background:var(--card);
         border:1.5px solid var(--blue);border-radius:var(--r-lg);padding-right:6px;min-width:0}
  /* Right padding mirrors the field's own 6px so the locate icon sits centred
     between the text cutoff and the field edge – its gaps are (6 + geo pad) on
     both sides, at every width. Left keeps the full 12px text inset. */
  .field input{flex:1;min-width:0;background:none;border:0;color:var(--ink);font-size:var(--fs-ui);
               padding:13px 6px 13px 12px;outline:none}
  .field input::placeholder{color:var(--muted)}
  /* The postcode idiom, applied only while the input looks like one (.pcish,
     toggled by the place router) – place names display as typed. */
  .field input.pcish{text-transform:uppercase}
  .geo{display:inline-flex;align-items:center;gap:5px;background:none;border:0;color:var(--blue);
       font-size:var(--fs-small);cursor:pointer;white-space:nowrap;padding:6px 2px}
  .geo:disabled{color:var(--muted)} .geo svg{width:17px;height:17px;flex:none}
  .geo.iconly{padding:6px}
  #rand{padding:6px 4px} #rand svg{width:auto;height:30px}
  /* Below the desktop column the input is the scarce resource: with place
     names in play it must fit "Lower Clapton", not just "BN1 1AA", so
     Locate me collapses to its icon (the button keeps an aria-label) and
     takes a wider tap pad in exchange. display:none here only ever HIDES,
     so it cannot out-rank .hidden – the rule the desktop tuning block
     below guards. */
  @media (max-width: 699px){
    .geo-lbl{display:none}
    .geo{padding:6px 10px}
  }
  .go{background:var(--green);color:#fff;border:0;border-radius:var(--r-lg);font-size:var(--fs-ui);
      font-weight:600;padding:0 18px;cursor:pointer;white-space:nowrap}
  .go:active{transform:scale(.98)}
  .err{color:var(--red);font-size:var(--fs-small);margin:0 2px}
  .err:not(:empty){margin:8px 2px 0}

  /* Place-search dropdown (index.html place router): candidates, typed live or
     submitted. Overlays the content below the field in the house's flat chrome
     – card on black, hairline border, no shadows. Row highlight mirrors the
     league table's current-row treatment. z-index clears the page flow (0–2)
     and stays under the modal (100) and fullscreen map (200). */
  .suggest{position:absolute;top:calc(100% + 8px);left:0;right:0;z-index:50;
    background:var(--card);border:1px solid var(--border);border-radius:var(--r-lg);
    padding:4px;display:flex;flex-direction:column}
  .suggest button{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
    background:none;border:0;border-radius:var(--r-md);color:var(--ink);
    font-size:var(--fs-ui);font-family:inherit;text-align:left;
    padding:10px;cursor:pointer}
  .suggest button:hover,.suggest button.hl{background:var(--blue-soft)}
  /* The name is the row's primary claim: it never wraps or shrinks, the
     context column yields first (ellipsis) – on a phone "Lower Clapton"
     must not fold in two while "Greater London" sits whole. */
  .suggest .s-place{white-space:nowrap;flex-shrink:0}
  .suggest .s-ctx{color:var(--muted);font-size:var(--fs-small);white-space:nowrap;
    overflow:hidden;text-overflow:ellipsis}

  .loc{font-size:var(--fs-body);color:var(--muted);margin:16px 0 18px}
  .loc b{color:var(--ink)}
  .la-lbl{font-size:var(--fs-body);color:var(--muted);margin:0}
  .la-nm{font-size:var(--fs-xl);font-weight:600;letter-spacing:-.5px;line-height:1.1;margin:2px 0 8px}
  .rankline{font-size:var(--fs-body);color:var(--muted);margin:0} .rankline b{color:var(--ink)}
  .ranklink{background:none;border:0;color:var(--blue);font-weight:600;font-size:var(--fs-body);padding:0;
            cursor:pointer;text-decoration:underline;text-underline-offset:2px;font-family:inherit}
  .na{color:var(--muted);font-size:var(--fs-body);margin:10px 0 0}

  .tiles{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin:16px 0 0}
  .tile{background:var(--card);border:1px solid var(--border);border-radius:var(--r-xl);min-height:172px;
        display:grid;grid-template-rows:auto 1fr auto;justify-items:center;align-items:center;
        text-align:center;padding:20px 12px}
  .tlab{font-size:var(--fs-small);color:var(--muted)}
  .tnum{font-size:var(--fs-display);font-weight:700;letter-spacing:-1px;line-height:1;align-self:start;padding-top:6px}
  .tnum.na{font-size:var(--fs-xl);color:var(--muted);letter-spacing:0}
  .tsub{font-size:var(--fs-lg);color:var(--muted)}
  .na-note{font-size:var(--fs-small);color:var(--muted);line-height:1.5;margin:12px 2px 0}
  .tgap{font-size:var(--fs-small);color:var(--muted);margin-top:10px}
  .tgap .g{font-weight:600} .pos .g,.g.pos{color:var(--green)} .neg .g,.g.neg{color:var(--red)}
  .badge{margin-top:9px;font-size:var(--fs-micro);font-weight:600;letter-spacing:.06em;
         padding:3px 10px;border-radius:var(--r-pill);border:1px solid}
  .badge.pos{color:var(--green);border-color:var(--green)}
  .badge.neg{color:var(--red);border-color:var(--red)}
  .x{text-transform:none}

  /* Share button: a secondary (outlined) action under the verdict card. Green
     primary stays with .go; this sits quieter until hover. Turns green briefly
     on a successful clipboard copy (.copied) to confirm the desktop path. */
  /* PARKED (Mika, 8 Aug): "it's kinda needy and takes up vertical space".
     The button, its handler and the ?area= links it produces are all still
     live - this is one line to take back out when the share loop is worth
     asking for again. See docs/shareable_cards.md. */
  .sharerow{margin:14px 0 0;display:none}
  .share{width:100%;display:inline-flex;align-items:center;justify-content:center;gap:8px;
         background:var(--card);color:var(--ink);border:1px solid var(--border);
         border-radius:var(--r-lg);font:600 15px var(--font);padding:13px 16px;cursor:pointer;
         transition:border-color .15s,color .15s,transform .05s}
  .share:hover{border-color:var(--blue)}
  .share:active{transform:scale(.99)}
  .share:focus-visible{outline:2px solid var(--blue);outline-offset:2px}
  .share .share-ic{width:17px;height:17px;flex:none;color:var(--blue)}
  .share.copied{border-color:var(--green);color:var(--green)}
  .share.copied .share-ic{color:var(--green)}

  .surprise{background:var(--card);border:1px solid var(--border);border-radius:var(--r-xl);
            padding:15px 16px 12px;margin:12px 0 0}
  .sur-head{font-size:var(--fs-md);font-weight:600;letter-spacing:-.3px;line-height:1.25;margin:0 0 7px}
  .sur-lab{font-size:var(--fs-caption);color:var(--muted);line-height:1.45;margin:0 0 9px}
  /* The lead-in's "just like this" -> About link. Without this it fell to the
     UA default (blue underline, visited purple) - the only untokened colour
     on the card. Same pattern as .about a and .nl-items a. */
  .sur-lab a{color:var(--blue);text-decoration:none}
  .sur-note{font-size:var(--fs-caption);color:var(--muted);line-height:1.45;margin:8px 0 0}
  .sur-note b{color:var(--ink)}
  .sur-badge{display:inline-block;font-size:var(--fs-micro);font-weight:600;letter-spacing:.06em;
             padding:3px 10px;border-radius:var(--r-pill);border:1px solid;margin-top:9px}

  /* .mapcard IS the Leaflet map container – Leaflet adds .leaflet-container to
     this same element – so the old `.mapcard .leaflet-container` (a *descendant*
     selector) never matched, and the map fell back to Leaflet's default #ddd.
     That pale ground is what flashed as the slider zoomed the tiles. Style the
     element directly with a dark ground that matches the tiles, so gaps and the
     idle state read as map. font:inherit puts map text (attribution) in Inter. */
  /* 4:3, not a fixed height, so the tile keeps its shape at every column width
     (was 230px, which read as a letterbox once the column grew). The
     ResizeObserver in app.js re-fits the map whenever this box changes. */
  /* 4:5 – taller than wide. The tile was 4:3, which on a phone gave the map
     about a third of the fold and made every lookup a letterbox; portrait
     buys the ring room to breathe and the carousel still clears the fold.
     Held in a token so design/tools/marker-rig.js can sweep the shape
     against real rosters rather than have it argued about. */
  .mapcard{aspect-ratio:var(--map-ar,4/5);border-radius:var(--r-xl);border:1px solid var(--border);margin:12px 0 0;
           overflow:hidden;position:relative;z-index:0;background:var(--map-ground);font:inherit}
  /* The credit is required and stays; it just does not have to look like a
     sticker. Same chrome family as the map buttons and the tooltips -
     --map-chrome-bg behind a blur - with only the INNER corner rounded, so
     it reads as part of the tile's own edge rather than a box sitting on
     top of it. The other three corners are square because the tile's
     overflow:hidden already clips them to its radius.
     The links stay --muted and only earn --ink-dim on hover: legible when
     looked for, quiet when not. app.js drops Leaflet's own prefix. */
  .mapcard .leaflet-control-attribution{background:var(--map-chrome-bg);
           -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);
           color:var(--muted);font-size:9px;line-height:1.7;padding:0 6px;
           margin:0;border-top-left-radius:var(--r-sm)}
  .mapcard .leaflet-control-attribution a{color:var(--muted);text-decoration:none}
  .mapcard .leaflet-control-attribution a:hover{color:var(--ink-dim);text-decoration:underline}

  /* Full-screen map modal. #mapbox wraps the map tile + slider + nearest-saunas
     list inline; adding .fullscreen (JS) turns it into a near-fullscreen sheet
     that stacks: map (fills the top), slider, then the scrolling list – the same
     Leaflet map and same #list, restacked. JS toggles only the class; button
     visibility is CSS below. */
  #mapbox{position:relative}
  .mapbtn{position:absolute;top:10px;right:10px;z-index:2;width:34px;height:34px;cursor:pointer;
          border-radius:var(--r-circle);background:var(--map-chrome-bg);border:1px solid var(--border);
          color:var(--ink);display:flex;align-items:center;justify-content:center;
          -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
  .mapbtn svg{width:16px;height:16px}
  .mapbtn:active{transform:scale(.94)}
  /* Expand shows only once the map is up (map loses .hidden on first lookup) and
     only while inline – :has() and :not() keep this out of the JS. Close is the
     mirror: hidden by its .hidden class until .fullscreen out-specifies it.
     Both sit TOP-LEFT: they are the same control at two ends of one journey, so
     the button that opens the view and the button that leaves it want the same
     corner rather than swapping sides under your thumb. */
  #mapExpand{display:none;left:10px;right:auto}
  #mapbox:not(.fullscreen):has(#map:not(.hidden)) #mapExpand{display:flex}
  /* Radar switch: top-right on the tile, opposite expand. It is a setting for
     what the tile does, not a way in or out of anything, so it stays clear of
     that corner. Gated by .radarable – app.js sets it only when a sweep is
     possible for this lookup (not an area card, motion allowed), because a
     toggle for something that cannot run is worse than no toggle. Off dims the
     button and strikes the icon through; the sweep line goes with it. */
  #mapRadar{display:none}
  #mapbox:not(.fullscreen).radarable:has(#map:not(.hidden)) #mapRadar{display:flex}
  #mapRadar .rslash{display:none}
  #mapRadar[aria-pressed="false"]{color:var(--muted)}
  #mapRadar[aria-pressed="false"] .rsweep{display:none}
  #mapRadar[aria-pressed="false"] .rslash{display:block}
  /* The icon turns while the sweep does. The button's job is to say what the
     map is doing, and a still icon over a turning map is the same lie as an
     unstruck icon over a stopped one. transform-box:view-box so the origin
     is the icon's own centre (12,12), not the path's bounding box – a line
     drawn from the centre outwards would otherwise pivot about its middle
     and wobble. Reduced motion keeps the still icon: the state is already
     carried by the strike, the colour and aria-pressed. */
  @media (prefers-reduced-motion:no-preference){
    #mapRadar[aria-pressed="true"] .rsweep{transform-box:view-box;
             transform-origin:12px 12px;animation:radar-icon 3.6s linear infinite}
  }
  @keyframes radar-icon{to{transform:rotate(360deg)}}

  /* ON/OFF for when the icon alone is not enough. The ICON keeps its circle -
     it is one of a set of round map buttons and should not become the odd
     one out - so the chrome moves off the button and onto .mapbtn-ic, and
     the word sits OUTSIDE that circle. The button itself goes transparent
     and grows to hold both, which keeps them one hit target: the word is
     part of the switch, it just is not inside the disc.

     Bare text over a map needs the halo the map labels use, since it no
     longer has a box behind it. No colour of its own: it inherits the
     button's, already --ink when the sweep runs and --muted when it does
     not, so the word and the icon can never disagree. aria-hidden in the
     markup - the button announces its own state, and this would say it
     twice. Opt-in (#mapbox.radarlabel, set by app.js) while it is judged. */
  #radarState{display:none}
  .mapbtn-ic{display:flex;align-items:center;justify-content:center;flex:none}
  #mapbox.radarlabel #mapRadar{width:auto;padding:0;gap:6px;background:none;
        border:0;border-radius:0;-webkit-backdrop-filter:none;backdrop-filter:none}
  #mapbox.radarlabel #mapRadar .mapbtn-ic{width:34px;height:34px;
        border-radius:var(--r-circle);background:var(--map-chrome-bg);
        border:1px solid var(--border);
        -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
  #mapbox.radarlabel #radarState{display:inline;font-size:var(--fs-micro);
        font-weight:700;letter-spacing:.08em;line-height:1;
        text-shadow:var(--map-label-shadow)}

  /* Find my location, bottom-left of the tile – the corner the attribution
     (bottom-right) and the two top buttons leave free. TILE ONLY: the
     fullscreen stack already has #mapLocate, and that one means something
     else entirely (recentre on the existing pin, deliberately not a fresh
     fix), so three location controls would be two too many. */
  #mapHere{display:none}
  /* It lives INSIDE #map (see the markup), because .mapcard is the
     containing block: bottom-left there is the tile's bottom-left, where
     #mapbox's would be far below the slider and the strip. The price is
     Leaflet's panes as neighbours instead of a stacking context to sit on
     top of, so this needs to clear the highest of them (userPane, 640). */
  #mapbox.herebtn:not(.fullscreen) #mapHere{
        display:flex;z-index:800;top:auto;bottom:10px;left:10px;right:auto}
  #mapHere:disabled{color:var(--muted)}
  #mapbox.fullscreen #mapClose{display:flex;width:38px;height:38px;right:auto;
           top:calc(14px + env(safe-area-inset-top));left:calc(14px + env(safe-area-inset-left))}
  #mapbox.fullscreen #mapClose svg{width:17px;height:17px}

  /* Full-bleed: the map is a view, not a floating sheet – it runs edge to edge
     (under the notch; tiles can safely paint there). The safe-area insets sit
     on the floating chrome instead: close/zoom/locate, scale bar, card,
     attribution each offset themselves clear of notch and home indicator. */
  #mapbox.fullscreen{position:fixed;z-index:200;overflow:hidden;background:var(--map-ground);
           inset:0}
  /* Fullscreen is an interactive map (pan + pinch-zoom, enabled in JS): the map
     fills the sheet and the inline slider/heading/list are hidden. The radius
     ring is hidden too – once you are exploring, the radius is no longer the
     point and the circle just gets in the way (it still frames the opening view,
     since the fit is computed from it). */
  #mapbox.fullscreen #map{position:absolute;inset:0;height:auto;aspect-ratio:auto;
           margin:0;border:0;border-radius:0}
  #mapbox.fullscreen .radius,#mapbox.fullscreen .s-headrow,#mapbox.fullscreen #list{display:none}
  #mapbox.fullscreen .leaflet-ring-pane{display:none}
  /* The radar sweep belongs to the tile, so it is hidden in here – but hidden
     with VISIBILITY, not display: display:none terminates a CSS animation, and
     the line would lose its place and restart at twelve o'clock on the way
     back. app.js mutes the names and blips for the duration (radarFrame), so
     what carries on behind this is a rotation and nothing else. */
  #mapbox.fullscreen .leaflet-radar-pane{visibility:hidden}

  /* Area mode (a shared ?area= card): the map is framed on the authority's
     boundary, so there is no radius to set and no "your location" to return
     to – both controls go. See docs/shareable_cards.md. */
  #mapbox.areamode .radius{display:none}
  #mapbox.areamode.fullscreen #mapLocate{display:none}

  /* Scale bar: centred at the top, clear of the close/zoom stack on the right. */
  #mapScale{display:none}
  #mapbox.fullscreen #mapScale{display:flex;flex-direction:column;align-items:center;gap:3px;
           position:absolute;z-index:2;pointer-events:none;
           top:calc(16px + env(safe-area-inset-top));left:50%;transform:translateX(-50%)}
  #mapScale .mapscale-bar{height:2px;width:60px;background:rgba(255,255,255,.85);border-radius:1px;
           box-shadow:0 0 2px rgba(0,0,0,.6)}
  #mapScale .mapscale-label{font-size:12.5px;color:var(--ink);text-shadow:0 1px 3px rgba(0,0,0,.75)}

  /* Tapped sauna's card: slides up from the bottom, dismissed by tapping the map. */
  #mapCard{display:none}
  #mapbox.fullscreen #mapCard{display:block;position:absolute;z-index:2;pointer-events:auto;
           left:calc(12px + env(safe-area-inset-left));right:calc(12px + env(safe-area-inset-right));
           bottom:calc(12px + env(safe-area-inset-bottom));
           background:var(--card);border:1px solid var(--border);border-radius:var(--r-row);
           overflow:hidden;opacity:0;transform:translateY(130%);
           transition:transform .2s ease,opacity .2s ease}
  #mapbox.fullscreen #mapCard.shown{opacity:1;transform:translateY(0)}

  /* Close sits TOP-LEFT in here, where expand was on the tile – leaving the view
     is the same gesture as entering it and belongs in the same corner. It also
     takes the close button out of the right-hand stack, where it was reading as
     the top of the zoom control rather than as its own thing; the map controls
     (zoom, then locate) now have that edge to themselves and start at the top
     of it. */
  #mapLocate{display:none}
  #mapbox.fullscreen #mapLocate{display:flex;top:calc(94px + env(safe-area-inset-top));
           right:calc(14px + env(safe-area-inset-right))}
  #mapbox.fullscreen .leaflet-top.leaflet-right{top:calc(14px + env(safe-area-inset-top))}
  #mapbox.fullscreen .leaflet-control-zoom{border:0;box-shadow:none;
           margin:0 calc(14px + env(safe-area-inset-right)) 0 0}
  /* Attribution sits in Leaflet's bottom-left corner; lift it off the home
     indicator now the box is full-bleed. */
  #mapbox.fullscreen .leaflet-bottom{bottom:env(safe-area-inset-bottom)}
  #mapbox.fullscreen .leaflet-control-zoom a{width:34px;height:34px;line-height:32px;font-size:18px;
           background:var(--map-chrome-bg);border:1px solid var(--border);color:var(--ink);
           -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
  #mapbox.fullscreen .leaflet-control-zoom a:hover{background:var(--map-chrome-bg-strong);color:var(--ink)}
  #mapbox.fullscreen .leaflet-control-zoom a:first-child{border-radius:var(--r-md) var(--r-md) 0 0}
  #mapbox.fullscreen .leaflet-control-zoom a:last-child{border-radius:0 0 var(--r-md) var(--r-md);border-top:0}

  /* Hover tooltips (sauna name + distance). Same floating-over-map chrome as
     the map buttons and the fullscreen zoom control: --map-chrome-bg behind a
     --border hairline, blurred. Arrowless – the default arrow is a solid
     triangle that cannot match a translucent box; app.js bindHover offsets the
     tooltip clear of the dot instead. Killed for every tooltip, labels too. */
  .leaflet-tooltip{background:var(--map-chrome-bg);border:1px solid var(--border);
           color:var(--ink);font-family:var(--font);font-size:12px;font-weight:600;
           border-radius:var(--r-md);padding:4px 9px;box-shadow:none;
           -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
  .leaflet-tooltip::before{display:none}

  /* The carousel's selected sauna, named on the tile (app.js carChipTo).
     DEFAULT IS THE SHIPPED CHROME ABOVE, untouched: this is the same object
     the map already shows for a marker – its name – just held open instead
     of waiting for a pointer a phone hasn't got. So it wants no styling of
     its own, and .sauna-chip carries none. The ring around the dot is what
     says "selected"; the callout only says which sauna.

     The two louder variants are rig options (CAR_CHIP_STYLE), not defaults,
     so they can be judged over a real map rather than argued about:

     --blue    the selection blue on the hairline, tying the callout to the
               ring and the card border. One notch up from prod.
     --callout the house callout chip (Mika's sketch, 4 Aug, specified in
               design/tools/dotplot-rig.js putCallout): solid --card
               housing, stroke in the MARKER's colour so the semantics
               survive the housing, and a pointer aimed at the dot. Loud on
               a dense ring, which is why it is not the default – but note
               the arrow killed for every tooltip above is killed because a
               solid triangle cannot match a TRANSLUCENT box, so a solid
               chip is exactly what earns the pointer back. Pointer down
               only: carChipTo always hangs the callout above its dot. */
  /* THE HOUSE CALLOUT SHAPE, shared with the surprise card: a solid --card
     box, a 1px outline, and the --r-sm corner - the same 5px the curve's own
     dot labels use (design/tools/dotplot-rig.js putCallout, where the SVG
     carries it as a literal rx="5"). --r-sm is the token; if that SVG ever
     wants tokening too, this is the number it should read.

     Solid, so the notch is possible at all: a solid triangle cannot match a
     TRANSLUCENT box, which is why the arrow is killed for every other
     tooltip. The outline is a CHOICE, and the notch always follows it, so
     the callout can never be outlined in one colour and pointed in another. */
  .sauna-chip{background:var(--card);border-color:var(--chip-outline,var(--border));
           border-radius:var(--r-sm);--chip-notch:var(--chip-outline,var(--border));
           -webkit-backdrop-filter:none;backdrop-filter:none}
  .sauna-chip--blue{--chip-outline:var(--blue)}
  .sauna-chip--magenta{--chip-outline:var(--bss)}
  /* ...and the shipped map chrome, kept as the calm alternative: translucent,
     blurred, wider corner, and its notch matches the box rather than an edge
     it does not have. */
  .sauna-chip--plain{background:var(--map-chrome-bg);border-color:var(--border);
           border-radius:var(--r-md);--chip-notch:var(--map-chrome-bg);
           -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}
  /* The notch, and the horizontal shift that keeps the callout on the tile.
     --notch-x is written by app.js carChipClamp when the box has been moved
     sideways to stay in frame: the box slides, the point stays over its dot -
     the same trick putCallout plays inside the surprise card's canvas. */
  .sauna-chip--notch.leaflet-tooltip-top::before{display:block;
           left:var(--notch-x,50%);border-top-color:var(--chip-notch)}

  /* Sauna name labels: permanent tooltips, virtualised in JS (app.js
     syncLabels) – bound only in fullscreen, only past LABEL_ZOOM, and only for
     markers inside the current view, so existence is the visibility switch and
     no CSS gating is needed. Bare text over the map: the chrome box above is
     stripped back off (a blur under a transparent box would still smear). */
  .sauna-label{background:none;border:0;box-shadow:none;padding:0;
           -webkit-backdrop-filter:none;backdrop-filter:none;
           color:var(--ink);font-size:11px;font-weight:600;white-space:nowrap;
           text-shadow:var(--map-label-shadow)}

  /* Your location pin: a divIcon (app.js renderMap), so unlike the Leaflet-
     drawn markers its colours resolve from the tokens. .pin--live is set only
     when the lookup came from a trusted geolocation fix (see the pages' geo
     handlers) – the slow first-iPhone "you are here" pulse; reduced-motion
     users keep the static pin. */
  .pin{position:relative;width:18px;height:18px;border-radius:var(--r-circle);
           background:var(--pin-fill);border:2px solid var(--blue)}
  @media (prefers-reduced-motion:no-preference){
    .pin--live::after{content:"";position:absolute;inset:-2px;border-radius:var(--r-circle);
             background:var(--blue);opacity:0;animation:pin-pulse 2.6s ease-out infinite}
  }
  @keyframes pin-pulse{
    0%{opacity:.45;transform:scale(.6)}
    70%,100%{opacity:0;transform:scale(2.4)}
  }

  /* The radar sweep off a live pin (app.js startRadar): a disc the size of the
     radius ring, centred on the pin by JS (left/top/width in LAYER pixels – the
     transform is spoken for by the rotation). The wedge is a conic gradient
     with its bright edge at 12 o'clock and the trail running back
     anticlockwise, so once the element rotates, the leading edge sits at
     exactly the element's rotation angle – which is what lets app.js read this
     animation's own clock and know which bearing the line has reached. The 5s
     here is only what the duration would be if the JS never ran: startRadar
     writes animation-duration from RADAR_RPM, which is where speed is set.
     No reduced-motion variant on purpose: the feature IS the motion, so there
     is nothing to degrade to – app.js checks the query and does not run.

     --radar-trail-from is where the trail begins, and app.js walks it back from
     360deg (no trail at all) to 296deg over the first 64 degrees of the very
     first turn. The trail is a record of where the line has BEEN, so drawing it
     complete at t=0 claims a history the sweep does not have yet: it should be
     swept into existence, not switched on. The 296deg fallback here is the
     settled trail, for the case where the JS never writes it; the mid stop
     rides at 72% of whatever the trail currently is, so the shape holds at
     every length. */
  .radar{position:absolute;border-radius:var(--r-circle);pointer-events:none;
           background:conic-gradient(from 0deg,
             var(--radar-tail) 0deg,
             var(--radar-tail) var(--radar-trail-from,296deg),
             var(--radar-trail) calc(var(--radar-trail-from,296deg)
                        + (360deg - var(--radar-trail-from,296deg))*.72),
             var(--radar-edge) 360deg);
           animation:radar-sweep 5s linear infinite}
  /* the scan line itself: a hairline from the centre to the rim at 12 o'clock,
     carried round by the parent's rotation. Brightest at the rim. */
  .radar::before{content:"";position:absolute;left:50%;top:0;width:1px;height:50%;
           background:linear-gradient(to top,var(--radar-tail),var(--radar-line))}
  @keyframes radar-sweep{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}

  /* A sauna the line has just crossed, named for a moment: the same bare-text
     label as the fullscreen ones (.sauna-label is bound alongside this class),
     on a fade-in / hold / fade-out cycle. An animation, not a transition, so it
     runs the moment Leaflet inserts the tooltip – and it beats the inline
     opacity Leaflet sets on the container, which a plain rule would not.
     The duration here is a fallback only: app.js writes the real one onto the
     element as it binds, from RADAR_LABEL_TURNS (a quarter of a revolution).
     The halo is heavier than .sauna-label's – these are read in passing, over
     whatever the sweep is crossing, and what they are really competing with is
     the basemap's own place names in the same visual register. */
  .radar-label{animation:radar-label 2.5s ease-out both;
           text-shadow:var(--radar-label-shadow)}
  @keyframes radar-label{
    0%{opacity:0}10%{opacity:1}72%{opacity:1}100%{opacity:0}
  }

  /* The dot's answer to the line (app.js radarBlip): the marker swells and
     settles as the sweep crosses it. transform-box:fill-box is the load-bearing
     bit – without it an SVG <path> scales about the SVG root's origin and the
     dot flies off the tile. On BSS stars the class lands on the disc inside the
     divIcon, never the icon element, which Leaflet positions by transform. */
  .sauna-ping{transform-box:fill-box;transform-origin:center;
           animation:sauna-ping .5s cubic-bezier(.2,.7,.3,1)}
  @keyframes sauna-ping{
    0%{transform:scale(1)}25%{transform:scale(1.6)}100%{transform:scale(1)}
  }

  /* ---- The line scan (area cards only) -------------------------------------
     The disc above turns about a centre; an area card has none – no postcode,
     no pin, no ring – so its tile gets a line instead, descending the frame and
     rising back. Settled on the bench (design/tools/linescan-rig.js, which
     keeps the alternatives and the measurements): top to bottom, 5.5 s a pass
     so a there-and-back is one breath, sinusoidal so it decelerates into each
     turn, and full bleed – a straight line's two ends ARE the tile's edges, so
     fading them only makes it look short.
     app.js owns the geometry: the @keyframes are written there, sampled from
     the same curve the crossing test reads, and the trail is scaled per frame.  */
  .linescan{position:absolute;pointer-events:none;overflow:hidden}
  .linescan .ls-carriage{position:absolute;left:0;top:0;height:0;width:100%;
           animation:linescan 5.5s linear infinite alternate}
  /* The trail is the head's SPEED, not its history: a fixed band hung off the
     head's own edge and scaled each frame to the distance it would cover in
     LINESCAN_TRAIL_S at the current rate. Under sine that reaches zero exactly
     at a turnaround, so the side it falls on flips while there is nothing to
     see. A history cannot do that – at a turn the last second genuinely lies on
     the far side, so it teleports across the head at full length. */
  .linescan .ls-trail{position:absolute;top:0;left:0;width:100%;
           transform-origin:0 0;transform:scaleY(0);
           background:linear-gradient(to top,var(--radar-tail),
                      var(--radar-trail) 45%,var(--radar-edge))}
  .linescan .ls-head{position:absolute;top:0;left:0;width:100%;
           background:linear-gradient(to right,transparent,var(--radar-line) 0,
                      var(--radar-line) 100%,transparent)}
  @keyframes linescan{from{transform:translateY(0)}to{transform:translateY(0)}}
  /* A sauna the line crosses that the authority does not own. Those are canvas
     dots (buildTinyDots) with no element to animate, so the ping is a div of
     its own, in their magenta, over the dot's place. They are never NAMED by
     the sweep – the names belong to the authority the card is about, and
     measured on Argyll letting them in cost the card three of its own
     eighteen to the shared label budget – but hovering one names it. */
  .ls-ghost{position:absolute;width:10px;height:10px;margin:-5px 0 0 -5px;
           border-radius:var(--r-circle);background:#ff4fd8;pointer-events:none;
           animation:ls-ghost-ping .5s cubic-bezier(.2,.7,.3,1) forwards}
  @keyframes ls-ghost-ping{
    0%{transform:scale(.35);opacity:.85}100%{transform:scale(1.7);opacity:0}
  }

  /* The authority's name, in the band the area fit's padding opens above the
     boundary. Its box IS the map buttons' box – same top, same height – so it
     sits on their centreline by construction rather than by a nudged offset.
     Capped clear of both; app.js steps the type down for the one authority
     name that needs it ("Bournemouth, Christchurch and Poole", on a phone).
     Decorative: the h2 above is the real one, so it is aria-hidden. */
  /* The authority's name, top-centre. Shipped on area cards; now on a
     postcode lookup too, where it names the authority the pin sits in
     (#mapbox.ladchip, set by app.js syncAreaName, which also decides there
     is a name worth showing). Decorative in both: the h2 above is the real
     one, so the element is aria-hidden.
     The gutter is measured from the two map buttons rather than assumed —
     the radar switch changes width when it carries its ON/OFF word. */
  #areaName{display:none}
  /* :has(#map:not(.hidden)) like the two buttons – without it the chip is the
     one piece of map chrome that can appear when there is no map, and if
     Leaflet fails to load it lands on the radius slider. */
  #mapbox.ladchip:not(.fullscreen):has(#map:not(.hidden)) #areaName{
           display:flex;position:absolute;
           z-index:2;top:10px;height:34px;
           left:var(--areaname-x,50%);transform:translateX(-50%);
           max-width:var(--areaname-max,calc(100% - 112px));
           align-items:center;pointer-events:none;
           padding:0 12px;border-radius:var(--r-pill);
           background:var(--map-chrome-bg);border:1px solid var(--border);
           -webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px);
           font-size:13px;font-weight:600;letter-spacing:.01em;color:var(--ink);
           white-space:nowrap;overflow:hidden;text-overflow:ellipsis}

  /* The heading line: count on the left, the way into the full list on the
     right - the slot a locations list gives its "see everything" link. The
     margins live on the row now, so the h3 keeps its size and loses its own. */
  .s-headrow{display:flex;align-items:baseline;justify-content:space-between;
        gap:12px;margin:28px 0 12px}
  .s-headrow .grouphead{margin:0}
  .grouphead{font-size:var(--fs-md);font-weight:600;margin:28px 0 12px}
  /* Reads as the row's link, not a control: same blue and chevron as
     .s-link on a card. Hidden by app.js when there is nothing to list, and
     with the whole row in fullscreen. */
  .seeall{background:none;border:0;padding:4px 0;font-family:inherit;
        font-size:var(--fs-small);font-weight:600;color:var(--blue);
        white-space:nowrap;cursor:pointer;flex:none}
  .seeall:active{opacity:.6}
  .seeall.hidden{display:none}
  ul{list-style:none;margin:0;padding:0}
  li{background:var(--card);border:1px solid var(--border);border-radius:var(--r-row);margin-bottom:10px;overflow:hidden}
  .item{display:flex;justify-content:space-between;align-items:flex-start;gap:12px;
        padding:15px 18px;text-decoration:none;color:inherit}
  /* NB saunaRow() always emits <div class="item">, never <a class="item">, so a
     row is not a link and has no pressed state. Only the .s-link anchor inside
     it is interactive. (An `a.item:active` rule lived here for months and never
     matched anything.) */
  .s-nm{font-size:var(--fs-md);font-weight:600}
  .bss{display:inline-block;font-size:var(--fs-micro);font-weight:600;letter-spacing:.05em;color:var(--bss);
       border:1px solid var(--bss);border-radius:var(--r-sm);padding:1px 5px;margin-left:8px;vertical-align:2px}
  /* "in <authority>" chip on the lookup page: marks the saunas inside the
     looked-up authority in the single radius list. Blue to tie to the map's
     area outline and the location pin. */
  .inla{display:inline-block;font-size:var(--fs-micro);font-weight:600;color:var(--blue);
        border:1px solid var(--blue);border-radius:var(--r-sm);padding:1px 5px;margin-left:8px;vertical-align:2px}
  .s-meta{font-size:var(--fs-small);color:var(--muted);margin-top:3px}
  .s-link{font-size:var(--fs-small);color:var(--blue);margin-top:7px;display:inline-flex;align-items:center;gap:3px}
  .s-r{font-size:var(--fs-small);color:var(--muted);white-space:nowrap;padding-top:3px}
  .chev{color:var(--muted);font-weight:400}

  /* ── Carousel mode (design exploration, this branch only) ──────────────
     #mapbox.carousel lays the nearest-saunas list out as a horizontal,
     edge-bled, snap-scrolling filmstrip under the map instead of a vertical
     stack. saunaRow() emits the same DOM in both modes; this block changes
     only the flow direction and the card's internal stacking, so the mode
     is one class. The custom properties are written by carVars() in app.js,
     which is what lets design/tools/carousel-rig.js drive them live – the
     settled numbers live there (CAR_*), not here. */
  /* :not(.fullscreen): the fullscreen sheet hides the list (see the
     #mapbox.fullscreen rule above) at the SAME specificity as this block,
     which sits later in the file and would win the cascade – leaving every
     card link an invisible tab stop behind the map. Scoping out beats
     re-asserting the hide further down.
     The 18px bleed/padding/scroll-padding mirror .wrap's side padding –
     change them together (the league modal documents the same coupling). */
  #mapbox.carousel:not(.fullscreen) #list{display:flex;gap:var(--car-gap,10px);
        overflow-x:auto;margin:0 -18px;padding:4px 18px;scroll-padding-inline:18px;
        scroll-snap-type:x mandatory;overscroll-behavior-x:contain;
        scrollbar-width:none}
  #mapbox.carousel #list::-webkit-scrollbar{display:none}
  #mapbox.carousel #list li{flex:0 0 var(--car-w,172px);margin-bottom:0;
        display:flex;scroll-snap-align:var(--car-snap,start)}
  /* the map-linked card: same blue as its marker's focus ring on the tile */
  #mapbox.carousel #list li.current{border-color:var(--blue)}
  /* Everything below scopes under #list, NOT bare .item: the fullscreen
     tapped-sauna card (#mapCard) is also inside #mapbox and also renders
     saunaItem() markup – bare selectors restyled it by accident. */
  #mapbox.carousel #list .item{flex-direction:column;align-items:stretch;gap:0;
        width:100%;padding:12px 14px;position:relative}
  /* town only on the cards: the meta tail (", Greater London, England")
     renders in the row and the fullscreen card, not here */
  #mapbox.carousel #list .s-mrest{display:none}
  /* the badges leave the clamped name for the card's top corner, beside the
     distance – absolute, so the two-line clamp cannot spend a line on a
     chip. .item is the containing block, which also lifts them clear of
     .s-nm's overflow clip. */
  #mapbox.carousel #list .s-badges{position:absolute;top:11px;right:12px;
        display:flex;gap:4px;max-width:55%}
  #mapbox.carousel #list .s-badges .bss{
        margin-left:0;vertical-align:baseline;min-width:0;
        overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* ...but the "in <authority>" chip does not fit beside the distance and
     never really did: a 156px card leaves it ~76px, where "in Westminster"
     wants 92 and "Kensington and Chelsea" 141, so it was ellipsised for all
     but the shortest authorities. Dropped from the CARDS only – the map's
     blue area outline already shows which saunas are inside the authority,
     and the strip is for scanning names and distances. The list row and the
     full-screen sheet still carry it, where the width exists. */
  #mapbox.carousel #list .inla{display:none}
  /* Distance leads the card, where a locations list puts its flag - but in
     the muted token, like every other supporting value: the name is what you
     are scanning for, and a white distance was outranking it. Area cards
     carry no distance at all, so the name simply leads there. */
  #mapbox.carousel #list .s-r{order:-1;padding:0 0 6px;color:var(--muted);font-weight:600}
  #mapbox.carousel #list .item>div:first-child{display:flex;flex-direction:column;
        flex:1;min-width:0}
  /* one type step down from the row's --fs-md: at ~172px the row size wraps
     almost every name to three lines */
  #mapbox.carousel #list .s-nm{font-size:var(--fs-body);line-height:1.3;
        display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
        overflow:hidden}
  #mapbox.carousel #list .s-meta{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  #mapbox.carousel #list .s-link{margin-top:auto;padding-top:8px}
  /* the empty-state row is a message, not a card – keep it in row form */
  #mapbox.carousel #list li:not(:has(.item)){flex:1 0 auto;display:block;
        padding:15px 18px}
  /* the carousel's focus ring on the map lives in its own pane (see
     carRingLayer in app.js): never a pointer target, and fullscreen is the
     explorer, not the filmstrip, so the ring stays on the tile */
  #map .leaflet-car-pane{pointer-events:none}
  /* The selection ring is blue over a DARK HALO, so it holds its own edge
     against both the magenta it encircles and whatever the basemap happens
     to be doing underneath - the same job --map-label-shadow does for bare
     text, in the only form an SVG stroke can take it. Two shadows: a tight
     one for the edge, a wider one for separation. */
  #map .leaflet-car-pane path{filter:drop-shadow(0 0 1.5px rgba(0,0,0,.95))
           drop-shadow(0 0 3px rgba(0,0,0,.75))}
  #mapbox.fullscreen .leaflet-car-pane{display:none}


  /* "In the local press" evidence block under the verdict card. */
  .newsline{margin-top:14px;padding:13px 16px;background:var(--card);
            border:1px solid var(--border);border-radius:var(--r-row)}
  .nl-h{font-size:var(--fs-micro);font-weight:600;letter-spacing:.05em;
        text-transform:uppercase;color:var(--muted)}
  .nl-sum{font-size:var(--fs-body);margin:6px 0 8px}
  .nl-items{list-style:none;margin:0;padding:0}
  .nl-items li{background:none;border:0;border-radius:0;margin:0 0 5px;overflow:visible;
               font-size:var(--fs-small)}
  .nl-items a{color:var(--blue);text-decoration:none}
  .nl-items a:hover{text-decoration:underline}
  .nl-src{color:var(--muted)}

  .radius{margin:0}
  .rhead{display:flex;justify-content:space-between;font-size:var(--fs-body);color:var(--muted);margin-bottom:10px}
  .rhead b{color:var(--ink);font-weight:600}
  input[type=range]{width:100%;accent-color:var(--blue);height:22px}

  .about{margin-top:44px}
  .about hr{border:0;border-top:1px solid var(--border);margin:0 0 22px}
  .about-lbl{font-size:var(--fs-body);color:var(--muted);margin:0 0 12px}
  .about h2{font-size:var(--fs-md);font-weight:600;line-height:1.3;margin:0 0 16px}
  .about p{font-size:var(--fs-body);color:var(--muted);line-height:1.6;margin:0 0 16px}
  .about b{color:var(--ink);font-weight:600}
  .about a{color:var(--blue);text-decoration:none}

  .modal{position:fixed;inset:0;background:var(--bg);z-index:100;display:flex;flex-direction:column;
         padding:20px 18px calc(20px + env(safe-area-inset-bottom))}
  .modal-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:14px}
  .modal-head h2{font-size:var(--fs-lg);font-weight:700;letter-spacing:-.5px;margin:0}
  .modal-sub{font-size:var(--fs-small);color:var(--muted);margin:3px 0 0}
  .modal-close{background:var(--card);border:1px solid var(--border);color:var(--ink);width:36px;height:36px;
               border-radius:var(--r-circle);font-size:var(--fs-ui);line-height:1;cursor:pointer;flex:none;
               display:flex;align-items:center;justify-content:center}
  .modal-close svg{width:15px;height:15px}
  .modal-search{background:var(--card);border:1.5px solid var(--border);border-radius:var(--r-lg);color:var(--ink);
                font-size:var(--fs-ui);padding:12px 14px;margin-bottom:12px;outline:none;width:100%}
  .modal-search:focus{border-color:var(--blue)}
  .modal-search::placeholder{color:var(--muted)}
  .modal-list{overflow-y:auto;flex:1;-webkit-overflow-scrolling:touch;margin:0 -6px}
  .lrow{display:flex;align-items:flex-start;gap:10px;padding:12px 6px;border-bottom:1px solid var(--border)}
  .lrow.current{background:var(--blue-soft);border-radius:var(--r-md);border-bottom-color:transparent}
  .lrank{font-size:var(--fs-small);color:var(--muted);width:32px;flex:none;padding-top:1px;font-variant-numeric:tabular-nums}
  .lname{flex:1;font-size:var(--fs-ui);font-weight:600;min-width:0}
  .lstats{font-size:var(--fs-small);color:var(--muted);text-align:right;flex:none;white-space:nowrap;padding-top:1px}
  .lstats .g{font-weight:600}
  .lempty{color:var(--muted);font-size:var(--fs-body);padding:16px 6px}

  /* Keyboard focus. :focus-visible only, so a mouse click never paints a ring
     but a Tab always does. Nothing here should be removed without a
     replacement – an invisible focus ring makes the page unusable by keyboard. */
  :focus-visible{outline:2px solid var(--blue);outline-offset:2px}
  /* A sauna row is a div, not a link – only the "Visit website" anchor inside
     it is focusable, so that is what takes the ring. */
  .s-link:focus-visible{outline-offset:3px;border-radius:var(--r-xs)}
  /* The postcode input sets outline:none (the blue field border is the resting
     state), so on keyboard focus the ring goes on the field, not the input. */
  .field:has(input:focus-visible){outline:2px solid var(--blue);outline-offset:2px}
  .modal-search:focus-visible{border-color:var(--blue)}

  /* Desktop tuning for the wider column. Kept to properties that don't touch
     `display`, so nothing here can out-rank .hidden (an #id display rule in a
     media query would beat the .hidden class and reveal #out before a lookup). */
  @media (min-width: 700px){
    /* The tile goes back to landscape above the phone breakpoint. 4:5 is a
       PHONE answer: there the map was a letterbox getting a third of the
       fold, and portrait bought it presence for nothing. On the 640px column
       the same ratio makes the map 604x755 and pushes the sauna strip about
       two full screens down - so the taller tile costs most exactly where
       there is the most room, and where the map is already wide enough to
       read. 4:3 restores what desktop shipped, which makes the whole change
       a phone improvement rather than a trade.

       Set on :root, not on .mapcard, so an INLINE --map-ar (which is how
       design/tools/marker-rig.js sweeps the shape) still wins here - a
       stylesheet rule on .mapcard would out-specify the rig at exactly the
       width worth testing. */
    :root{--map-ar:4/3}
    .wrap{padding-top:38px}
    /* The league modal is position:fixed and ignores .wrap, so its rows would
       stretch the full viewport. Cap and centre the content to the same column. */
    .modal{padding-left:max(18px,calc((100% - 640px) / 2));
           padding-right:max(18px,calc((100% - 640px) / 2))}
  }

  /* Must be last: wins the cascade tie-break against any earlier same-
     specificity display rule (e.g. .modal{display:flex}) so elements that
     are both .modal and .hidden actually stay hidden until JS opens them. */
  .hidden{display:none}

/* ---- Spec v2 series break (Jul 2026) --------------------------------------
   Both rules below belong to the same announcement and come off together when
   the notice is retired. Tokens only, per design/README.md. */

/* The full note in the About block. Marks the model-update paragraphs so they
   read as an announcement rather than body copy. */
.about .update-note {
  border-left: 3px solid var(--blue);
  padding-left: 14px;
  /* #model-update is a jump target (the verdict notice links to it), so leave
     air above it rather than pinning the paragraph to the viewport edge. */
  scroll-margin-top: 22px;
}

/* The dated one-liner under the verdict badge (app.js modelUpdateNote), on the
   lookup and pcon. It repeats the About block's blue rule so the two read as
   one announcement seen twice, and stays at caption size with no card of its
   own: the tiles above and the surprise card below are the loud parts, and a
   notice that shouted would re-frame every verdict as a correction.
   TEMPORARY – review on or after 1 Oct 2026, see app.js. */
.updated {
  font-size: var(--fs-caption);
  color: var(--muted);
  line-height: 1.45;
  border-left: 3px solid var(--blue);
  padding-left: 11px;
  margin: 12px 0 0;
}
.updated a { color: var(--blue); text-decoration: none; white-space: nowrap; }
.updated a:hover { text-decoration: underline; }
