/* Reach Home Groups map.

   Brand primitives and @font-face live in /theme.css, generated per tenant
   from churches/<id>.json. This file holds only what every church shares:
   semantic roles, the type scale, and layout. Duplicating the palette here
   would give two sources for one set of colours, and they would drift.

   The roles below reference primitives through var(), so overriding those
   upstream recolours the whole map without touching anything here. */

:root {

  /* How far anything anchored to the BOTTOM of the map may be lifted before it
     leaves the TOP of it.

     Google's bundled cluster is 153px tall and sits 24px above the map's
     bottom edge, so it needs 177px of room; 8px more keeps it off the very
     edge. The lift below is otherwise a raw pixel count with nothing bounding
     it, and the viewport is the thing that shrinks — press Cmd-+ on a 1440x813
     window and at 150% the camera control is at y=-40, at 175% the zoom
     buttons follow, and by 250% Google's logo and credits have gone too. That
     last part matters most: it puts the attribution off screen while map
     imagery is still displayed, which is the exact thing the lift was added to
     prevent. Zoom is not really the variable — height is. A 1440x560 window at
     100% does it as well. */
  --ask-lift-max: max(0px, 100dvh - 185px);
  /* ── Roles ── */
  --bg: var(--paper);
  --surface: var(--paper-lightest);
  /* color-scheme-1, read off reachchurch.cc/style-guide rather than guessed:
       background  #f4f2eb   the ground the directory sits on
       foreground  #fff      cards and fields, a step ABOVE that ground
       text        black     not charcoal — the scheme is explicit
       border      #0c080126 the near-black neutral at 15%
     The directory had this inverted: a near-white ground with cards barely
     distinguishable from it, so nothing read as a card. */
  --scheme-fg: #fff;
  --scheme-text: #000;
  --line: var(--paper-dark);
  --line-strong: var(--charcoal-lighter);
  --text: var(--charcoal);
  --text-2: var(--charcoal-light);
  /* Was #8a8a86 — hand-mixed, and 3.41:1 on --surface, under the 4.5:1 floor.
     Every other value here traces to a Reach primitive; this now does too. */
  --text-3: var(--charcoal-light);

  /* Category colours are NOT defined here. They arrive from /theme.css as
     --cat-<status>-<role>, generated per tenant, and one pairing feeds the map
     circle, the key dot and the status pill at once. Redefining a role here
     would silently win over the tenant's own choice, because styles.css loads
     after theme.css. */


  /* Type scale. Previously 13 sizes, nine of them inside a 4px band and several
     on half-pixels — steps too small to read as hierarchy, large enough to read
     as sloppiness. Seven steps, integer, each doing distinct work. */
  --t-micro: 12px;

  /* Vertical correction for mark/wordmark lockups. Geometric cap-centring
     against the mark is -0.058em from the line-box centre, but the descender
     in "Groups" pulls the perceived mass down, so dead-centred caps read high.
     Half the correction is what actually looks centred — measured against the
     mark's own top and bottom edges at 3x. */
  --cap-centre-nudge: -0.03em;
  --t-small: 13px;
  --t-body: 15px;
  --t-label: 15px;
  --t-title: 20px;
  --t-display: 32px;
  --t-display-print: 40px;

  --radius: 8px;
  /* .button.is-secondary's border and lift, from the style guide: the near-black
     neutral at 15%. One definition, worn by every secondary button. */
  --secondary-line: color-mix(in srgb, var(--charcoal-darkest) 15%, transparent);
  /* Built from the tenant's own neutral, not a literal. These were Reach
     charcoal hexes with the alpha baked into the hex — the last four palette
     values in this stylesheet a church could not change from its config. */
  --shadow-ink: var(--charcoal);
  --shadow: 0 1px 2px color-mix(in srgb, var(--shadow-ink) 6%, transparent),
            0 4px 16px color-mix(in srgb, var(--shadow-ink) 5%, transparent);
}

/* Ported from reachchurch.cc .text-style-tagline:
     font-family: var(--_typography---font-styles--subtitle)  → Tay Birdie
     text-transform: uppercase; font-size: 1.125rem; font-weight: 400
   The guide also ships .is-event-details at 0.875rem, which establishes that
   this style scales down; the sizes below follow that precedent rather than
   forcing 18px onto every micro-label. Tracking stays `normal` per the guide —
   the face is already wide, and added tracking made it look stretched. */
.text-style-tagline {
  font-family: var(--font-tagline);
  text-transform: uppercase;
  font-weight: 400;
  letter-spacing: normal;
  display: inline-block;
}

* { box-sizing: border-box; }

/* Any display value set on an element itself outranks [hidden]'s display:none
   from the UA stylesheet, so `el.hidden = true` silently does nothing to a
   grid or a flex container. That has caught #print-bar, #export-notice and
   two elements on the dashboard. One rule ends the whole class of bug. */
[hidden] { display: none !important; }
/* dvh, not vh or %. On iOS Safari the visual viewport shrinks and grows as the
   toolbars collapse; vh is frozen at the largest size (so content is cut off)
   and % depends on an ancestor chain that the dynamic toolbar does not update.
   dvh tracks the live viewport, which is what keeps the map edge-to-edge as
   the bars come and go. */
html, body {
  height: 100%;
  height: 100dvh;
  margin: 0;
  overscroll-behavior: none; /* no rubber-band revealing the page background */
}

body {
  font-family: var(--font-body);
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

/* Fixed halves, not a draggable split. The directory is a browsing surface in
   its own right, not a sidebar next to the map, and a handle that lets someone
   squeeze it to a quarter mostly produces a layout they did not want. */
#app {
  display: grid;
  grid-template-columns: 1fr 1fr;
  height: 100%;
}

/* ── Sidebar ─────────────────────────────────────────────────────────────── */

/* The SIDEBAR scrolls, not the group list inside it. With Legend + Show +
   four filter facets above it, an inner-scrolling list was being squeezed to
   about one visible row — the filters won all the space and the actual content
   lost. Scrolling the whole column lets the list take the height it needs. */
#sidebar {
  display: flex; flex-direction: column; min-height: 0;
  /* Paper, so the white cards and fields inside it have something to sit on. */
  background: var(--paper); color: var(--scheme-text);
  border-right: 1px solid var(--line);
  overflow-y: auto; overflow-x: hidden;
}

/* Reach's "R" mark, inlined from reach_icon.svg so it can take currentColor —
   the published asset is fill="white" for the site's dark footer and would be
   invisible on paper. It sits to the LEFT of the wordmark, which is where every
   reference pattern puts it (NBC News, South China Morning Post).

   Its height is the CAP HEIGHT of the title beside it, so the mark and the "H"
   in "Home Groups" are exactly the same height. Perfectly Nineties measures a
   cap ratio of 0.728 of font-size, so expressing the mark as 0.728em against
   the header's own font-size keeps them locked together at any size rather
   than needing a magic pixel value per breakpoint.

   The wordmark is centred on the mark optically, not geometrically. Plain
   `align-items: center` centres the line box; --cap-centre-nudge lifts the
   title from there to where it reads centred. See that token for why it is
   half of the pure cap-centring value. */
.brand {
  padding: 22px 22px 18px;
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: center;
  /* Wraps rather than collides: "About Neighbourhood Communities" beside a long
     lockup will not fit every pane, and a second line is better than an
     overlap. The row gap is larger than the column gap because a wrapped row
     needs to read as a separate row, not as a crowded continuation. */
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 16px 12px;
  /* 26px: half way back from the step down to --t-title (20px) and the 32px
     it started at. Display size pushed the title bar onto two rows on any
     laptop under 1153px, and 20px bought that back but read small for the
     one heading this page has.
     Set on the LOCKUP, not on the h1: the mark is height:1em of this, so
     sizing only the text left the mark towering over the words and broke the
     mark-to-wordmark ratio the lockup was tuned for. Both step together. */
  font-size: 26px;
}
.brand h1 {
  margin: 0;
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: 1em;
  line-height: 1.05;
  letter-spacing: -0.005em;
  color: var(--charcoal-darkest);
}
/* Optical correction so the caps — not the line box — centre on the mark. */
.brand h1,
.mt-title { position: relative; top: var(--cap-centre-nudge); }

.brand-mark {
  flex: none;
  /* May be an <img> now, not only an inline <svg>: a church can supply markup
     or a hosted file. Both sit in the lockup the same way. */
  display: block;
  object-fit: contain;
  /* Above cap height, per the nav lockups in Binance / NYT / F1 — the mark
     leads the wordmark rather than sitting inside it. */
  height: 1em;
  width: auto;
  color: var(--charcoal-darkest);
}

.panel { padding: 16px 22px; border-bottom: 1px solid var(--line); }
/* The field and the assistant share a row. 12px, matching the gap between
   the filter controls below them, so the two rows read as one system. */
.panel-search { padding-bottom: 14px; display: flex; align-items: center; gap: 12px; }
.panel-search .search { flex: 1; min-width: 0; }

.search { display: flex; align-items: center; gap: 9px; position: relative; }
.search svg { width: 17px; height: 17px; color: var(--text-3); flex: none; position: absolute; left: 12px; }
.search input {
  font: inherit;
  font-size: var(--t-small);
  width: 100%;
  padding: 10px 12px 10px 37px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--scheme-fg);
  color: var(--text);
}
.search input:focus-visible { outline: 2px solid var(--lakeside); outline-offset: -1px; }
.search input::-webkit-search-cancel-button { cursor: pointer; }
.panel h2 {
  margin: 0 0 11px; font-size: var(--t-label); color: var(--text-3);
  display: flex; justify-content: space-between; align-items: baseline;
}
.panel h2 em { font-style: normal; font-family: var(--font-body); font-size: var(--t-micro); }

/* The Home Groups total reads as part of its own label rather than as a value
   parked at the far edge, so this heading opts out of the space-between the
   other panels use, and the count inherits the heading's size outright. */
.panel-grow h2 { justify-content: flex-start; }
.panel-grow h2 em {
  font-family: inherit;
  font-size: inherit;
  color: inherit;
  margin-left: 0.45em;
}

/* Live counts beside a category row or a tag option. Same size as the label
   they belong to — only the colour steps back, so the number reads as
   secondary without becoming a second, smaller typographic scale. */
.opt-count {
  font-style: normal;
  font-size: inherit;
  color: var(--text-3);
  margin-left: 0.35em;
  font-variant-numeric: tabular-nums;
}

/* No rule under the list. It is the last thing in the pane, so that border
   separated the content from nothing at all — and in export mode .panel-foot
   brings its own border-top, so nothing is lost by dropping it. */
.panel-grow { flex: none; display: flex; flex-direction: column; border-bottom: 0; }
/* Pinned so "Print view" is reachable without scrolling to the end of 18
   groups, and so it does not drift mid-list as facets expand and collapse. */
#app:not([data-export="on"]) .panel-foot { display: none; }

.panel-foot {
  border-bottom: 0; margin-top: auto;
  position: sticky; bottom: 0; z-index: 2; isolation: isolate;
  background: var(--paper-light);
  border-top: 1px solid var(--line);
}

/* ── Legend ──────────────────────────────────────────────────────────────── */

/* ── Key + filter ────────────────────────────────────────────────────────
   One control, not two. Each row is: checkbox, a clear gap, then the swatch
   bound tight to its label — 12px against 7px, so the swatch reads as part of
   the words rather than as a second control sitting next to the checkbox.

   Its own dot sizes rather than the .swatch classes: those are 24px with 2px
   borders and read as heavy blobs at this scale. .swatch itself stays, because
   renderPrintLegend() reuses it to stop the two legends drifting apart. */
/* ── Status filter ───────────────────────────────────────────────────────
   Bottom of the filter panel, one horizontal row: a quiet label, then the
   three toggles. It sits below the tag filters and reads lighter than them
   because it is the filter every church has and most people leave alone —
   a footnote to the panel rather than a fifth dropdown.

   Wraps rather than scrolls or truncates: at 375px three labels this long do
   not fit on one line, and a filter you cannot see is a filter nobody uses. */
.status-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px 18px;
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
.status-label {
  font-size: var(--t-micro);
  color: var(--text-3);
  flex: none;
  /* Centre-aligned rather than baseline-aligned: the row's controls are dots
     and checkboxes, which have no baseline of their own, so aligning to one
     left the label sitting high against them. */
  position: relative;
  top: var(--cap-centre-nudge, 0px);
}

/* Horizontal inside the row. It was a stacked list under the group heading,
   where it doubled as the key; the swatches still carry that job here. */
.key { display: flex; flex-wrap: wrap; align-items: center; gap: 9px 30px; margin: 0; }
.key label {
  display: flex;
  align-items: center;
  font-size: var(--t-small);
  cursor: pointer;
  user-select: none;
}
/* The -1px is optical, not arbitrary. align-items: center lines these up with
   the label's LINE BOX, whose centre measures ~1px below the centre of the
   caps themselves — so the control and swatch read as sitting slightly low
   against the text. Measured at both breakpoints; the residual either way is
   under a quarter pixel. */
.key input {
  accent-color: var(--lakeside);
  width: 15px;
  height: 15px;
  flex: none;
  margin: 0 12px 0 0;
  position: relative;
  top: -1px;
}
.key-dot { top: -1px; }
/* ── Category dot ────────────────────────────────────────────────────────
   ONE design, worn by both the key and the group list. They were two
   different marks for the same three categories — the list drew closed as a
   hollow ring with a solid border, the key as a dashed one — which meant the
   swatch you learned in the key was not the swatch you then scanned for in
   the list. Size and placement stay per-context below; only the design is
   shared. */
.cat-open {
  background: color-mix(in srgb, var(--cat-open-fill) 32%, transparent);
  border: 1.5px solid var(--cat-open-line);
}
/* The outer ring is how launching reads on the map, so the dot carries it too
   — at 1px, since 2.5px on a 10px dot closes into a solid disc. */
.cat-launching {
  background: color-mix(in srgb, var(--cat-launching-fill) 42%, transparent);
  border: 1.5px solid var(--cat-launching-line);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--cat-launching-line) 38%, transparent);
}
.cat-closed { background: color-mix(in srgb, var(--cat-closed-fill) 45%, transparent); }
/* A conic gradient masked to a ring, NOT `border: dashed`. The browser picks
   its own dash length for a border, and on a circle this small that came out
   as four chunky arcs that read more like a broken shape than a dashed one.
   An angular pattern is specified rather than inferred, so the count is 8 at
   every size instead of drifting with the diameter. */
.cat-closed::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: repeating-conic-gradient(
    var(--cat-closed-line) 0 26.1deg,
    transparent 26.1deg 45deg
  );
  -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1.5px));
  mask: radial-gradient(farthest-side, transparent calc(100% - 1.5px), #000 calc(100% - 1.5px));
}

.key-dot {
  /* 14px matches .group-item .dot exactly. Same design AND same size, so the
     swatch learned here is literally the mark scanned for in the list. */
  width: 14px;
  height: 14px;
  border-radius: 50%;
  box-sizing: border-box;
  flex: none;
  position: relative;
  margin-right: 7px;
}

.swatch { width: 24px; height: 24px; border-radius: 50%; flex: none; box-sizing: border-box; }
.swatch-open {
  background: color-mix(in srgb, var(--cat-open-fill) 32%, transparent);
  border: 2px solid var(--cat-open-line);
}
.swatch-launching {
  background: color-mix(in srgb, var(--cat-launching-fill) 42%, transparent);
  border: 2px solid var(--cat-launching-line);
  box-shadow: 0 0 0 2.5px color-mix(in srgb, var(--cat-launching-line) 38%, transparent);
}
.swatch-closed {
  background: color-mix(in srgb, var(--cat-closed-fill) 45%, transparent);
  border: 2px dashed var(--cat-closed-line);
}


/* ── Filters ─────────────────────────────────────────────────────────────── */



/* ── Group list ──────────────────────────────────────────────────────────── */

.group-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  /* Three columns, dropping to two then one at measured widths.
     
     The number that matters is 208px: the longest unbreakable word in Reach's
     group names is "Issaquah/Sammamish" at 175px in the heading face, plus the
     card's 13px padding either side. Narrower than that and a three-up grid
     starts breaking names across lines that should not break.
     
     Turned into viewport breakpoints because the pane is exactly half the
     window now — 3x208 + 2x16 gap + 44 padding, doubled, is 1400; the same sum
     for two columns is 952. Both are exact, not guesses.
     
     repeat(auto-fill, minmax(max(208px, (100% - 34px) / 3), 1fr)) expresses
     this against the CONTAINER and would be better, but the percentage inside
     max() does not clamp in practice — measured at nine widths, every one of
     them still came out three columns, down to 121px cards. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 16px;
}
/* 2x208 + 16 gap + 44 padding, doubled. */
@media (max-width: 1400px) {
  .group-list { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 952px) {
  .group-list { grid-template-columns: 1fr; }
}

.group-row { list-style: none; }

/* The row is a <button>; these resets make it look like the list item it is
   while keeping the semantics and keyboard behaviour of a real control. */
.group-row { display: flex; }

/* Both the <button> and the <a> form — see groupRow(). The two must be
   indistinguishable: a visitor should not be able to tell from the card that
   this group is missing an address in Planning Center. */
.group-item {
  display: flex;
  text-decoration: none;
  flex-direction: column;
  height: 100%;
  width: 100%;
  padding: 0;
  text-align: left;
  background: var(--scheme-fg);
  border: 1px solid var(--line);
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  font: inherit;
  color: inherit;
  transition: border-color 120ms ease, box-shadow 120ms ease;
}
.group-item:hover { border-color: var(--line-strong); box-shadow: 0 2px 10px rgba(0,0,0,0.06); }
/* The visual state lives on .is-active below; aria-current, which the list now
   sets alongside it, is what announces it. This rule carried a blue ring under
   an attribute nothing ever set, and is gone rather than left to fight it. */

.g-thumb { display: block; aspect-ratio: 16 / 10; background: var(--paper); }
.g-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* No invented imagery for a group without a photo: a tinted panel carrying
   the category dot keeps the grid even and says "no photo" honestly. */
.g-thumb-empty { display: grid; place-items: center; width: 100%; height: 100%; }
.g-thumb-empty .dot { width: 22px; height: 22px; position: relative; }

/* Fills whatever height the row settles on, so the footer can pin to the
   bottom and the pills line up across a row instead of floating at the end of
   each card's own text. */
.g-info { display: flex; flex-direction: column; gap: 5px; padding: 12px 13px 13px; flex: 1; }
/* The heading face, same as "Home Groups" beside the mark. The title is what
   people scan; setting it in the body mono at 13px made every card read as an
   undifferentiated block of text. */
.g-name {
  /* A name longer than its column WRAPS. The card clips its overflow, so
     without this "20s&30s in Issaquah/Sammamish" simply lost its end — and no
     minimum column width can be chosen that survives every church's naming.
     The grid sizing above keeps this from being needed often; this keeps it
     from ever being a truncated word. */
  overflow-wrap: break-word;
  margin: 3px 0 6px;
  font-family: var(--font-heading);
  font-weight: 400;
  font-size: 22px;
  line-height: 1.12;
  letter-spacing: -0.005em;
  color: var(--charcoal-darkest);
}
.g-when { font-size: var(--t-micro); color: var(--text-3); line-height: 1.4; }
/* Status leads the card. Its own row rather than sharing one with the city,
   so it reads as a state rather than as another piece of metadata. */
.g-status { display: flex; }
/* Tags sit last and stay quiet: they are opt-in detail, not the headline. */
.g-tags { margin-top: auto; padding-top: 8px; }
.g-area { font-size: var(--t-micro); color: var(--text-3); }

/* Tag chips. Off for every tag group until a church opts one in, so most maps
   never render these at all. */
.g-tags { display: flex; flex-wrap: wrap; gap: 5px; }
.tag-chip {
  font-size: var(--t-micro);
  line-height: 1.5;
  padding: 0 8px;
  border-radius: 999px;
  background: var(--paper);
  border: 1px solid var(--line);
  color: var(--text-2);
}

/* The popup reuses the card wholesale, so it inherits the card's internals and
   only needs its own outer frame. */
.iw-card { display: flex; flex-direction: column; }
.iw-card .g-thumb { border-radius: 0; }
.iw-card .g-info { padding: 12px 14px 10px; }

/* With launching switched off map-wide, its key row and its column in the
   dashboard are not "empty", they are meaningless. */
#app[data-launching="off"] .key label[data-cat="launching"] { display: none; }

.pill {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 8px;
  border-radius: 999px;
  border: 0;
  white-space: nowrap;
}
/* Every status colour on this map comes from ONE pairing per category,
   generated per tenant in /theme.css: the circle on the map, the dot in the
   key and this pill all read the same four values. They used to be three
   separate definitions, which is how the map, the key and the card ended up
   disagreeing about what "closed" looks like. */
.pill-open { background: var(--cat-open-wash); color: var(--cat-open-ink); }
.pill-launching { background: var(--cat-launching-wash); color: var(--cat-launching-ink); }
.pill-closed { background: var(--cat-closed-wash); color: var(--cat-closed-ink); }

/* No fill on either state, and that is the whole fix.
   Both used to tint toward paper, which worked when the pane was near-white
   and paper was a tint sitting ON a card. The pane IS paper now, so a paper
   fill did not tint the card — it dissolved it into the background. Select a
   group, come back to the list, and the card you had chosen was the one that
   had stopped looking like a card.
   The cards stay white and hover lifts. Selected gets NOTHING — no fill, no
   ring, no bar. Three versions of this were tried and each was worse than the
   last: paper fill (dissolved the card into the pane), blue ring (announced
   like a call to action), grey ring (quieter, still an outline nobody asked
   for). The card you last tapped does not need marking. The map already moved
   to it, and an outline arriving on a card you have moved on from is the app
   telling you something you knew and no longer care about.
   .is-active stays in the markup — the map and aria-current read it. */



/* Hidden, not ghosted. One checkbox removing a circle from the map while
   leaving its row visible at low opacity makes a single control mean two
   different things in two panes — and 34% opacity fails contrast anyway. */
.group-row:has(.group-item.is-dim), .group-item.is-dim { display: none; }

.list-empty {
  padding: 14px 9px; font-size: var(--t-small); line-height: 1.5;
  color: var(--text-3);
}

/* ── Buttons ─────────────────────────────────────────────────────────────── */

.btn {
  appearance: none; border: 1px solid var(--line-strong); background: var(--surface);
  color: var(--text); font-size: var(--t-label);
  padding: 11px 15px; border-radius: var(--radius); cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
}
.btn:hover { background: var(--paper); }
/* Primary actions all use the Reach button scheme defined below (.btn-reach).
   Only the full-width layout differs, since these sit in a sidebar column. */
.btn-primary {
  width: 100%;
  background: var(--lakeside);
  border: 1px solid var(--lakeside-dark);
  border-bottom: 0;
  border-radius: 6px;
  box-shadow: 0 4px 0 0 var(--lakeside-dark);
  color: #fff;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  text-transform: var(--button-case, none);
  letter-spacing: normal;
  transition: box-shadow 140ms ease, border-color 140ms ease;
}
.btn-primary:hover {
  background: var(--lakeside);
  border-top-color: transparent;
  border-right-color: transparent;
  border-left-color: transparent;
  box-shadow: 0 0 0 0 var(--lakeside-dark);
}

.foot-note { margin: 11px 0 0; font-size: var(--t-micro); color: var(--text-3); line-height: 1.5; }

/* ── Map ─────────────────────────────────────────────────────────────────── */

#map-wrap {
  position: relative; min-width: 0; overflow: hidden;
  background: var(--bg);
}

/* Centring applies ONLY in print mode. Applied unconditionally it turned
   #print-stage into a flex item that collapsed to its content height, which
   took the live map down to ~20px tall. `safe` centring keeps an oversized
   artboard's top-left reachable instead of overflowing past the scroll origin. */
#app[data-mode="print"] #map-wrap {
  display: flex; align-items: safe center; justify-content: safe center;
  overflow: auto;
  /* Reserve room for the floating toolbar so the artboard is never tucked
     underneath it. --print-bar-h is measured at runtime, because the bar wraps
     to two rows on narrow windows and a fixed guess would be wrong at one
     width or the other. */
  padding-top: calc(var(--print-bar-h, 56px) + 26px);
  padding-bottom: 26px;
}
#print-frame { position: relative; width: 100%; height: 100%; }
#map { position: absolute; inset: 0; }

#map-error {
  position: absolute; inset: 0; display: grid; place-items: center;
  padding: 40px; text-align: center; color: var(--text-2); font-size: var(--t-body);
}
#map-error code {
  display: block; margin-top: 12px; padding: 11px 13px; border-radius: var(--radius);
  background: var(--surface); border: 1px solid var(--line); font-size: var(--t-micro); color: var(--text);
}

/* Info window — quiet down Google's default chrome. */
.gm-style .gm-style-iw-c { border-radius: var(--radius); padding: 0; box-shadow: var(--shadow); }
.gm-style .gm-style-iw-d { overflow: hidden !important; }
/* No horizontal padding. The wrapper's 16px inset is left from the bespoke
   popup this used to be; now that the popup renders the SAME card as the
   directory, that inset showed up as a white margin down both sides of the
   photo. The card supplies its own padding, so the only job left here is the
   room under the call-to-action. */
.iw { padding: 0 0 16px; width: 100%; box-sizing: border-box; font-family: var(--font-body); }

/* Reach's primary button, ported from .button on reachchurch.cc:
     background   Lakeside #3886ce
     border       1px lakeside-dark on three sides (bottom is style:none)
     box-shadow   0 4px 0 lakeside-dark   ← the 4px "lift"
     radius       6px
     type         Courier Prime 400, uppercase, letter-spacing normal
   On hover the guide collapses the shadow to 0 and fades the side borders, so
   the button presses down. Using box-shadow rather than a border for the lift
   means that press causes no layout shift. Padding follows .button.is-small
   (8px 20px), which suits a 320px card. */
.iw a.iw-cta { margin: 2px 14px 0; }

/* Google focuses the card's first control when it opens. For keyboard users
   that is right and the ring should show; after a mouse click it is a stray
   outline on a button the user just aimed at. The ring is offset clear of the
   button's 4px lift so the two never overlap into a muddle. */
.iw a.iw-cta:focus-visible {
  outline: 2px solid var(--lakeside-dark);
  outline-offset: 3px;
}
#app[data-input="pointer"] .iw a.iw-cta:focus-visible { outline: none; }

/* Google gives the InfoWindow container a tabindex, so the global focus ring
   matched it and drew a border around the whole card whenever one opened. The
   ring belongs on the control a keyboard user is actually on, not on the
   surface containing it. */
#app[data-input="pointer"] .gm-style-iw-c:focus-visible,
#app[data-input="pointer"] .gm-style-iw-c *:focus-visible { outline: none; }
.gm-style .gm-style-iw-c:focus { outline: none; }

/* ── Print mode ──────────────────────────────────────────────────────────── */

/* Covered by the blanket [hidden] rule at the top of this file. */

#print-bar {
  position: absolute; top: 14px; left: 50%; transform: translateX(-50%);
  z-index: 30; display: flex; align-items: center; gap: 14px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--radius); padding: 10px 14px; box-shadow: var(--shadow);
  font-size: var(--t-small); white-space: nowrap;
}
#print-bar label { display: flex; align-items: center; gap: 6px; color: var(--text-2); }
#print-bar select {
  font-family: var(--font-body); font-size: var(--t-small); padding: 5px 7px;
  border-radius: 6px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--text);
}
#print-bar .chk { gap: 5px; }
#print-bar .spacer { width: 4px; }

#print-overlay { display: none; }

#app[data-mode="print"] { grid-template-columns: 1fr; }
#app[data-mode="print"] #sidebar { display: none; }
/* And the assistant, all of it. The badge is fixed at z-index 63 against a
   print bar at 30, so in export mode it floated over the poster — measured at
   (740, 824) on a 1440x900 window, sitting on the artboard it is supposed to
   be beside. Nothing about asking for a recommendation belongs in a view whose
   whole purpose is to be captured as an image, so it goes rather than being
   restacked: a lower z-index would still leave it on the page, and on top of
   the map it is drawn over. */
#app[data-mode="print"] .ask-fab-dock,
#app[data-mode="print"] .ask-fab,
#app[data-mode="print"] .ask-drawer { display: none; }

#app[data-mode="print"] #print-frame {
  background: var(--paper); box-shadow: 0 3px 34px color-mix(in srgb, var(--shadow-ink) 15%, transparent); flex: none;
}
#app[data-mode="print"] #print-overlay {
  display: block; position: absolute; inset: 0; pointer-events: none; z-index: 5;
}

.print-title {
  position: absolute; top: 34px; left: 36px;
  background: color-mix(in srgb, var(--paper-lightest) 94%, transparent);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 20px 26px 18px; box-shadow: 0 1px 12px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
}
.print-title h2 {
  margin: 0; font-family: var(--font-heading); font-weight: 400;
  font-size: var(--t-display-print); line-height: 1.02; color: var(--charcoal-darkest);
}
.print-title p { margin: 9px 0 0; font-size: var(--t-title); color: var(--text-3); }

.print-legend {
  position: absolute; left: 36px; bottom: 46px;
  background: color-mix(in srgb, var(--paper-lightest) 94%, transparent);
  border: 1px solid var(--line); border-radius: var(--radius);
  padding: 17px 22px; box-shadow: 0 1px 12px color-mix(in srgb, var(--shadow-ink) 7%, transparent);
  display: grid; gap: 10px; min-width: 224px;
}
.print-legend .pl-row { display: flex; align-items: center; gap: 12px; font-size: var(--t-title); }
.print-legend .pl-row em { font-style: normal; font-family: var(--font-body); font-size: var(--t-small); color: var(--text-3); margin-left: auto; }
.print-legend .pl-note {
  font-size: var(--t-micro); color: var(--text-3); line-height: 1.5;
  border-top: 1px solid var(--line); padding-top: 10px; margin-top: 1px;
}

/* Group-name labels drawn over the map in print mode. */
/* ONE label treatment for every group, wherever it ends up sitting.
   Previously displaced labels got a bordered white plate while centred ones
   were plain text, so the map carried two competing styles at once — which is
   what made it look cluttered. Following the pattern Tide Guide uses for
   multi-feature map labelling: quiet text on a halo, no box, no border, placed
   adjacent to its feature. Size and width are set per-label in JS from the
   circle's current pixel radius. */
.map-label {
  position: absolute;
  transform: translate(-50%, -50%);
  font-family: var(--font-body);
  font-weight: 700;
  line-height: 1.22;
  text-align: center;
  color: var(--charcoal-darkest);
  /* A thick halo is what lets plain text stay legible over a circle fill, a
     road, or water without needing a plate behind it. */
  text-shadow:
    0 0 3px var(--paper), 0 0 3px var(--paper), 0 0 4px var(--paper),
    0 0 6px var(--paper), 0 0 10px var(--paper);
  pointer-events: none;
}


/* Interactive-only Google controls are hidden while printing. Attribution and
   the Google logo stay visible — the Maps Platform terms require them to
   remain on any map image you produce. */
#app[data-mode="print"] .gm-control-active,
#app[data-mode="print"] .gmnoprint:not(.gm-style-cc),
#app[data-mode="print"] .gm-fullscreen-control { display: none !important; }

/* ── Info-window media ───────────────────────────────────────────────────── */

/* The close button is removed entirely. The card already dismisses on a map
   click, a drag, a zoom, or Escape, so the control was a redundant target — and
   it sat on top of the group photo, needing a scrim that darkened the image
   purely to keep itself readable. Collapsing the header row also lets the photo
   start at the very top of the card. */
.gm-style .gm-style-iw-chr { display: none !important; }

   translucent disc behind the button itself, which is the smallest surface
   that can do the job — a gradient pool over the corner darkened far more of
   the image than the 34px control needed. */

/* ── Focus ───────────────────────────────────────────────────────────────── */

/* :focus-visible rather than :focus, so the ring appears for keyboard and
   assistive-tech users without flashing on every mouse click. It is kept, not
   removed — this is a public church page and losing the focus indicator would
   make it unusable without a mouse. */
:where(a, button, input, select, [tabindex]):focus-visible {
  outline: 2px solid var(--lakeside);
  outline-offset: 2px;
  border-radius: 3px;
}
.iw a:focus-visible { outline-color: var(--lakeside-dark); }

/* ── In-circle status badges ─────────────────────────────────────────────── */

.circle-badge {
  position: absolute;
  transform: translate(-50%, -50%);
  font-family: var(--font-tagline);
  text-transform: uppercase;
  font-weight: 400;
  letter-spacing: 0.02em;
  line-height: 1;
  white-space: nowrap;
  pointer-events: none;
  user-select: none;
}

/* NEW reads bright: full-strength Mustard Seed with a paper halo so it stays
   vivid against the fill rather than muddying into it. */
.circle-badge.is-launching {
  color: var(--mustard-dark);
  text-shadow:
    0 0 3px var(--paper-lightest), 0 0 3px var(--paper-lightest),
    0 0 6px var(--paper-lightest), 0 0 10px var(--paper-lightest);
}

/* ── Tag facet filters ───────────────────────────────────────────────────── */

/* Clear sits BESIDE the word Filter, not pushed to the far edge. Out at the
   right it read as page furniture; next to the label it reads as an action on
   the thing the label names. */
.panel-facets h2 { align-items: center; justify-content: flex-start; gap: 12px; }

/* Horizontal bar. Wraps rather than scrolls: a wrapped filter is still
   reachable, a horizontally-scrolled one is frequently missed entirely. */
.facet-bar { display: flex; flex-wrap: wrap; gap: 9px; align-items: center; }

.facet-bar .facet { position: relative; border-top: 0; }
.facet-bar .facet > summary {
  padding: 9px 13px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--scheme-fg);
  gap: 10px;
  white-space: nowrap;
}
.facet-bar .facet[open] > summary { border-color: var(--lakeside); background: var(--scheme-fg); }
.facet-bar .facet > summary:hover { border-color: var(--charcoal-lighter); }

/* Overlaid, not inline: in a horizontal bar an inline panel would shove every
   other control out of position the moment one opened. */
.facet-bar .facet-options {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 25;
  /* Sized to its content, up to double the floor. The fixed 210px was breaking
     "Location-Based" across two lines, and a filter option that wraps reads as
     two options. Capped so one long tag name cannot run a panel off-screen. */
  width: max-content;
  min-width: 210px;
  max-width: 420px;
  max-height: 320px;
  overflow-y: auto;
  padding: 12px 14px;
  background: var(--scheme-fg);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  box-shadow: 0 6px 22px rgba(0, 0, 0, 0.12);
}
/* The last one would otherwise open off the right edge of the pane. */
.facet-bar .facet:last-child .facet-options { left: auto; right: 0; }

/* Only ever on screen when something IS filtered, so it can afford to be a
   real control rather than a hint. As an underlined word it was quieter than
   the filter chips it undoes — which is backwards, since it is the way out of
   a state the visitor may not have meant to be in. Body face, not the tagline
   face of the label beside it: this is a button, not a heading. */
/* ── Group type ──────────────────────────────────────────────────────────────
   Single-select, so its summary carries the CHOSEN type rather than the facet
   name, and the (i) sits inside the control because it describes that choice.
   Anywhere else it would look like it described the filter panel. */
/* Only while OPEN. The `:has(.type-info)` version outlined the control simply
   because a description existed for the chosen type, so a map that opened on a
   default type opened with one filter ringed in blue for no reason a visitor
   could act on. Blue here means "you are in this control", nothing else. */
.facet-bar .facet-type[open] > summary { border-color: var(--lakeside); }

/* The glyph rides in its own box. Centring the BUTTON's text centres its line
   box, and a line box carries descender space that a lowercase "i" never uses,
   so the letter sat visibly high. Same trap as the sheet close button, and the
   same fix: give the mark a box of its own and place it. */
.type-info {
  appearance: none; cursor: pointer; flex: none;
  display: grid; place-items: center;
  width: 16px; height: 16px; padding: 0; margin-left: 2px;
  border: 1px solid var(--lakeside); border-radius: 50%;
  background: var(--surface); color: var(--lakeside-dark);
  font-family: var(--font-body); font-size: 10px;
}
/* 0.83px, measured rather than eyeballed. A lowercase "i" in Courier Prime
   has 6.66px of ink above the baseline and none below, so at 10px/1 its ink
   centre sits 0.83px ABOVE the centre of its own box: place-items: center
   centres the BOX, and the letter reads high inside the circle. Nudging the
   glyph down by that much is the same as lifting the circle, which is how it
   looks from the outside.

   Horizontally it is already centred to within 0.14px, which is below anything
   the eye can resolve at this size, so nothing is done about it. */
.type-info span { display: block; line-height: 1; position: relative; top: 0.83px; }
.type-info:hover { background: var(--lakeside-lightest); }
.type-info:focus-visible { outline: 2px solid var(--lakeside); outline-offset: 2px; }

/* The (i) beside the list heading, for a church with ONE group type. There is
   no dropdown to hang it off then, but the description is still the church's
   own words about what these groups are — and the heading is where they are
   named. */
.panel-grow h2 .type-info { margin-left: 8px; position: relative; top: -1px; }
.panel-grow .type-note { margin: 0 0 12px; }

/* The church's own words, from Planning Center. Sits under the bar rather than
   floating over it: it can run to a paragraph, and a tooltip that long is a
   panel pretending not to be one. */
.type-note {
  flex-basis: 100%;
  margin-top: 2px;
  padding: 12px 14px;
  background: var(--scheme-fg);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  font-size: var(--t-small);
  line-height: 1.55;
  color: var(--text-2);
}
.type-note a { color: var(--lakeside-dark); }
.type-note p { margin: 0 0 8px; }
.type-note p:last-child { margin-bottom: 0; }

.facet-clear {
  appearance: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 5px;
  font-family: var(--font-body); font-size: var(--t-micro); line-height: 1;
  color: var(--lakeside-dark);
  background: var(--lakeside-lightest);
  border: 1px solid color-mix(in srgb, var(--lakeside) 38%, transparent);
  border-radius: 999px;
  padding: 5px 11px 5px 9px;
  transition: background 120ms ease, border-color 120ms ease;
}
.facet-clear svg { width: 10px; height: 10px; flex: none; }
.facet-clear:hover { background: var(--lakeside-lighter); border-color: var(--lakeside); }
.facet-clear:focus-visible { outline: 2px solid var(--lakeside); outline-offset: 2px; }

.facet { border-top: 1px solid var(--line); }
.facet:first-of-type { border-top: 0; }

/* 13px, not 9. Measured against Glassdoor, Workable and Shop, whose filter
   accordions all run ~44px rows; ours were 36.6px, which read cramped against
   a full-width divider and made a small click target for a control that is
   tapped as often as this one. */
.facet > summary {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 13px 0; cursor: pointer; user-select: none;
  font-size: var(--t-small); color: var(--text-2); list-style: none;
}
.facet > summary::-webkit-details-marker { display: none; }
/* A square with two borders, rotated — so its rendered width is side x sqrt(2).
   8px gives ~11.3px, which matches the references; 7px gave 9.9px and read
   undersized beside a 12px label.

   translateY(-25%) rather than the old translate(-2px,-2px): rotating about
   the centre leaves the V's point sitting low, and a PROPORTIONAL correction
   survives a size change. The fixed pixel version silently stopped centring
   the moment the box grew. Transforms apply right-to-left, so this rotates
   first and then lifts. */
.facet > summary::after {
  content: ""; flex: none; width: 8px; height: 8px; margin-left: auto;
  border-right: 1.6px solid var(--text-3);
  border-bottom: 1.6px solid var(--text-3);
  transform: translateY(-25%) rotate(45deg);
  transition: transform 140ms ease;
}
.facet[open] > summary::after { transform: translateY(25%) rotate(-135deg); }
.facet > summary:hover { color: var(--text); }

.facet-count:not(:empty) {
  font-style: normal; font-size: var(--t-micro);
  background: var(--lakeside); color: #fff;
  min-width: 16px; height: 16px; padding: 0 4px;
  border-radius: 20px; display: grid; place-items: center;
}

/* The old 2px left padding indented the options a hair off the summary above
   them, for no reason anyone could state. Flush left, and the gap opens up to
   match the roomier rows. */
.facet-options { display: grid; gap: 9px; padding: 3px 0 15px; }
.facet-options label {
  display: flex; align-items: center; gap: 9px;
  font-size: var(--t-small); cursor: pointer; line-height: 1.35;
  /* An option that wraps reads as two options. With the panel sized to its
     content this is what tells it how wide "its content" actually is. */
  white-space: nowrap;
}
/* Counts line up down the right edge, with a floor on the gap so a long name
   and its number never collide once the panel shrinks to fit. */
.facet-options .opt-count { margin-left: auto; padding-left: 16px; }
.facet-options input { accent-color: var(--lakeside); width: 14px; height: 14px; flex: none; }

/* ── Print stage (fit-to-viewport) ───────────────────────────────────────── */

/* #print-frame keeps its TRUE pixel size so the export renders at full
   resolution; #print-stage carries the visually scaled dimensions so layout,
   centring and scrolling behave. Without this an A3 or portrait frame is simply
   larger than the window and its top-left is unreachable. */
#print-stage { position: relative; width: 100%; height: 100%; }
#app:not([data-mode="print"]) #print-stage { position: absolute; inset: 0; }
#app[data-mode="print"] #print-stage { flex: none; }
#app[data-mode="print"] #print-frame { transform-origin: top left; }

.print-attribution {
  position: absolute; right: 10px; bottom: 8px; z-index: 6;
  font-family: var(--font-body); font-size: 11px; color: #5f5f5c;
  text-shadow: 0 0 3px var(--paper-lightest), 0 0 3px var(--paper-lightest);
  display: none;
}
#app[data-mode="print"] .print-attribution { display: block; }

/* Export and Done sit side by side, so they match in height and footprint —
   different widths made the pair look accidental. */
/* Export and Done sit side by side and share a HEIGHT, not a width — each
   sizes to its own label. Height is pinned rather than left to intrinsic
   content because the two carry different classes and were otherwise
   resolving to 41px and 38px. */
.btn-inline {
  width: auto;
  height: 42px;
  padding: 0 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  line-height: 1;
}
#print-bar .scale-note {
  font-size: var(--t-micro); color: var(--text-3); font-variant-numeric: tabular-nums;
}
#btn-export[disabled] { opacity: 0.55; cursor: progress; }

/* ── Scrollbars ──────────────────────────────────────────────────────────── */

/* macOS hides overlay scrollbars until you scroll, so a long sidebar reads as
   "the list is only three groups". A permanently visible, quiet track is the
   discoverability cue. */
#sidebar { scrollbar-width: thin; scrollbar-color: var(--charcoal-lighter) transparent; }
#sidebar::-webkit-scrollbar { width: 10px; }
#sidebar::-webkit-scrollbar-track { background: transparent; }
#sidebar::-webkit-scrollbar-thumb {
  background: var(--charcoal-lighter); border-radius: 10px; border: 3px solid var(--surface);
}
#sidebar::-webkit-scrollbar-thumb:hover { background: var(--text-3); }

.panel-foot::before {
  content: ""; position: absolute; left: 0; right: 0; top: -14px; height: 14px;
  background: linear-gradient(to top, var(--paper-light), transparent);
  pointer-events: none;
}

/* ── Mobile title bar ────────────────────────────────────────────────────── */

/* Desktop shows the sidebar header instead. */
#mobile-titlebar { display: none; }

/* ── Mobile bottom sheet ─────────────────────────────────────────────────── */

/* Hidden on desktop, where the sidebar is simply always present. */
.sheet-head { display: none; }

/* ── Reach button scheme ─────────────────────────────────────────────────── */

/* Ported from `.button` on reachchurch.cc/style-guide, in one place so every
   primary action shares it:
     background   Lakeside #3886ce
     border       1px lakeside-dark on three sides (bottom is style:none)
     box-shadow   0 4px 0 lakeside-dark   ← the 4px "lift"
     radius       6px
     type         Courier Prime 400, 16px, uppercase, letter-spacing normal
   On hover the guide collapses the shadow and fades the side borders so the
   button presses down. Using box-shadow for the lift means no layout shift. */
.btn-reach {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.75rem;
  padding: 10px 24px;
  background: var(--lakeside);
  border: 1px solid var(--lakeside-dark);
  border-bottom: 0;
  border-radius: 6px;
  box-shadow: 0 4px 0 0 var(--lakeside-dark);
  color: #fff;
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  text-transform: var(--button-case, none);
  letter-spacing: normal;
  text-decoration: none;
  cursor: pointer;
  transition: box-shadow 140ms ease, border-color 140ms ease;
}
.btn-reach:hover {
  border-top-color: transparent;
  border-right-color: transparent;
  border-left-color: transparent;
  box-shadow: 0 0 0 0 var(--lakeside-dark);
}
/* .button.is-small in the guide. */
.btn-reach.is-small { padding: 8px 20px; }

/* .button.is-icon: the arrow sits after the label at 20px with an 8px gap.
   The column-gap override is the guide's own — .button's base gap is 12px and
   is-icon tightens it, because an icon needs less air than a second word. */
.btn-reach.is-icon { column-gap: 8px; }
.btn-arrow { width: 20px; height: 20px; flex: none; }

/* ── Secondary buttons ───────────────────────────────────────────────────────
   .button.is-secondary from reachchurch.cc/style-guide, read off the live page
   rather than eyeballed:
     background   transparent
     border       1px neutral-darkest at 15%, on three sides
     box-shadow   0 4px 0  same colour   ← the same 4px lift as the primary
     radius       6px
     type         Courier Prime 400, uppercase
     padding      10px 24px, or 8px 20px for is-small
   Hover differs from the primary in one telling way: the lift collapses to 1px
   rather than to 0, so the button presses but does not flatten. The side
   borders fade, which the primary's own :hover does for both.

   backdrop-filter: blur(10px) is the guide's too, and it earns its place here —
   these sit on a floating bar over a live map. */
.btn-secondary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 8px 20px;
  /* Paper, not transparent and not white. The guide sets transparent, which on
     its own paper page reads as paper — over this sidebar's near-white surface
     the same declaration read as a white button, which is the guide's SEPARATE
     is-alternate style. Naming the fill keeps it the button the guide shows. */
  background: var(--paper);
  backdrop-filter: blur(10px);
  border: 1px solid var(--secondary-line);
  border-bottom: 0;
  border-radius: 6px;
  box-shadow: 0 4px 0 0 var(--secondary-line);
  color: var(--charcoal-darkest);
  font-family: var(--font-body);
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  text-transform: var(--button-case, none);
  letter-spacing: normal;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: box-shadow 140ms ease, border-color 140ms ease;
}
.btn-secondary:hover {
  border-top-color: transparent;
  border-right-color: transparent;
  border-left-color: transparent;
  box-shadow: 0 1px 0 0 var(--secondary-line);
}
.btn-secondary:focus-visible { outline: 2px solid var(--lakeside-dark); outline-offset: 3px; }

/* ── Title-bar links ─────────────────────────────────────────────────────────
   Optional, and absent unless the church has set one. They wear the guide's
   secondary style at its small size, which is exactly the register wanted: a
   real button, quieter than anything that acts on the map itself. */
/* pointer-events: auto is not optional. #mobile-titlebar sets `none` so the
   bar never steals a map drag, and without this the menu inside it could not
   be tapped at all. */
/* The lockup as a link: it must not look like one. A masthead that goes home
   is an expectation, not a call to action, and underlining it would make the
   title compete with the buttons beside it. */
/* The mark and the wordmark, as one flex child of the header.
   Server-rendered, not assembled by app.js: the header spaces its children
   apart, so while the mark and the title were two separate children and the
   nav was still hidden, space-between threw the title against the right edge
   until the script ran and pulled it back left. A visible jump on every load,
   worst on a phone where the script lands last. */
.brand-lockup {
  display: flex; align-items: center; gap: inherit; min-width: 0;
}
.brand-home {
  display: flex; align-items: center; gap: inherit;
  color: inherit; text-decoration: none;
  /* Not optional, and the reason the masthead stopped going anywhere on a
     phone: #mobile-titlebar sets pointer-events: none so the bar never steals
     a drag meant for the map underneath it, and every child that IS meant to
     be touched has to say so. .bar-links does. This did not, so the link was
     in the DOM with the right href, styled, focusable — and inert. A tap on it
     landed on the bar behind it and nothing happened. */
  pointer-events: auto;
}
/* No hover state at all. A masthead going home is an expectation, not an
   invitation — recolouring it on hover makes the title look like it is
   offering something, which is the job of the buttons beside it. The cursor
   already says it is clickable. Focus still rings, because a keyboard user has
   no cursor to tell them. */
.brand-home:focus-visible { outline: 2px solid var(--lakeside); outline-offset: 4px; border-radius: 4px; }

/* No margin-left: auto. The lockup and the buttons are spaced apart by the
   header's own justify-content, which does the right thing on BOTH lines:
   sharing a line they sit at either end, and once the buttons wrap onto a line
   of their own they start at the left margin like everything else on the page.
   An auto margin would have kept them pinned right, hanging off the edge under
   a left-aligned title. */
.bar-links { display: flex; align-items: center; gap: 8px; pointer-events: auto; }
.bar-links[hidden] { display: none; }
.bar-link { font-size: 13px; padding: 7px 14px; }

/* One menu instead of two buttons, on a bar 375px wide. A menu holding a
   single item is a tap spent finding out there was nothing behind it, so this
   only ever appears when there are two — see renderBarLinks(). */
.bar-menu { position: relative; }
/* No border, no fill, no lift: on a phone this sits INSIDE the floating title
   bar, and a button-within-a-card reads as two nested surfaces. It should look
   like part of the bar it lives in. */
.bar-menu > summary {
  list-style: none;
  display: grid; place-items: center;
  width: 34px; height: 34px; padding: 0;
  margin-right: -6px;
  background: none;
  border: 0;
  box-shadow: none;
  border-radius: 6px;
  color: var(--charcoal-darkest);
  cursor: pointer;
}
.bar-menu > summary::-webkit-details-marker { display: none; }
.bar-menu > summary svg { width: 18px; height: 14px; }
/* No hover or open fill either. The panel it opens is the feedback, and a
   background swap on a 34px icon inside a floating card is one surface too
   many. Focus still shows a ring, because a keyboard user has nothing else. */
.bar-menu > summary:focus-visible { outline: 2px solid var(--lakeside); outline-offset: 2px; }
.bar-menu-panel {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  display: grid; gap: 2px; min-width: 190px; padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  box-shadow: var(--shadow);
}
.bar-menu-item {
  padding: 10px 12px; border-radius: 6px;
  font-family: var(--font-body); font-size: var(--t-small);
  color: var(--text); text-decoration: none; white-space: nowrap;
}
.bar-menu-item:hover { background: var(--paper); }


/* ── Mobile menu trigger ─────────────────────────────────────────────────── */

/* Base styles come BEFORE the media query on purpose: these rules and the
   ones inside @media have identical specificity, so source order decides the
   winner. With the query first, `display: none` here silently overrode it and
   the button never appeared. */
/* Sits over the map, so it stays deliberately compact: one line, tighter
   padding and a smaller type size than a full-width primary action. Losing
   `white-space: nowrap` when this moved onto .btn-reach is what let the label
   wrap onto two lines and balloon the button. */
#menu-fab {
  display: none;
  position: fixed;
  z-index: 55;
  left: 50%;
  transform: translateX(-50%);
  /* The style guide's `is-alternate`: white ground, black ink, a light neutral
     border and lip. It swaps places with the assistant, which has taken the
     primary blue — two buttons side by side cannot both be the primary action,
     and between "open the list" and "ask for help choosing", the second is the
     one worth the colour. */
  background: var(--scheme-fg);
  border-color: var(--charcoal-lighter);
  box-shadow: 0 4px 0 0 var(--charcoal-lighter);
  color: var(--scheme-text);
  /* 34, not 26. Google's attribution sits in the bottom-left corner of the map
     and its logo starts 26px up — so once the assistant badge grew to match
     this button's full height, aligned with it, the badge covered the top of
     that logo. Maps' terms require the attribution to stay visible, so the
     pair moves up together rather than the badge breaking alignment. */
  bottom: calc(34px + env(safe-area-inset-bottom, 0px));
  white-space: nowrap;
  padding: 7px 16px;
  gap: 9px;
  font-size: var(--t-body);
}
/* .btn-reach's hover collapses the lip and drops the border to transparent,
   which works on a blue button because the fill still holds the shape. On a
   white one over a map it would dissolve the button's own edge, so this
   collapses the lip and keeps the outline. */
#menu-fab:hover {
  border-top-color: var(--charcoal-lighter);
  border-right-color: var(--charcoal-lighter);
  border-left-color: var(--charcoal-lighter);
  box-shadow: 0 0 0 0 var(--charcoal-lighter);
}
#menu-fab .fab-bars { width: 15px; height: 12px; }

/* Same footprint and position as the FAB it replaces, so the action at the
   bottom of the screen does not appear to jump when the drawer opens. */
#btn-view-filtered {
  display: none;
  position: fixed;
  /* Above the drawer (60), which is the whole point — it floats over the list
     rather than scrolling away at the end of it. */
  z-index: 62;
  left: 50%;
  transform: translateX(-50%);
  /* Paper, where the Directory button it replaces is white. It appears OVER
     the open list, whose cards are white — a white button on white cards has
     no edge of its own, and this one needs to read as floating above them
     rather than sitting among them. Black ink either way, and neither wears
     the primary colour: the assistant is the only thing here that does. */
  background: var(--paper);
  border-color: var(--charcoal-lighter);
  box-shadow: 0 4px 0 0 var(--charcoal-lighter);
  color: var(--scheme-text);
  /* Level with the Directory button it replaces. */
  bottom: calc(34px + env(safe-area-inset-bottom, 0px));
  white-space: nowrap;
  padding: 7px 16px;
  gap: 9px;
  font-size: var(--t-body);
}
/* Keeps its own edge on hover, for the reason #menu-fab does: .btn-reach
   drops the border to transparent, which a blue fill survives and a white one
   does not. */
#btn-view-filtered:hover {
  border-top-color: var(--charcoal-lighter);
  border-right-color: var(--charcoal-lighter);
  border-left-color: var(--charcoal-lighter);
  box-shadow: 0 0 0 0 var(--charcoal-lighter);
}
#btn-view-filtered .fab-pin { width: 13px; height: 16px; flex: none; }

/* Drawn as an SVG rather than with borders. The border version squeezed three
   2px rules into an 11px box, leaving ~1.5px gaps that closed up into a solid
   block at phone density. */
.fab-bars {
  width: 18px;
  height: 14px;
  flex: none;
  overflow: visible;
}

/* Inverts the button it sits on: white fill, blue text, both straight from
   .btn-reach's own tokens (#fff is that button's text colour, --lakeside-dark
   its border and shadow).

   --lakeside-dark rather than --lakeside because this is an 11px numeral:
   plain --lakeside on white measures 3.84:1, under the 4.5:1 AA floor for
   text this size. --lakeside-dark measures 5.61:1 and is no less the brand
   blue — it is the step the button already uses. */
#fab-badge:not([hidden]) {
  font-style: normal; font-family: var(--font-body); font-size: var(--t-micro);
  background: #fff; color: var(--lakeside-dark);
  min-width: 18px; height: 18px;
  border-radius: 999px; display: grid; place-items: center;

  /* place-items: center centres the numeral's LINE BOX, and a line box carries
     descender space the digits never use — so measured against rendered ink
     the numeral sat 1.25px ABOVE the circle's centre (4.5px of white above it,
     7px below). Corrected from both ends: the circle rises 1px, and 1px of
     top padding pushes the numeral down half a pixel inside it. */
  position: relative;
  top: -1px;
  padding: 1px 5px 0;
}

/* Desktop keeps the export button and never shows the mobile Save. */
#btn-menu-save { display: none; }

/* Export is reached deliberately, from the dashboard's "Print and export"
   button, and is never offered to a visitor. Hidden by DEFAULT rather than
   revealed by JS, so it cannot flash into view before the check runs. */

/* Driven entirely by the media query and the export flag, with no JS deciding
   what counts as "desktop". That means resizing a window across the breakpoint
   corrects itself, where a width measured once at load would not. */
#export-notice { display: none; }

/* ── Mobile layout ───────────────────────────────────────────────────────── */

/* Under ~560px of PANE (not window) the bar cannot hold a row, so it stacks.
   Container queries would be exact; this approximates with the window, which is
   correct because the pane is a fixed fraction of it. */
/* Two columns, and the panels stay OVERLAID rather than pushing the page.
   Stacked one per row, five filters plus the status row filled the drawer
   before a single card was visible; two columns halve that. Overlaid because
   an in-flow panel opening between two columns shoves the other column's
   controls down the page while you are reading them. */
@media (max-width: 1100px) {
  .facet-bar { display: grid; grid-template-columns: 1fr 1fr; align-items: start; gap: 9px; }
  .facet-bar .facet > summary { justify-content: space-between; }
  /* The note the (i) opens spans both columns: it is a paragraph, not a
     control, and half a drawer is not enough width for one. */
  .facet-bar .type-note { grid-column: 1 / -1; }

  .facet-bar .facet-options {
    /* Wide enough to read, and never wider than the drawer. */
    width: max-content;
    min-width: 100%;
    max-width: min(320px, calc(100vw - 56px));
    max-height: 60vh;
  }
  /* Which way each panel opens is decided by alignFacetPanel() in app.js, by
     measurement. Two CSS parity rules lived here and both were wrong: the
     (i) note is a full-width grid item, so opening it moves every filter
     after it into the other column while the selector stays put. This is the
     starting position it measures from. */
  .facet-bar > .facet .facet-options { left: 0; right: auto; }
}

@media (max-width: 860px) {
  :root { --browser-chrome: calc(100lvh - 100dvh); }

  /* Map owns the screen; the sidebar is a drawer that is fully off-screen
     until the trigger is pressed. An earlier version rested it partly open,
     which put three overlapping affordances on screen at once (peek sheet,
     filter pill, Save) with no clear division of labour. */
  #app { grid-template-columns: 1fr; grid-template-rows: 1fr; }

  /* One card per row. The drawer is 375px wide at its narrowest, so a second
     column leaves ~157px per card — the photo stops carrying and the group
     name wraps to four lines. A single column also matches what a phone
     directory is: a list you thumb through, not a grid you scan. */
  .group-list { grid-template-columns: 1fr; }

  /* ── Full-bleed map on iOS Safari ──────────────────────────────────────
     The page box stays 100dvh so every fixed control keeps positioning
     against the reachable area. Only the MAP grows, to 100lvh — the height
     the viewport would have with Safari's bars retracted — so it paints
     underneath the bottom URL bar instead of stopping at it.

     Fixed rather than a tall grid item on purpose: out of flow, the extra
     height cannot make the document scrollable, so a vertical swipe still
     pans the map instead of scrolling the page out from under it.

     Google's logo and "Map data ©…" ride the map's new bottom edge into the
     strip below the URL bar, which stays visible — so attribution is still
     legible, as Maps Platform terms require. Do NOT lift them back up. */
  #map-wrap {
    position: fixed;
    inset: 0 0 auto 0;
    height: 100lvh;
    z-index: 0;
  }

  /* The zoom control is the one piece that must NOT ride down with the rest:
     anchored to the map's bottom, it would land behind the URL bar and stop
     being tappable. --browser-chrome is exactly the bar's height, and
     collapses to 0 where there is no browser chrome (desktop, and the
     Home Screen standalone app), making this rule self-cancelling there.
     Targets the bundle WRAPPER, not .gmnoprint: Google nests the camera and
     zoom controls inside .gm-bundled-control-on-bottom, so matching every
     .gmnoprint lifted the cluster once per nesting level. If Google ever drops
     the class the rule simply stops applying — the buttons sit low, which is
     survivable; a double lift is not. The attribution is deliberately not
     matched here, so it stays down in the visible strip. */
  #map .gm-bundled-control-on-bottom {
    transform: translateY(calc(-1 * var(--browser-chrome)));
  }

  #sidebar {
    position: fixed; inset: 0; z-index: 60;
    max-height: none; border-right: 0;
    transform: translateY(100%);
    transition: transform 280ms cubic-bezier(0.32, 0.72, 0, 1);
    will-change: transform;
    overscroll-behavior: contain;
  }
  #app[data-menu="open"] #sidebar { transform: translateY(0); }

  /* Zero height on purpose: the close button rides ON the Filter heading's row
     rather than sitting in a strip of its own above it. With the title gone
     that strip was pure gap — 56px of nothing between the X and the first
     thing it sat over. The row is a sticky overlay so the button stays put
     while the drawer scrolls, and pointer-events pass through everywhere the
     button itself is not. */
  .sheet-head {
    display: flex; align-items: flex-start; justify-content: flex-end;
    position: sticky; top: 0; z-index: 3;
    height: 0;
    padding: 0 22px 0 20px;
    overflow: visible;
    pointer-events: none;
  }
  /* The close button shares the SEARCH field's row rather than floating over
     it. 18px drops the 38px button's centre onto the input's centre: the
     panel's 16px top padding plus half the field's 42px height, less half the
     button. Measured, not eyeballed — re-measure if either changes. */
  .sheet-close { pointer-events: auto; position: relative; top: 18px; }

  /* And the field gives up the width the button needs, so the two sit side by
     side with a real gap instead of the X sitting on top of the input. 22px
     panel padding + 38px button + 12px gap. */
  .panel-search { padding-right: 72px; }
  /* An SVG rather than a "&times;" glyph. The character's own metrics sit off
     centre inside its line box, so `place-items: center` centred the box and
     left the mark visibly low and right of the circle's middle. A drawn cross
     is centred by construction. */
  .sheet-close {
    appearance: none; border: 1px solid var(--line-strong); background: var(--scheme-fg);
    width: 38px; height: 38px; border-radius: 50%;
    color: var(--text-2);
    display: grid; place-items: center; cursor: pointer; flex: none;
    padding: 0;
  }
  .sheet-close svg { width: 20px; height: 20px; display: block; }
  .sheet-close:hover { background: var(--paper); color: var(--text); }

  /* The sidebar header is replaced on mobile by the floating title bar, so only
     one of the two is ever in the accessibility tree. */
  .brand { display: none; }

  /* Floating card over the map, matching reachchurch.cc's nav: inset 20px from
     the sides, white, 8px radius, 1px paper-dark border. Reach's own bar sits
     17px from the top; here it also clears the notch. */
  #mobile-titlebar {
    display: flex;
    flex-direction: row;
    align-items: center;
    /* The lockup at one end, the menu at the other. Without this the menu sat
       immediately after the title instead of against the right edge, because
       .bar-links lost its auto margin when the desktop header started spacing
       its own children. */
    justify-content: space-between;
    gap: 11px;
    font-size: 26px;
    position: fixed;
    z-index: 50;
    top: calc(14px + env(safe-area-inset-top, 0px));
    left: 20px;
    right: 20px;
    padding: 11px 20px;
    background: var(--paper-lightest);
    border: 1px solid var(--paper-dark);
    border-radius: 8px;
    pointer-events: none; /* purely a label — never steals a map drag */
  }
  /* Hidden behind the open drawer, which carries its own header. */
  #app[data-menu="open"] #mobile-titlebar { display: none; }

  .mt-title {
    font-family: var(--font-heading);
    font-weight: 400;
    font-size: 1em;
    line-height: 1.05;
    color: var(--charcoal-darkest);
  }

  /* Sits under the floating title bar, matching its card treatment so it reads
     as part of the same layer rather than as an error page. */
  #app[data-export="on"] #export-notice:not([hidden]) {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    position: fixed;
    z-index: 51;
    top: calc(78px + env(safe-area-inset-top, 0px));
    left: 20px;
    right: 20px;
    padding: 13px 14px 13px 16px;
    background: var(--mustard-lightest);
    border: 1px solid var(--mustard-lighter);
    border-radius: 8px;
    font-size: var(--t-small);
    line-height: 1.45;
    color: var(--mustard-darker);
  }
  #export-notice strong { font-weight: 700; }
  #export-notice button {
    appearance: none; border: 0; background: none; padding: 0;
    color: var(--mustard-darker); cursor: pointer; flex: none;
    width: 22px; height: 22px; display: grid; place-items: center;
  }
  #export-notice button svg { width: 17px; height: 17px; display: block; }
  /* And no Export button to contradict it. The notice explains an absence, so
     the thing it explains has to actually be absent — a tappable Export beside
     a note saying export needs a bigger screen is worse than either alone. */
  .bar-menu-panel [data-action="export"],
  #bar-links [data-action="export"] { display: none; }
  #btn-menu-save { display: block; }
  #app:not([data-export="on"]) .panel-foot { display: none; }

.panel-foot { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }

  /* Smaller than desktop, not larger. These were sized up for arm's length,
     which was right when the filters stacked one per row; against a
     two-column filter grid the status row became the heaviest thing in the
     drawer and pushed the first card off screen. The padding keeps the tap
     target around 24px even though the mark and the label shrank. */
  .key { gap: 10px 16px; }
  .key label { font-size: var(--t-micro); padding: 2px 0; }
  .key input { width: 14px; height: 14px; margin-right: 8px; }
  .key-dot { width: 11px; height: 11px; margin-right: 6px; }

  #menu-fab { display: inline-flex; }
  #app[data-menu="open"] #menu-fab { display: none; }

  /* Appears once something has CHANGED since the drawer opened, not merely
     because filters are on: opening an already-filtered drawer and touching
     nothing leaves nothing to confirm. Both conditions, because on the map it
     would sit on top of the FAB. */
  #app[data-menu="open"][data-dirty="1"] #btn-view-filtered { display: inline-flex; }
  /* Room for whichever button is hovering over the list. Without it the last
     card sits underneath something permanently in the way of the thing you
     scrolled to see — measured on a 390x844 phone, 55px of the last card was
     behind the assistant button with the list scrolled all the way down.

     Whenever the drawer is open, NOT only when it is dirty. There is always
     exactly one button floating at the bottom: "View filtered map" once
     something has changed, and the assistant button the rest of the time. The
     dirty-only version left the commonest case — open the directory, scroll,
     change nothing — as the one with no room at the end.

     88px = 34px of standoff plus the taller of the two buttons (38px) plus
     16px so the card ends above it rather than against it. */
  #app[data-menu="open"] #sidebar {
    padding-bottom: calc(88px + env(safe-area-inset-bottom, 0px));
  }

  #print-bar {
    left: 12px; right: 12px; transform: none;
    flex-wrap: wrap; white-space: normal; gap: 10px 12px;
  }
}

/* Save is only meaningful once something changed; until then it is inert and
   Close is the honest way out. */
/* Disabled loses the lift as well as the colour — keeping the 4px shadow made
   an inert button still look pressable. */
#btn-menu-save[disabled],
#btn-export[disabled] {
  cursor: not-allowed;
  background: var(--charcoal-lighter);
  border-color: var(--charcoal-lighter);
  box-shadow: none;
  color: var(--paper-lightest);
}

@media (prefers-reduced-motion: reduce) {
  #sidebar { transition: none; }
}

/* ── The recommender ──────────────────────────────────────────────────────
   A sheet that rises from the bottom-right, over the directory on a desktop
   and over the whole screen on a phone. The shape is the one every assistant
   in this position has converged on — grabber, log, prompt chips, input —
   because it is the one people already know how to use.

   It is NOT modal. Someone reading a recommendation should be able to look at
   the map it names, and a dialog that traps focus would take that away. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* The style guide's `is-alternate`: white, a light neutral border, and the same
   4px lip every solid button in the scheme carries. That variant exists for
   buttons sitting over imagery, which is exactly where this one lives — and it
   is why this is not mustard. There IS no mustard button in the guide, and
   inventing one would fail anyway: white on Mustard Seed measures 2.20:1.
   The mustard lands on the spark instead, at --mustard-dark, which is 3.40:1
   on white and clears the 3:1 a meaningful graphic needs. */
.ask-fab {
  position: fixed;
  /* Above the mobile drawer (60) and the View-filtered button (62): this one
     stays put while the rest of the page changes underneath it. */
  z-index: 63;
  left: 20px;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  display: inline-flex;
  align-items: center;
  gap: 9px;
  padding: 8px 18px 8px 14px;
  /* A pill, where the guide's buttons are 6px. This one is a floating badge
     that collapses to a circle on a phone, and a shape that changes radius
     halfway through that move would read as two different buttons. */
  border-radius: 999px;
  /* The primary scheme, exactly as .btn-reach wears it: Lakeside ground, white
     ink, a darker Lakeside border and lip. This is the one thing on the page
     asking to be tried, so it takes the colour the church uses for asking. */
  background: var(--lakeside);
  border: 1px solid var(--lakeside-dark);
  border-bottom: 0;
  box-shadow: 0 4px 0 0 var(--lakeside-dark);
  color: #fff;
  font-family: var(--font-body);
  text-transform: var(--button-case, none);
  letter-spacing: normal;
  font-size: var(--t-small);
  line-height: 1.5;
  cursor: pointer;
  /* The press gets its own short transition. The 280ms one on a phone belongs
     to the journey between badge and button, and a press that takes that long
     does not read as a press at all. */
  transition: box-shadow 140ms ease, border-color 140ms ease, translate 120ms ease;
}
/* The floating circle has no lip. The lip is what makes a BUTTON look
   pressable; on a 37.5px disc it read as a shadow the circle was sitting in
   rather than an edge it stood on. It comes back the moment the badge becomes
   a button — see the expanded state on mobile.
   The bottom border returns with it: the base rule drops that because the lip
   stands in for it, and without both the circle was missing its own bottom
   edge. */
/* The circle is the same blue, and still carries no lip — the lip is what
   makes a BUTTON look pressable, and on a disc it reads as a shadow the circle
   is sitting in. Its bottom border comes back in place of it, since the base
   rule drops that on the assumption a lip is standing in. */
#ask-fab {
  box-shadow: none;
  border-bottom: 1px solid var(--lakeside-dark);
}

/* In the search row on a desktop: not floating, so no fixed positioning and
   no z-index — it is simply the second thing in that row. */
.ask-inline {
  position: static;
  flex: none;
  border-radius: 6px;
  white-space: nowrap;
}
.ask-inline[hidden] { display: none; }
/* Desktop only. The search panel it lives in is also the top of the mobile
   sheet, so on a phone this would sit inches from the floating badge doing
   exactly the same job. */
@media (max-width: 860px) {
  .ask-inline { display: none; }
}

/* Presses down onto its own lip, like every other button in the scheme — and
   nothing else. No border change on press and none while the drawer is open:
   an outline appearing on a button you are already holding down says nothing
   the movement has not already said, and one that stays on afterwards reads as
   a stuck state. */
/* `translate`, NOT `transform`. On a phone the badge is POSITIONED by a
   transform — left:50%, then translated back out to the left edge — so a
   :active rule that sets `transform` replaces that positioning wholesale.
   Measured on a 390px screen: at rest the circle sits at x=16 carrying
   translateX(-179px); the moment a finger touched it the transform became
   translateY(2px), the -179 was gone, and the button travelled 179px right to
   x=195. It travelled rather than jumped because the mobile rule transitions
   transform over 280ms — so what you saw was the badge setting off toward the
   middle of the screen, which is exactly where its expanded state lives, and
   springing back when the finger lifted. iOS then cancelled the click,
   because the element it belonged to had moved out from under the finger: a
   38px circle that has moved 179px is nowhere near it any more. The expanded
   button survived the identical bug only by being 249px wide — it moved
   124px, and the finger was still, just barely, on it.

   `translate` composes with `transform` rather than replacing it, so a press
   stays a press whatever the button's position happens to be made of. */
.ask-fab:active { box-shadow: 0 1px 0 0 var(--lakeside-dark); translate: 0 3px; }
/* The circle has no lip to press onto, so it just moves. Giving it one on the
   way down would be a shadow appearing out of nowhere mid-press. */
#ask-fab:active { box-shadow: none; translate: 0 2px; }
.ask-fab:focus-visible { outline: 2px solid var(--lakeside); outline-offset: 2px; }
/* Not a box anywhere but a phone. The badge keeps its own fixed positioning on
   a desktop and this wrapper has no effect there at all. */
.ask-fab-dock { display: contents; }
/* The spark carries the "this is generated" meaning, so it takes the accent
   while the words stay in reading ink. */
.ask-spark { width: 17px; height: 17px; flex: none; color: currentColor; }

/* FIXED, not absolute. Absolutely positioned, the closed drawer sat a full
   height plus 40px below its container and grew the scrollable area to reach
   it — so opening it shrank the page's scroll height and everything jumped.
   A fixed element does not extend the scroll area, so there is nothing to
   shake. */
.ask-drawer {
  position: fixed;
  z-index: 65;
  left: 20px;
  bottom: calc(20px + env(safe-area-inset-bottom, 0px));
  width: min(400px, calc(100% - 40px));
  /* Bounded by the room the map controls need above it, not just by the
     viewport. With the lift clamped but the sheet unbounded, the cluster stops
     going off screen and instead parks at the top of the map where the sheet
     covers it — trading one invisible control for another. 210px = the 185px
     budget above, plus the 16px the desktop lift adds, plus slack. */
  max-height: min(560px, calc(100dvh - 210px));
  display: flex;
  flex-direction: column;
  /* colour-scheme-2 from the style guide: white ground, charcoal text,
     #dcdad4 border. A reading surface rather than a control — this is where
     the answers land, and they want the same white the group cards use. */
  background: #fff;
  color: var(--charcoal);
  border: 1px solid var(--line);
  border-radius: 14px;
  box-shadow: 0 10px 40px color-mix(in srgb, var(--shadow-ink) 18%, transparent);
  overflow: hidden;

  /* Slides up from the edge it is anchored to, rather than appearing. Driven
     by visibility rather than the hidden attribute, because display:none
     cannot be transitioned out of — an element that is not laid out has no
     position to animate from. */
  transform: translateY(calc(100% + 40px));
  opacity: 0;
  visibility: hidden;
  transition:
    transform 320ms cubic-bezier(0.2, 0, 0, 1),
    opacity 200ms ease,
    visibility 0s linear 320ms;
}
#app[data-ask="open"] .ask-drawer {
  transform: none;
  opacity: 1;
  visibility: visible;
  /* Visible immediately on the way in, and only after the slide on the way
     out — otherwise it vanishes and then animates nothing. */
  transition:
    transform 320ms cubic-bezier(0.2, 0, 0, 1),
    opacity 160ms ease,
    visibility 0s;
}
@media (prefers-reduced-motion: reduce) {
  .ask-drawer, #app[data-ask="open"] .ask-drawer { transition: opacity 120ms ease, visibility 0s; }
  .ask-drawer { transform: none; }
}

.ask-head {
  display: flex; align-items: center; gap: 10px;
  padding: 13px 12px 12px 16px;
  border-bottom: 1px solid var(--line);
  position: relative;
}
.ask-head h2 { margin: 0; font-size: var(--t-micro); color: var(--text-3); flex: 1; }
/* Present on every size but only doing real work on a phone, where the sheet
   is tall enough that the top edge needs to say "this pulls down". */
.ask-grabber {
  display: none;
  position: absolute; top: 6px; left: 50%; transform: translateX(-50%);
  width: 36px; height: 4px; border-radius: 2px; background: var(--line-strong);
}
.ask-close {
  appearance: none; border: 0; background: none; padding: 0; cursor: pointer;
  width: 28px; height: 28px; display: grid; place-items: center;
  color: var(--text-2); flex: none; border-radius: 6px;
}
.ask-close:hover { color: var(--text); background: var(--paper); }
.ask-close svg { width: 17px; height: 17px; display: block; }

.ask-log {
  flex: 1; min-height: 0; overflow-y: auto;
  padding: 16px; display: grid; gap: 13px; align-content: start;
}
.ask-msg { font-size: var(--t-small); line-height: 1.55; }
.ask-msg p { margin: 0 0 9px; }
.ask-msg p:last-child { margin-bottom: 0; }
.ask-msg a { color: var(--lakeside-dark); text-underline-offset: 2px; }
.ask-msg.is-you {
  justify-self: end; max-width: 85%;
  background: var(--lakeside-lightest); border: 1px solid var(--lakeside-lighter);
  border-radius: 12px 12px 3px 12px; padding: 9px 12px;
}
.ask-msg.is-bot { max-width: 100%; }
.ask-msg.is-error { color: var(--danger); }
/* The disclaimer rides with the opening message rather than sitting in a
   banner: it is read once, where it is relevant, and then gets out of the way. */
.ask-note {
  font-size: var(--t-micro); color: var(--text-3); line-height: 1.5;
  padding-top: 9px; border-top: 1px solid var(--line);
}
.ask-note a { color: var(--text-2); }

/* Three dots, so a wait reads as thinking rather than as nothing happening. */
.ask-typing { display: inline-flex; gap: 4px; align-items: center; height: 18px; }
.ask-typing i {
  width: 5px; height: 5px; border-radius: 50%; background: var(--text-3);
  animation: ask-blink 1.2s infinite ease-in-out;
}
.ask-typing i:nth-child(2) { animation-delay: 0.18s; }
.ask-typing i:nth-child(3) { animation-delay: 0.36s; }
@keyframes ask-blink { 0%, 60%, 100% { opacity: 0.25; } 30% { opacity: 1; } }
@media (prefers-reduced-motion: reduce) {
  .ask-typing i { animation: none; opacity: 0.5; }
}

/* Scrolled, never wrapped: three chips on three lines pushes the input off a
   short phone screen, which is where this is used most. */
.ask-chips {
  display: flex; gap: 8px; padding: 0 16px 12px;
  overflow-x: auto; scrollbar-width: none;
}
.ask-chips::-webkit-scrollbar { display: none; }
.ask-chips:empty { display: none; }
.ask-chip {
  flex: none; white-space: nowrap; cursor: pointer;
  font-family: var(--font-body); font-size: var(--t-micro);
  padding: 7px 12px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--paper-lighter);
  color: var(--text-2);
}
.ask-chip:hover { border-color: var(--lakeside); color: var(--text); }

.ask-form {
  display: flex; align-items: flex-end; gap: 8px;
  padding: 12px 12px calc(12px + env(safe-area-inset-bottom, 0px)) 16px;
  border-top: 1px solid var(--line);
}
.ask-form textarea {
  flex: 1; resize: none; max-height: 108px;
  font-family: var(--font-body); font-size: var(--t-small); line-height: 1.5;
  border: 0; background: none; color: var(--text); padding: 6px 0;
}
.ask-form textarea:focus-visible { outline: none; }
.ask-send {
  appearance: none; border: 0; cursor: pointer; flex: none;
  width: 32px; height: 32px; border-radius: 50%;
  background: var(--lakeside); color: var(--paper-lightest);
  display: grid; place-items: center;
}
.ask-send:disabled { background: var(--charcoal-lighter); cursor: default; }
.ask-send svg { width: 17px; height: 17px; display: block; }

/* On a phone it is a true bottom sheet: full width, rising from the edge,
   with the rounded top and grabber that says so. */
@media (max-width: 860px) {
  .ask-fab {
    /* Fixed, and it stays exactly there: over the map, and over the directory
       sheet once that is open. The sheet is z-index 60 and covers the screen,
       so anything lower disappears the moment someone opens the list — which
       is the moment they are most likely to want this. */
    position: fixed;
    left: 16px;
    /* 30px while it is a lipless circle, so its bottom edge lands on 782 —
       the same line the Directory button's lip ends on. Bottom-aligned rather
       than same-height, because without a lip they cannot be the same height
       and a shared bottom edge is what actually reads as level. */
    bottom: calc(30px + env(safe-area-inset-bottom, 0px));
    /* A circle, not a pill with its label hidden — which is what this was, and
       it came out 44 wide by 37 tall.
       41.5px is the Directory button's TOTAL height: a 22.5px line box, 7px of
       padding twice, its single 1px top border (the Reach scheme leaves the
       bottom border off), and the 4px solid lip its box-shadow paints. That
       lip is part of the button to every eye that sees it, so matching the
       37.5px box alone left this reading small beside it.
       The two now occupy the same band exactly, top and bottom. */
    /* 37.5px of box plus the 4px lip is 41.5 — the Directory button's total
       height, arrived at the same way rather than faked with a single number. */
    height: 37.5px;
    padding: 0;
    gap: 0;
  }
  /* Over the MAP the words cost more than they give: the spark and the sheet's
     own heading carry the meaning, and the map needs its corners. Which leaves
     the mark to do all the work, so it grows to suit — 17px in a 41.5px circle
     read as a button someone had forgotten to finish. */
  .ask-spark { width: 24px; height: 24px; }

  /* ── The badge has three states on a phone ──────────────────────────────
     Over the map, a circle in the bottom-left corner.
     Over the open directory, the full button at the bottom centre — the
       Directory button has vacated that spot, and a list of groups is exactly
       where "ask for a recommendation" is worth spelling out.
     Over the open directory WITH filters touched, back to the circle, because
       View Filtered Map takes the centre and two buttons cannot have it.

     Both moves are the same transition run forwards and backwards, so the
     badge shrinks away in step with that button arriving and grows back as it
     leaves. Closing the menu drops data-menu and it collapses on the way out
     with the sheet.

     Driven entirely by attributes the app already sets. No script: a state
     machine written twice is a state machine that disagrees with itself. */
  /* The badge is centred by LAYOUT, and only offset by transform.
     It used to centre itself with translateX(-50%) — a percentage that
     resolves against the badge's own width, which is the one thing changing
     during this animation. Position and size were therefore not independent,
     and a browser that resolves that percentage once at the start of the
     transition rather than per frame animates the badge as though it were
     still a 37.5px circle: it travels until its centre is near the middle of
     the screen, then re-centres at its real width the moment the transition
     ends, jumping left by half the button. Reported from an iPhone twice, in
     those words, and invisible in headless WebKit, which resolves it per
     frame.
     A full-width flex dock does the centring instead, so the only transform
     left is a constant: 34.75px is 16px of inset plus half the collapsed
     circle, and it is the distance from the middle of the screen to where the
     circle's centre belongs. Nothing in it refers to the badge's own size. */
  .ask-fab-dock {
    display: flex;
    justify-content: center;
    position: fixed;
    left: 0;
    right: 0;
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    z-index: 63;
    /* The dock spans the screen; only the badge inside it is touchable. */
    pointer-events: none;
    transition: bottom 280ms cubic-bezier(0.2, 0, 0, 1);
  }
  #app[data-menu="open"]:not([data-dirty="1"]) .ask-fab-dock {
    bottom: calc(34px + env(safe-area-inset-bottom, 0px));
  }

  .ask-fab {
    position: static;
    pointer-events: auto;
    transform: translateX(calc(34.75px - 50vw));
    min-width: 37.5px;
    /* max-content, not auto. As a flex item in a full-width dock, `auto` sizes
       against the space the dock offers and a shrinkable label will take that
       ceiling rather than its own text — which is how 54px went missing off
       the end of the words the first time the label was made shrinkable.
       max-content sizes to the words and ignores the room around them. */
    width: max-content;
    justify-content: center;
    overflow: hidden;
    /* No `bottom` here: the badge is static inside the dock now, and the dock
       is what rises. */
    transition:
      transform 280ms cubic-bezier(0.2, 0, 0, 1),
      padding 280ms cubic-bezier(0.2, 0, 0, 1),
      border-radius 280ms cubic-bezier(0.2, 0, 0, 1),
      box-shadow 280ms cubic-bezier(0.2, 0, 0, 1),
      gap 280ms cubic-bezier(0.2, 0, 0, 1);
  }
  .ask-fab-label {
    /* A 0fr -> 1fr grid track rather than max-width, so the growth ends when
       the movement does.

       max-width has to be given a number, and the number has to clear the
       longest label any church might use, so it was 15rem. The label is about
       150px wide. The track therefore ran 0 -> 240px to cover a 150px job, and
       under an ease-out this steep it had reached the words' full width about
       43% of the way through — measured: full size at ~120ms of a 280ms move,
       after which the badge went on travelling for another 160ms with nothing
       else changing. Growing and travelling are meant to be one movement and
       they had come apart, which on a phone reads as the badge sliding to the
       middle and then snapping open.

       An fr track resolves to exactly the content's width, so the two now run
       on the same curve for the same duration whatever the church calls its
       groups. A hidden element cannot animate, which is why this is not
       display: none. */
    /* The clip lives HERE, on the container, not on the span inside it.
       `0fr -> 1fr` in an auto-sized container resolves the flex fraction
       TWICE. This element's own box comes out at 187px x p — linear, and it is
       what sizes the pill, which is why the pill tracked the travel and the
       checks went green. But the track INSIDE it re-resolves against that now
       definite width and lands at 187px x p x p. Clipping on the span clipped
       the words to the track, so the text was revealed on the SQUARE of the
       progress: measured, 6% revealed at 26% travelled, 48% at 70%, 85% at
       93%, matching p^2 to a tenth of a point at every frame.
       What that looks like is the button arriving at full size and full stop
       with a slab of empty blue on its right, the last syllables still filling
       in after the movement has ended — which is the "snap expands" that has
       been reported three times. The container's box is the linear one, so the
       clip belongs on it. */
    overflow: hidden;
    display: grid;
    grid-template-columns: 0fr;
    opacity: 0;
    transition:
      grid-template-columns 280ms cubic-bezier(0.2, 0, 0, 1),
      opacity 140ms linear;
  }
  /* min-width, and nothing else. A grid item will not shrink below its own
     content unless it is told it may, and a track that cannot shrink cannot
     animate from zero — so this stays. The overflow does NOT: it used to clip
     here, which clipped the words to the quadratic track rather than to the
     label's linear box. The span now lays out at the track's width, its
     nowrap text overflows it, and the label above clips that. */
  .ask-fab-label > span {
    min-width: 0;
    /* One line. Wrapping made a two-line label inside a 41.5px button, which
       is a button with its own text hanging out of it. */
    white-space: nowrap;
  }

  #app[data-menu="open"]:not([data-dirty="1"]) .ask-fab {
    transform: translateX(0);
    /* Less on the left than the right: the mark is a compact shape where the
       label is a long one, so equal padding leaves the left looking slack.
       This hugs the mark in. */
    padding: 0 18px 0 10px;
    gap: 8px;
    /* 6px: the radius every button in the style guide carries, and what
       .btn-reach uses. Not --radius, which is 8 and belongs to cards and
       panels. It is a circle while it is a badge and a button once it is a
       button. */
    border-radius: 6px;
    /* And now that it IS a button, it stands on a lip like the rest of them,
       taking the Directory button's own offset so the two sit identically. */
    box-shadow: 0 4px 0 0 var(--lakeside-dark);
    border-bottom: 0;
  }
  #app[data-menu="open"]:not([data-dirty="1"]) .ask-fab-label {
    grid-template-columns: 1fr;
    opacity: 1;
    /* In step with the button, not after it.
       This used to wait 120ms and then fade for 160, on the reasoning that
       the words should arrive once there was somewhere to put them. That was
       true when the width animation finished at 43% and left the label with
       nowhere to go — but it meant the first 160ms of a 280ms move, 82% of the
       travel and 81% of the growth, happened inside an EMPTY blue pill, and
       the words only turned up once the badge had stopped. What reads as the
       button "snapping open" at the end is the text arriving, not the shape
       changing.
       The track grows in lockstep with the slide now, so the room arrives
       progressively and the words can come with it: the clip does the real
       work, revealing them left to right as the button widens, and the fade
       only takes the hard edge off the first frame. */
    /* 80ms, and no delay. Once the clip reveals the words progressively —
       left to right, in step with the button — the fade has only one job
       left: taking the hard edge off the first frame. Running it for 120ms
       after a 20ms wait meant full opacity at 140ms, while two thirds of the
       journey is done by about 90ms, so the words were still half-transparent
       for most of the movement. Measured against production: opacity 0.52 at
       the two-thirds mark, five runs landing between 0.517 and 0.542 — which
       also sat right on top of the check asserting it, and made that check
       fail about one run in five. */
    transition:
      grid-template-columns 280ms cubic-bezier(0.2, 0, 0, 1),
      opacity 80ms linear;
  }

  @media (prefers-reduced-motion: reduce) {
    .ask-fab, .ask-fab-label { transition: none; }
  }

  .ask-drawer {
    position: fixed;
    inset: auto 0 0 0;
    width: 100%;
    /* 82dvh alone was already too generous even without zoom: on a tall
       phone it leaves under 185px of headroom, so a long conversation buried
       the attribution on its own. */
    max-height: min(82dvh, calc(100dvh - 195px));
    border-radius: 16px 16px 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
  }
  .ask-grabber { display: block; }
  .ask-head { padding-top: 17px; }
  /* While the sheet is open these would sit on top of it, or — worse, in the
     badge's case — underneath it. The sheet spans the width of a phone, so the
     badge at bottom-left was inside its footprint at a lower z-index: still
     display:flex, still in the tab order, still hit-testable, but every tap on
     it landed on the message box in front of it. The comment here used to say
     a script hid it, and no script did. On a phone the sheet IS the badge's
     open state, so there is nothing for it to do while the sheet is up; on a
     desktop it stays put and closes the panel, which is why this is scoped to
     the mobile query. */
  #app[data-ask="open"] #menu-fab,
  #app[data-ask="open"] #btn-view-filtered,
  #app[data-ask="open"] .ask-fab { display: none; }

  /* Google's attribution rides up above the assistant sheet.
     The sheet covers the bottom of the screen, which is exactly where Google
     puts its logo — so with the sheet open the map stayed on screen with no
     attribution on it at all, and the Maps Platform terms require it to remain
     visible on displayed imagery. --ask-sheet-h is written by app.js from the
     sheet's measured height, because that height depends on the conversation
     in it. The control cluster goes with it for the same reason it is lifted
     for the URL bar: a button under the sheet cannot be pressed.

     The logo is targeted through its wrapper, not directly: Maps renders that
     anchor as display:inline via an inline style, and a transform on an inline
     non-replaced element is ignored — the rule applied and the logo did not
     move. Its wrapper is an unclassed positioned div, so :has() is how to
     reach it. The .gm-style-cc credits below are block-level and move on
     their own. */
  #app[data-ask="open"] #map div:has(> a[href*="maps.google"]),
  #app[data-ask="open"] #map .gm-style-cc {
    transform: translateY(calc(-1 * min(var(--ask-sheet-h, 0px), var(--ask-lift-max))));
  }
  #app[data-ask="open"] #map .gm-bundled-control-on-bottom {
    transform: translateY(calc(-1 * min(var(--ask-sheet-h, 0px), var(--ask-lift-max))));
  }
}

@media (min-width: 861px) {
  /* The desktop panel opens over the bottom-right of the map, and the bottom
     right of a Google map is where its zoom and camera controls live. All
     three were underneath it and unclickable — measured, not guessed: a
     hit-test at the centre of each returned the panel. They ride up above it
     instead of sideways, so they stay on the edge a mouse expects them on.
     The attribution sits lower still and stays clear on its own. */
  #app[data-ask="open"] #map .gm-bundled-control-on-bottom {
    transform: translateY(calc(-1 * min(var(--ask-sheet-h, 0px) + 16px, var(--ask-lift-max))));
    transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1);
  }

  /* On a NARROW desktop the panel reaches the attribution as well.
     The panel is 400px wide against the right edge, and Google's logo sits at
     the left — so they miss each other on a wide screen and collide on a
     small one. Measured at 960x542: the logo spans x 485-551 and the panel
     starts at x 540, putting an 11x6px corner of the wordmark behind it. Small,
     and still the attribution being covered. Below about 971px they overlap;
     under 861 the phone rules lift it already, so this covers the band between
     the two. */
  @media (max-width: 980px) {
    #app[data-ask="open"] #map div:has(> a[href*="maps.google"]),
    #app[data-ask="open"] #map .gm-style-cc {
      /* +28, not the +16 the controls use. The attribution starts flush with
         the bottom of the viewport, and the panel floats 20px above it, so
         clearing the panel's TOP edge costs the sheet's height plus that 20px
         plus a gap. At +16 the wordmark ended 4px below the panel's top and
         was still clipped — measured, logo bottom 194 against a panel top of
         190. The controls need less because they do not start at the floor. */
      transform: translateY(calc(-1 * min(var(--ask-sheet-h, 0px) + 28px, var(--ask-lift-max))));
      transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1);
    }
  }

  /* Bottom-right of the viewport, always, whichever button opened it. Having
     it follow the button meant the panel moved around the screen between one
     visit and the next — and a thing that is somewhere different each time you
     open it is a thing you have to find rather than a thing you know where to
     look for. */
  .ask-drawer { left: auto; right: 20px; width: min(400px, calc(50% - 40px)); }

  /* The floating one belongs over the MAP now that the search row carries the
     full button. Bottom-left of the map is the mirror of where it sits on a
     phone, and the far corner from Google's attribution and its zoom cluster.
     The map is the right half, so the left edge of it is 50%. */
  #ask-fab {
    left: calc(50% + 20px);
    right: auto;
    bottom: calc(20px + env(safe-area-inset-bottom, 0px));
    /* 1.5x the phone's 37.5. On a desktop this floats over a half-screen map
       with no label to explain it, where the phone's version sits next to a
       Directory button that gives it context. */
    width: 56px;
    height: 56px;
    padding: 0;
    gap: 0;
    justify-content: center;
    border-radius: 999px;
  }
  #ask-fab .ask-fab-label { display: none; }
  #ask-fab .ask-spark { width: 30px; height: 30px; }
}

/* Lists in a reply. The model writes one whenever it names more than one
   option, and until now the markers arrived as literal asterisks on their own
   lines. Tight leading and a modest indent: this is two or three groups, not
   a document outline. */
.ask-msg ul, .ask-msg ol { margin: 0 0 9px; padding-left: 20px; }
.ask-msg ul:last-child, .ask-msg ol:last-child { margin-bottom: 0; }
.ask-msg li { margin: 0 0 5px; }
.ask-msg li:last-child { margin-bottom: 0; }
.ask-msg ul { list-style: disc; }
.ask-msg ol { list-style: decimal; }

/* Safari on iOS zooms the whole page in when you focus a field whose text is
   smaller than 16px, and it does not zoom back out. Opening the assistant
   focuses its box for you, so the panel arrived with the page magnified and
   the map shoved off to one side. 13px was the value on every field here.
   Only on coarse pointers, so the desktop keeps its own scale — and expressed
   as a floor rather than a flat size, so a field that is already larger is
   left alone. */
@media (pointer: coarse) {
  #search,
  .ask-form textarea,
  #print-bar select,
  #print-bar input {
    font-size: max(16px, var(--t-small));
  }
}
