  /* =========================================================================
     DESIGN TOKENS
     One place to see (and change) every color, shadow and font the app
     uses. Grouped by role rather than by hue, so "what does this get used
     for" is always answerable at a glance. Nothing here changes what the
     app looks like today - it's the same values as before, just named.
     ========================================================================= */
  :root {
    /* ---- Typography --------------------------------------------------- */
    /* Plus Jakarta Sans everywhere (see the Google Fonts <link> in <head>)
       for a single, consistent, characterful typeface instead of the
       generic OS font stack. The system stack is kept as a fallback for
       the instant before the web font loads (or if it fails to). */
    --font-sans: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

    /* ---- Brand green (primary action / identity color) ---------------- */
    --brand: #2ecc71;              /* primary green: buttons, links, accents */
    --brand-dark: #219150;         /* featured-marker pin, deeper accents */
    --brand-grad-start: #34d67f;   /* gradient light stop (active pills, badges) */
    --brand-grad-end: #24b366;     /* gradient dark stop */
    --brand-gradient: linear-gradient(135deg, var(--brand-grad-start), var(--brand-grad-end));
    --brand-ink: #05341c;          /* dark green text set on top of --brand-gradient */
    --brand-rgb: 46, 204, 113;     /* raw components, for rgba() shadows/tints below */
    --brand-tint: #eafaf0;         /* faint green wash: active row bg, fallback thumb bg */
    --brand-tint-faint: #f7fdfa;   /* even fainter green wash: carousel arrow hover */
    --brand-tint-strong: #d4f5df;  /* stronger green wash: search-match highlight, banner border */
    --brand-tint-ink: #0a5c2e;     /* dark green text set on a green wash, e.g. search-match text */
    --link-ink: #1a8f4f;           /* green hyperlink color on a plain white background */

    /* ---- Small standalone accents -------------------------------------- */
    --distance-bg: #e0f8e9;        /* "x.x km" distance pill background */
    --distance-ink: #0e7a3d;       /* distance pill text */
    --star: #e8a20c;               /* rating star */
    --user-location: #4285f4;      /* "you are here" map dot - matches Maps blue on purpose */

    /* ---- Neutral text scale, darkest to lightest ----------------------- */
    --ink: #1a1a1a;    /* primary headings/text */
    --icon: #333;      /* icon-weight secondary text and icon buttons */
    --muted: #555;     /* secondary body text */
    --meta: #777;      /* meta text - ratings row, secondary detail lines */
    --faint: #999;     /* placeholder / tertiary text */
    --faint-2: #888;   /* alternate tertiary tone used in a couple of spots */
    --ink-rgb: 0, 0, 0; /* raw components for the neutral drop-shadows below */
    --ink-tint-rgb: 26, 26, 26; /* matches --ink's actual color (unlike --ink-rgb above, this DOES flip in dark mode) - for hover tints that should track --ink rather than always stay black */

    /* ---- Surfaces & borders --------------------------------------------- */
    --surface: #ffffff;
    --bg-app: #fafafa;            /* app chrome background: sidebar, featured strip */
    --bg-hover: #f5f5f5;          /* generic hover background */
    --bg-pill: #f0f0f0;           /* segmented control background */
    --hover-tint-green: #f7f9f8;  /* place-card hover: a barely-there green-tinted wash */
    --hover-tint-gray: #e2e2e2;   /* popup share-button hover */
    --border: #e5e5e5;
    --border-light: #eee;
    --border-lighter: #ddd;
    --divider: #f2f2f2;

    /* ---- One-off banner accents ----------------------------------------- */
    --loading-glow: #f4faf6;
    --welcome-bg-start: #f0fdf5;
    --welcome-bg-end: #e3f9ec;
    --welcome-ink: #145a32;

    color-scheme: light;
  }

  /* DARK MODE - same token names, dark values; every rule elsewhere already
     uses var(), so this alone re-themes the app. --ink-rgb is NOT redefined
     (only backs shadow rgba() overlays, and black-based still reads fine on
     dark). Toggled via .dark on <html> - see the inline script in <head>. */
  :root.dark {
    /* ---- Brand green tints, re-based for a dark surface ---------------- */
    --brand-tint: #12291c;
    --brand-tint-faint: #0f1f16;
    --brand-tint-strong: #1a4228;
    --brand-tint-ink: #7be8a0;
    --link-ink: #4ade80;

    /* ---- Small standalone accents --------------------------------------- */
    --distance-bg: #123a22;
    --distance-ink: #6fe39a;

    /* ---- Neutral text scale, now light-on-dark -------------------------- */
    --ink: #f2f2f2;
    --ink-tint-rgb: 242, 242, 242;
    --icon: #d8d8d8;
    --muted: #b8b8b8;
    --meta: #9a9a9a;
    --faint: #808080;
    --faint-2: #8a8a8a;

    /* ---- Surfaces & borders, white to almost-black ---------------------- */
    --surface: #1c1c1e;
    --bg-app: #141416;
    --bg-hover: #2a2a2c;
    --bg-pill: #262628;
    --hover-tint-green: #182019;
    --hover-tint-gray: #3a3a3c;
    --border: #333336;
    --border-light: #2a2a2c;
    --border-lighter: #3a3a3c;
    --divider: #2a2a2c;

    /* ---- One-off banner accents ------------------------------------------ */
    --loading-glow: #0e1410;
    --welcome-bg-start: #102015;
    --welcome-bg-end: #0d2417;
    --welcome-ink: #8fe3ac;

    /* Lets native form controls, scrollbars etc. pick a dark rendering
       instead of fighting the page around them. */
    color-scheme: dark;
  }

  /* Leaflet's own stylesheet (loaded separately, not part of this app's
     design tokens) hardcodes a white popup/control background and dark
     text - these are the only spots that need an explicit override since
     everything else on the map (pins, the tile layer itself) is already
     handled elsewhere. */
  :root.dark .leaflet-popup-content-wrapper,
  :root.dark .leaflet-popup-tip {
    background: var(--surface);
    color: var(--ink);
  }

  :root.dark .leaflet-popup-close-button {
    color: var(--icon) !important;
  }

  :root.dark .leaflet-control-zoom a {
    background: var(--surface);
    color: var(--ink);
    border-color: var(--border) !important;
  }

  :root.dark .leaflet-control-zoom a:hover {
    background: var(--bg-hover);
  }

  :root.dark .leaflet-control-attribution {
    background: rgba(28, 28, 30, 0.8);
    color: var(--muted);
  }

  :root.dark .leaflet-control-attribution a {
    color: var(--link-ink);
  }

  /* The loading screen reads its colors from the same tokens as
     everything else (--surface, --loading-glow, --ink, --faint-2), so it
     was already picking up SOME dark styling automatically - but that
     general cascade still left the subtitle a dimmer grey and kept the
     light mode's soft gradient glow. This is a more literal, specific
     override: a flat off-black background and fully white text for both
     lines, no gradient. */
  :root.dark #loading-overlay {
    background: #0d0d0d;
  }

  :root.dark #loading-title,
  :root.dark #loading-subtitle {
    color: #f5f5f5;
  }

  /* Both modals re-theme via tokens automatically; only the backdrop dim
     needs its own override, for more separation now the card itself is dark. */
  :root.dark .app-modal-backdrop,
  :root.dark .submit-modal-backdrop {
    background: rgba(0, 0, 0, 0.7);
  }

  /* =========================================================================
     GLOBAL RESET
     ========================================================================= */
  * { box-sizing: border-box; }

  /* overflow: hidden is load-bearing: #main is position:absolute beneath
     the in-flow #topbar, so without this an uncaptured scroll bubbles up
     to html/body and drags #topbar off-screen. */
  html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    overflow: hidden;
    font-family: var(--font-sans);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  /* Form controls don't inherit the page font by default in most browsers
     (they use the OS's UI font instead) - without this, every button and
     input would silently fall back to a different typeface than the rest
     of the page the moment a custom font like this one is introduced. */
  button, input, select, textarea {
    font-family: inherit;
  }


  /* Visually hidden but still readable by search engines and screen readers -
     not display:none, which some crawlers discount as cloaked/hidden content.
     This is the standard, widely-used accessible-text pattern (e.g. skip
     links) and describes real page content, so it's safe for SEO use. */
  .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;
  }

  /* FALLBACK CONTENT STYLING - plain, always-visible (not .sr-only, not
     <noscript>-gated) styling for #place-list, present on first paint
     before any JS runs. Popup card itself uses Leaflet's own classes plus
     .popup-* - see "MAP POPUP CONTENT" further down. */
  .ssr-list {
    list-style: none;
    margin: 0;
    padding: 0;
  }

  .ssr-list li {
    padding: 12px 16px;
    border-bottom: 1px solid var(--border-light);
  }

  .ssr-list a {
    color: var(--link-ink);
    font-weight: 600;
    text-decoration: none;
  }

  .ssr-list a:hover {
    text-decoration: underline;
  }

  .ssr-list .ssr-meta {
    display: block;
    margin-top: 2px;
    color: var(--meta);
    font-size: 12.5px;
  }

  .ssr-list .ssr-details {
    display: block;
    margin-top: 4px;
    color: var(--ink);
    font-size: 12.5px;
    line-height: 1.4;
  }

  /* #ssr-active-popup itself now just reuses .place-modal-backdrop/
     .place-modal/.place-modal-content (rendered with the "visible" class
     already present, so it needs no JS to appear). No bespoke
     positioning rule needed here; it's visually identical to the real
     interactive modal from first paint, JS-capable browser or not. */

  /* ============================================================
     TOP BAR (logo, sidebar toggle, search, sort pills)
     ============================================================ */
  #topbar {
    height: 60px;
    background: var(--surface);
    border-bottom: 1px solid var(--border);
    box-shadow: 0 2px 6px rgba(var(--ink-rgb), 0.05);
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    padding: 0 20px;
    gap: 20px;
    position: relative;
    z-index: 1000;
  }

  /* Groups the sidebar toggle + logo into the topbar's first grid
     column, so the search bar (the middle column) can be truly
     centered regardless of how wide this group or the sort pills on
     the other side happen to be. */
  #topbar-left {
    display: flex;
    align-items: center;
    gap: 20px;
    min-width: 0;
  }

  #logo {
    color: var(--ink);
    font-size: 20px;
    font-weight: 700;
    letter-spacing: 0.5px;
    white-space: nowrap;
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    border-radius: 8px;
    padding: 4px 6px;
    margin: -4px -6px;
    text-decoration: none;
  }

  #logo:hover { background: rgba(var(--ink-rgb), 0.06); }

  #logo .dot { color: var(--brand); }

  #sidebar-toggle {
    background: transparent;
    border: none;
    color: var(--ink);
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    padding: 6px 8px;
    border-radius: 6px;
    line-height: 1;
  }

  #sidebar-toggle:hover { background: rgba(var(--ink-rgb), 0.06); }

  #logo-img {
    height: 34px;
    width: 34px;
    border-radius: 50%;
    object-fit: cover;
  }

  /* Header nav link to the articles section - sits between the logo and
     the search bar in #topbar-left (see the HTML above). Styled to match
     #logo/#sidebar-toggle's hover treatment rather than looking like a
     distinct nav bar, since it's the only other nav-level link up here. */
  #articles-nav-link {
    display: flex;
    align-items: center;
    gap: 6px;
    color: var(--ink);
    font-size: 14px;
    font-weight: 700;
    text-decoration: none;
    white-space: nowrap;
    padding: 6px 10px;
    border-radius: 8px;
  }

  #articles-nav-link:hover { background: rgba(var(--ink-rgb), 0.06); }

  #articles-nav-link.active {
    background: var(--brand-tint);
    color: var(--brand-dark);
  }

  #articles-nav-link .articles-nav-icon { display: flex; }

  /* ============================================================
     TOPBAR ICON SIZING
     One class for the whole shared line-icon set (see the TOPBAR ICONS
     PHP block above the markup). Each icon inherits its color from the
     control it sits in via currentColor, so hover/active/dark-mode
     states need no per-icon rules - only the box size differs.
     ============================================================ */
  .ui-icon { display: block; flex: none; width: 18px; height: 18px; }
  #sidebar-toggle .ui-icon { width: 20px; height: 20px; }
  #articles-nav-link .ui-icon { width: 17px; height: 17px; }
  #search-icon .ui-icon { width: 16px; height: 16px; }
  #dark-mode-toggle .ui-icon { width: 17px; height: 17px; }
  .prayer-widget-icon .ui-icon { width: 14px; height: 14px; }

  /* ============================================================
     SEARCH BAR & AUTOCOMPLETE SUGGESTIONS
     ============================================================ */
  #search-wrap {
    justify-self: center;
    width: 100%;
    max-width: 480px;
    position: relative;
  }

  #search {
    width: 100%;
    height: 38px;
    border-radius: 19px;
    border: 1px solid var(--border);
    padding: 0 40px 0 16px;
    font-size: 14px;
    font-weight: 700;
    outline: none;
    background: var(--surface);
    color: var(--ink);
    box-shadow: inset 0 1px 3px rgba(var(--ink-rgb), 0.12);
    transition: background 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease;
  }

  #search:focus {
    background: var(--surface);
    border-color: rgba(var(--brand-rgb), 0.5);
    box-shadow: inset 0 1px 3px rgba(var(--ink-rgb), 0.12), 0 0 0 3px rgba(var(--brand-rgb), 0.18);
  }

  #search::placeholder { color: var(--faint-2); font-weight: 600; }

  #search-icon {
    position: absolute;
    right: 14px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--faint);
    display: flex;
    pointer-events: none;
  }

  #search-suggestions {
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    right: 0;
    background: var(--surface);
    border-radius: 12px;
    box-shadow: 0 8px 24px rgba(var(--ink-rgb), 0.25);
    overflow: hidden;
    display: none;
  }

  #search-suggestions.visible { display: block; }

  .suggestion-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px;
    min-height: 44px;
    cursor: pointer;
    border-bottom: 1px solid var(--bg-pill);
  }

  .suggestion-item:last-child { border-bottom: none; }
  .suggestion-item:hover, .suggestion-item:active { background: var(--bg-hover); }

  /* Arrow-key highlight (see searchInput's keydown handler below) -
     same visual as :hover, since both mean "this is the one Enter would
     select right now". */
  .suggestion-item.highlighted { background: var(--bg-hover); }

  .suggestion-thumb {
    width: 40px;
    height: 40px;
    border-radius: 8px;
    object-fit: cover;
    flex-shrink: 0;
  }

  .suggestion-thumb-fallback {
    object-fit: contain;
    background: var(--brand-tint);
    padding: 4px;
  }

  /* Same featured-thumbnail ring treatment as the sidebar's
     .place-thumb-frame, sized for this list's slightly smaller 40x40
     thumbnail instead of 44x44 - featuredThumbRingSvg's viewBox scales
     to whatever box it's given, so no separate SVG is needed. */
  .suggestion-thumb-frame {
    position: relative;
    width: 48px;
    height: 48px;
    flex-shrink: 0;
  }

  .suggestion-thumb-frame .suggestion-thumb {
    position: absolute;
    inset: 4px;
    width: 40px;
    height: 40px;
  }

  .suggestion-text {
    flex: 1;
    min-width: 0;
  }

  .suggestion-name {
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .suggestion-meta {
    font-size: 12px;
    color: var(--faint-2);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .suggestion-rating {
    color: var(--muted);
    font-weight: 600;
  }

  .price-tag {
    color: var(--muted);
    font-weight: 600;
  }

  .prayer-space-badge {
    display: inline-flex;
    align-items: center;
    vertical-align: -2px;
    color: var(--icon);
  }

  .clothing-badge {
    display: inline-flex;
    align-items: center;
    vertical-align: -2px;
    color: #e67e22;
  }

  .suggestion-distance {
    color: var(--muted);
  }

  .suggestion-details {
    font-size: 12px;
    color: var(--muted);
    font-style: italic;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-top: 2px;
  }

  .search-match {
    background: var(--brand-tint-strong);
    color: var(--brand-tint-ink);
    font-weight: 700;
    border-radius: 3px;
    padding: 0 1px;
  }

  .suggestion-empty {
    padding: 14px 16px;
    font-size: 13px;
    color: var(--faint);
    text-align: center;
  }

  /* ============================================================
     SORT / FILTER PILLS
     ============================================================ */
  /* Groups the sort pills and the dark-mode toggle into the topbar's
     third grid column, the same way #topbar-left groups the sidebar
     toggle + logo into the first. */
  #topbar-right {
    display: flex;
    align-items: center;
    gap: 12px;
  }

  #dark-mode-toggle {
    background: transparent;
    border: 1px solid var(--border-lighter);
    color: var(--icon);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 8px 9px;
    border-radius: 8px;
    cursor: pointer;
    flex-shrink: 0;
  }

  #dark-mode-toggle:hover {
    background: var(--bg-hover);
  }

  #filter-group {
    display: flex;
    background: var(--bg-pill);
    border-radius: 19px;
    padding: 3px;
    gap: 2px;
    white-space: nowrap;
  }

  .filter-btn {
    border: none;
    background: transparent;
    color: var(--muted);
    font-size: 13px;
    padding: 7px 14px;
    border-radius: 16px;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
  }

  .filter-btn:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.06); }
  .filter-btn:active { transform: scale(0.96); }

  .filter-btn.active {
    background: var(--brand-gradient);
    color: var(--brand-ink);
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(var(--brand-rgb), 0.35);
  }

  .filter-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
  }

  @media (max-width: 640px) {
    #topbar {
      height: auto;
      padding: 8px 10px;
      gap: 8px;
      row-gap: 8px;
      grid-template-columns: auto auto;
      grid-template-areas:
        "left filter"
        "search search";
    }
    #topbar-left {
      grid-area: left;
      gap: 8px;
    }
    #logo-text {
      display: none;
    }
    #logo-img {
      height: 30px;
      width: 30px;
    }
    #articles-nav-link .articles-nav-text {
      display: none;
    }
    #articles-nav-link {
      padding: 6px 8px;
    }
    #sidebar-toggle {
      padding: 4px 6px;
    }
    #sidebar-toggle .ui-icon { width: 18px; height: 18px; }
    #search-wrap {
      grid-area: search;
      justify-self: stretch;
      max-width: none;
      min-width: 0;
      width: 100%;
    }
    #search {
      font-size: 13px;
      padding: 0 30px 0 12px;
    }
    #topbar-right {
      grid-area: filter;
      justify-self: end;
      gap: 6px;
    }
    #filter-group {
      gap: 1px;
      padding: 2px;
    }
    .filter-btn {
      padding: 6px 8px;
      font-size: 11px;
    }
    #dark-mode-toggle {
      padding: 6px 7px;
    }
    #dark-mode-toggle .ui-icon { width: 15px; height: 15px; }
  }

  /* PRAYER TIME WIDGET - #prayer-widget sits in #topbar-right on wide
     screens; below 768px it hides for #prayer-widget-m next to the logo
     instead. Both share js-prayer-widget so one set of script logic
     (PRAYER TIMES COUNTDOWN) keeps them in sync. */
  #prayer-widget,
  #prayer-widget-m {
    display: flex;
    align-items: center;
    gap: 6px;
    background: var(--bg-pill);
    border-radius: 16px;
    padding: 7px 12px;
    font-size: 12px;
    font-weight: 600;
    color: var(--ink);
    white-space: nowrap;
    flex-shrink: 0;
  }

  /* Hidden by default - only shown next to the logo below the 768px
     breakpoint (see that media query further down), where #prayer-widget
     itself gets hidden instead. */
  #prayer-widget-m {
    display: none;
  }

  .prayer-widget-icon { display: flex; }

  .prayer-widget-name {
    color: var(--brand-dark);
  }

  .prayer-widget-countdown {
    font-variant-numeric: tabular-nums;
    color: var(--muted);
    font-weight: 700;
  }

  /* ============================================================
     FEATURED CAROUSEL
     Lives inside #content-col (see PAGE LAYOUT SHELL below), stacked
     above #map-wrapper - so it only spans the map's width, not the
     sidebar's, and the sidebar itself starts right at the top of
     #main instead of being pushed down to match its height.
     ============================================================ */
  #featured {
    position: relative;
    flex-shrink: 0;
    height: 138px;
    background: var(--bg-app);
    border-bottom: 1px solid var(--border-lighter);
    z-index: 950;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }

  /* Decorative background - a faint abstract nodes-and-links texture,
     since the strip often has a lot of empty space around just 3 cards.
     A single wide, hand-placed layout (viewBox 0 0 1400 144,
     preserveAspectRatio="none") rather than a tiled <pattern> - it
     stretches to fill whatever width the strip actually is instead of
     visibly repeating. Purely visual: aria-hidden and sits behind
     everything else in #featured. */
  #featured-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
  }

  #featured-label,
  #featured-row {
    position: relative;
    z-index: 1;
  }

  #featured-label {
    /* Hidden to reclaim the vertical space it used to take above the
       carousel cards - the cards themselves (#featured-row) still show,
       just with no "Featured" caption above them. #featured's own
       height is trimmed to match (see below). */
    display: none;
  }

  #featured-row {
    position: relative;
    flex: 1;
    min-height: 0;
    display: flex;
    align-items: center;
    padding: 0 44px;
    overflow: hidden;
  }

  #featured-track {
    display: flex;
    align-items: stretch;
    justify-content: flex-start;
    gap: 12px;
    width: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    scrollbar-width: none;
    padding: 6px 0;
    height: 100%;
    box-sizing: border-box;
  }

  #featured-track.centered {
    justify-content: center;
  }

  #featured-track::-webkit-scrollbar { display: none; }

  .featured-card {
    min-width: 220px;
    max-width: 220px;
    height: 100%;
    overflow: hidden;
    background: var(--surface);
    border: 2px solid var(--brand);
    border-radius: 12px;
    padding: 6px 10px;
    cursor: pointer;
    scroll-snap-align: start;
    display: flex;
    flex-direction: column;
    box-sizing: border-box;
    box-shadow: 0 1px 3px rgba(var(--ink-rgb), 0.04);
    transition: box-shadow 0.2s ease, transform 0.2s ease;
  }

  .featured-card:hover {
    box-shadow: 0 8px 20px rgba(var(--brand-rgb), 0.16);
    transform: translateY(-2px);
  }

  .featured-name {
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
    margin-bottom: 2px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex-shrink: 0;
  }

  .featured-meta {
    font-size: 12px;
    color: var(--meta);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    flex-shrink: 0;
  }

  .featured-arrow {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 32px;
    height: 32px;
    border-radius: 50%;
    background: var(--surface);
    border: 1px solid var(--border-light);
    cursor: pointer;
    font-size: 16px;
    line-height: 1;
    color: var(--icon);
    z-index: 951;
    box-shadow: 0 2px 8px rgba(var(--ink-rgb), 0.08);
    transition: background 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
  }

  .featured-arrow:hover {
    background: var(--brand-tint-faint);
    box-shadow: 0 4px 12px rgba(var(--ink-rgb), 0.12);
  }
  .featured-arrow:active { transform: translateY(-50%) scale(0.92); }
  #featured-prev { left: 6px; }
  #featured-next { right: 6px; }

  /* PAGE LAYOUT SHELL - #content-col stacks #featured above #map-wrapper
     so the carousel only spans the map's width, not the sidebar too. See
     updateChromeOffsets() for keeping #main's top in sync with the topbar. */
  #main {
    position: absolute;
    top: 60px;
    left: 0;
    right: 0;
    bottom: 0;
    display: flex;
  }

  #content-col {
    flex: 1;
    min-width: 0;
    display: flex;
    flex-direction: column;
    height: 100%;
  }

  @media (max-width: 768px) {
    #featured {
      height: 128px;
    }
    #featured-row {
      padding: 0 36px;
    }
    .featured-card {
      min-width: 180px;
      max-width: 180px;
    }
    /* Topbar's cramped enough here that #prayer-widget's padding doesn't
       fit - swap to the compact #prayer-widget-m copy next to the logo. */
    #prayer-widget {
      display: none;
    }
    #prayer-widget-m {
      display: flex;
      padding: 5px 8px;
      gap: 4px;
      font-size: 10px;
    }
    /* Keeps just the icon + countdown ("🌙 6:29:01") beside the logo -
       dropping the "Dhuhr" name label is what actually makes this fit
       next to the logo instead of needing its own row. */
    #prayer-widget-m .prayer-widget-name {
      display: none;
    }
  }

  /* On a phone turned sideways, the viewport is short - the topbar, the
     one-time welcome banner, and the featured carousel stacked on top of
     each other can eat up most or all of that height,
     leaving next to nothing for the map/sidebar underneath (this is what
     "landscape doesn't work" actually was - not a bug in the map itself).
     updateChromeOffsets() in the script measures each of these elements'
     real rendered height and positions #main right after them, so hiding
     the non-essential ones here is enough on its own - no JS changes
     needed, #main's height reclaims that space automatically. */
  @media (max-height: 500px) and (orientation: landscape) {
    #topbar {
      height: 44px;
      grid-template-columns: auto 1fr auto;
      grid-template-areas: "left search filter";
    }
    #topbar-left {
      grid-area: left;
    }
    #search-wrap {
      grid-area: search;
      max-width: 320px;
    }
    #topbar-right {
      grid-area: filter;
    }
    #welcome-banner,
    #featured,
    #prayer-widget-m {
      display: none;
    }
  }

  /* 420px: narrower and the 6 amenity filter buttons + reset button in
     #sidebar-header wrap onto a second line. A flex column, not a scroll
     container itself - #place-list below is the one child that scrolls
     (flex:1/min-height:0), so the native scrollbar is scoped to just that,
     not the whole sidebar including the fixed banner/header rows. */
  #sidebar {
    width: 420px;
    min-width: 420px;
    background: var(--bg-app);
    border-right: 1px solid var(--border-lighter);
    overflow: hidden;
    height: 100%;
    display: flex;
    flex-direction: column;
    transition: width 0.2s ease, min-width 0.2s ease;
  }

  /* The one scrolling region inside #sidebar - see the comment on #sidebar
     above for why this replaced position:sticky on the banner/header. */
  #place-list {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
  }

  #sidebar.collapsed {
    width: 0;
    min-width: 0;
    border-right: none;
    overflow: hidden;
  }

  @media (max-width: 768px) {
    #sidebar {
      position: absolute;
      /* Must beat #featured's z-index: 950 - a lower value here let the
         featured carousel render in front of the sidebar's filter buttons
         on mobile, blocking touches on them (a stacking-order bug, not scroll). */
      z-index: 960;
      box-shadow: 2px 0 8px rgba(var(--ink-rgb), 0.15);
      /* Caps to the viewport on phones narrower than 420px, avoiding
         horizontal page scroll. */
      width: min(420px, 100%);
      min-width: 0;
    }

    /* #sidebar-collapse-btn's left:-14px only makes sense on desktop where
       sidebar/map sit side by side; on mobile's full-width overlay it'd be
       pushed nearly off-screen. #sidebar-toggle in the topbar already
       covers collapse/reopen on mobile, so just hide this one. */
    #sidebar-collapse-btn {
      display: none;
    }
  }

  #sidebar-header {
    padding: 14px 16px 8px;
    font-size: 12px;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--faint-2);
    font-weight: 600;
    /* Fixed, non-scrolling row above #place-list in the flex column (see
       #sidebar's comment); flex-shrink:0 stops it being squeezed. */
    flex-shrink: 0;
    background: var(--bg-app);
    border-bottom: 1px solid var(--border-light);
    padding-bottom: 10px;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 8px;
  }

  /* Restyled as one segmented-control "pill" (grey track, gradient-green
     active chip) rather than individually outlined boxes - the same
     visual language as #filter-group/.filter-btn (the Best/Top rated/
     Closest sort pills up in the topbar), so the two toggle groups read
     as the same family of control instead of two different button
     styles. Each of the 7 buttons (6 categories + reset) is flex:1 so
     they always divide the track's width evenly between them and add up
     to exactly 100%, rather than being sized by their own padding/gap
     and having to be hand-tuned against the sidebar's width every time
     either changes - guarantees one line with no leftover space and no
     wrap, at any sidebar width. */
  #sidebar-header-actions {
    display: flex;
    align-items: center;
    flex-wrap: nowrap;
    width: 100%;
    background: var(--bg-pill);
    border-radius: 14px;
    padding: 3px;
    gap: 2px;
    flex-shrink: 0;
  }

  /* Icon-only (no more "Reset view" label) so it sits as a same-sized
     chip alongside the amenity-category icon buttons instead of being
     the one wider, text-length-dependent button that bumped the row
     down to two lines. Not a toggle like its siblings (there's no
     "active" state for it), so it keeps its own green icon color rather
     than the neutral grey the category chips sit at when off. */
  #sidebar-reset-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* Same flex:1 sizing as .amenity-category-btn below - see
       #sidebar-header-actions for why. */
    flex: 1 1 0;
    min-width: 0;
    background: transparent;
    border: none;
    color: var(--brand-dark);
    font-size: 22px;
    font-weight: 600;
    line-height: 1;
    text-transform: none;
    letter-spacing: normal;
    padding: 8px 0;
    border-radius: 11px;
    cursor: pointer;
    white-space: nowrap;
    transition: background 0.2s ease, transform 0.15s ease;
  }

  #sidebar-reset-btn:hover {
    background: rgba(var(--ink-rgb), 0.06);
  }

  #sidebar-reset-btn:active {
    transform: scale(0.94);
  }

  /* A real <a href="?categories=..."> rather than a <button> - see the
     HTML below - so each amenity category has a crawlable, static link
     in the page source for SEO, not just a JS-only click target. Reset
     the usual anchor styling (underline, link-blue) since it's styled
     and behaves as a toggle-button chip instead. */
  .amenity-category-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    /* flex:1 rather than flex-shrink:0 - all 7 buttons in the row (see
       #sidebar-header-actions) divide its width evenly between them and
       always add up to exactly 100%, so the row fits on one line and
       fills it completely regardless of the sidebar's exact width. */
    flex: 1 1 0;
    min-width: 0;
    background: transparent;
    border: none;
    color: var(--muted);
    padding: 8px 0;
    border-radius: 11px;
    cursor: pointer;
    line-height: 1;
    text-decoration: none;
    transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
  }

  /* Bigger than their hardcoded 15x15 markup attributes (which stay as a
     sane non-JS fallback size) - scales the icons up to match the
     larger button chips above without needing to touch the SVG markup
     itself. */
  .amenity-category-btn svg {
    width: 23px;
    height: 23px;
  }

  .amenity-category-btn:hover { color: var(--ink); background: rgba(var(--ink-rgb), 0.06); }
  .amenity-category-btn:active { transform: scale(0.94); }

  /* Toggled-on state - the same brand gradient + dark-green ink used by
     .filter-btn.active (the sort pills), so a toggled-on category and a
     selected sort option read as the exact same kind of "this one's
     chosen" treatment. Shared by all six amenity categories rather than
     giving any one of them its own distinct "active" color, since
     they're all the same kind of toggle. */
  .amenity-category-btn.active {
    background: var(--brand-gradient);
    color: var(--brand-ink);
    font-weight: 600;
    box-shadow: 0 2px 8px rgba(var(--brand-rgb), 0.35);
  }

  /* Floats over the map at the sidebar/map seam rather than living
     inside the sidebar header row - with five amenity-category buttons
     plus reset/collapse crammed into that row now (after adding the
     "clothing" category), the row could overflow its fixed-width
     sidebar and push this button out of view entirely. Living in
     #map-wrapper instead (see the HTML below, right next to its mirror
     image #sidebar-reopen-btn) means it's never subject to the
     header's own layout/overflow - it's always in the same fixed
     spot, half over the sidebar's right edge and half over the map. */
  #sidebar-collapse-btn {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 12px;
    left: -14px;
    z-index: 970;
    width: 28px;
    height: 28px;
    background: var(--surface);
    border: none;
    color: var(--icon);
    font-size: 13px;
    line-height: 1;
    border-radius: 8px;
    cursor: pointer;
    text-transform: none;
    letter-spacing: normal;
    font-weight: 700;
    box-shadow: 0 2px 10px rgba(var(--ink-rgb), 0.25);
    transition: background 0.15s ease, box-shadow 0.15s ease;
  }

  #sidebar-collapse-btn.hidden { display: none; }

  #sidebar-collapse-btn:hover {
    background: var(--bg-hover);
    box-shadow: 0 4px 14px rgba(var(--ink-rgb), 0.3);
  }

  #sidebar-reopen-btn {
    display: none;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 900;
    width: 36px;
    height: 36px;
    border: none;
    border-radius: 8px;
    background: var(--surface);
    color: var(--icon);
    font-size: 16px;
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(var(--ink-rgb), 0.25);
    transition: background 0.15s ease, box-shadow 0.15s ease;
  }

  #sidebar-reopen-btn.visible { display: flex; }

  #sidebar-reopen-btn:hover {
    background: var(--bg-hover);
    box-shadow: 0 4px 14px rgba(var(--ink-rgb), 0.3);
  }

  /* ============================================================
     SIDEBAR PLACE CARDS
     ============================================================ */
  .place-card {
    padding: 12px 16px;
    border-bottom: 1px solid var(--divider);
    cursor: pointer;
    border-left: 3px solid transparent;
    transition: background 0.18s ease, border-color 0.18s ease, padding-left 0.18s ease;
  }

  .place-card:hover { background: var(--hover-tint-green); }
  .place-card.active { background: var(--brand-tint); border-left-color: var(--brand); }

  .place-name {
    font-size: 14px;
    font-weight: 600;
    color: var(--ink);
    margin-bottom: 3px;
  }

  .place-meta {
    font-size: 12px;
    color: var(--meta);
    display: flex;
    align-items: center;
    gap: 4px;
  }

  .place-rating {
    color: var(--star);
    font-weight: 600;
  }

  .place-address {
    font-size: 12px;
    color: var(--faint);
    margin-top: 2px;
  }

  .place-details {
    font-size: 12px;
    color: var(--muted);
    margin-top: 4px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  .featured-details {
    font-size: 11px;
    line-height: 1.35;
    color: var(--meta);
    margin-top: 3px;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    flex-shrink: 0;
  }

  /* Styled as a distinct callout (outline, no fill) rather than plain
     text, so the description - usually the most useful part of the
     popup for deciding whether to go - doesn't get lost among the
     rating/address/actions rows around it. Used to have a tinted green
     background here, but that lowered contrast against the dark text
     enough to be hard to read - plain white/surface (matching the card
     it sits on) with just a border reads as a distinct box just as well
     without that trade-off. */
  .popup-details {
    font-size: 13.5px;
    line-height: 1.45;
    color: var(--ink);
    background: transparent;
    border: 1px solid var(--border-lighter);
    border-radius: 8px;
    padding: 8px 10px;
    margin: 8px 0;
    display: -webkit-box;
    -webkit-line-clamp: 5;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* Inside the place modal (not the static SSR fallback card, which has
     no JS to scroll it and stays clamped), a long description scrolls
     within its own box instead of being cut off after 5 lines - swaps
     the -webkit-line-clamp truncation for a fixed max-height + its own
     scrollbar. */
  .place-modal-content .popup-details {
    display: block;
    -webkit-line-clamp: unset;
    font-size: 14.5px;
    max-height: 160px;
    overflow-y: auto;
  }

  /* The rest of the modal's content scaled up to match its larger card
     size (440px on desktop, almost the full map area on mobile) - same
     fonts/buttons as the old small map popup, just bigger, and still the
     same divided "grid" of rows (rating/address/meta) underneath. */
  .place-modal-content .popup-thumb {
    height: 150px;
  }

  .place-modal-content .popup-title {
    font-size: 19px;
  }

  .place-modal-content .popup-rating {
    font-size: 15px;
    padding-bottom: 8px;
    margin-bottom: 8px;
  }

  .place-modal-content .popup-address {
    font-size: 14px;
    padding-bottom: 8px;
    margin-bottom: 8px;
  }

  .place-modal-content .popup-phone {
    font-size: 14px;
  }

  .place-modal-content .popup-meta-row {
    min-height: 30px;
    padding-bottom: 10px;
    margin-bottom: 10px;
  }

  .place-modal-content .popup-distance {
    font-size: 13px;
    padding: 3px 12px;
  }

  .place-modal-content .popup-icon-btn {
    width: 34px;
    height: 34px;
  }

  .place-modal-content .popup-icon-btn svg {
    width: 19px;
    height: 19px;
  }

  /* Scale the photo-overlay buttons (report, Facebook, Instagram) up
     to match .popup-icon-btn's larger modal size, same as immediately
     above - keeps every round icon button in the modal the same size and
     padding regardless of whether it sits on the photo or below it. */
  .place-modal-content .popup-thumb-report-btn,
  .place-modal-content .popup-thumb-social-btn {
    width: 34px;
    height: 34px;
  }

  .place-modal-content .popup-thumb-report-btn svg,
  .place-modal-content .popup-thumb-social-btn svg {
    width: 19px;
    height: 19px;
  }

  .place-modal-content .popup-link {
    font-size: 13px;
    padding: 10px;
  }

  .place-modal-content .popup-link-apple {
    font-size: 13px !important;
  }

  .place-modal-content .popup-link svg {
    width: 19px;
    height: 19px;
  }

  /* ============================================================
     EMPTY STATE, MAP CONTAINER & RESET-VIEW CONTROL
     ============================================================ */
  #empty-state {
    padding: 30px 16px;
    color: var(--faint);
    font-size: 13px;
    text-align: center;
  }

  #map-wrapper {
    flex: 1;
    min-height: 0;
    position: relative;
  }

  #map {
    height: 100%;
    width: 100%;
  }

  #reset-bar {
    position: absolute;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%) translateY(8px);
    z-index: 900;
    background: var(--brand-gradient);
    color: var(--brand-ink);
    font-weight: 700;
    font-size: 13px;
    padding: 11px 22px;
    border-radius: 24px;
    cursor: pointer;
    box-shadow: 0 4px 14px rgba(var(--ink-rgb), 0.28);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
    white-space: nowrap;
  }

  #reset-bar.visible {
    opacity: 1;
    pointer-events: auto;
    transform: translateX(-50%) translateY(0);
  }

  #reset-bar:hover {
    box-shadow: 0 6px 18px rgba(var(--ink-rgb), 0.32);
    transform: translateX(-50%) translateY(-2px);
  }

  /* ============================================================
     TOAST NOTIFICATIONS (e.g. "Link copied to clipboard")
     ============================================================ */
  #toast {
    position: absolute;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%) translateY(8px);
    background: var(--ink);
    color: var(--surface);
    font-size: 13px;
    font-weight: 600;
    padding: 10px 18px;
    border-radius: 20px;
    box-shadow: 0 6px 18px rgba(var(--ink-rgb), 0.3);
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease, transform 0.2s ease;
    z-index: 1000;
    white-space: nowrap;
  }

  #toast.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }

  /* ============================================================
     THUMBNAIL IMAGES - shared by sidebar cards, featured cards, and popups
     ============================================================ */
  .place-thumb {
    width: 44px;
    height: 44px;
    border-radius: 8px;
    object-fit: cover;
    flex-shrink: 0;
  }

  .place-thumb-fallback {
    object-fit: contain;
    background: var(--brand-tint);
    padding: 5px;
  }

  /* Wraps a featured place's sidebar thumbnail in a decorative ring +
     star badge (see featuredThumbRingSvg) - sized a bit larger than the
     44x44 thumbnail so the ring sits just outside its edge rather than
     overlapping the photo itself. */
  .place-thumb-frame {
    position: relative;
    width: 52px;
    height: 52px;
    flex-shrink: 0;
  }

  .place-thumb-frame .place-thumb {
    position: absolute;
    inset: 4px;
    width: 44px;
    height: 44px;
  }

  .place-thumb-ring {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
  }

  /* Every card in the featured carousel is, by definition, a featured
     place. The green border itself lives on .featured-card as a whole
     (see below) rather than on the thumbnail - this wrapper just
     positions the small corner star badge (same one used on featured
     .place-thumb in the sidebar) relative to the image. */
  .featured-thumb-frame {
    position: relative;
    width: 100%;
    height: 46px;
    margin-bottom: 3px;
    flex-shrink: 0;
  }

  .featured-thumb-frame .featured-thumb {
    position: absolute;
    inset: 3px;
    width: calc(100% - 6px);
    height: 40px;
    margin-bottom: 0;
  }

  .featured-thumb-badge {
    position: absolute;
    right: -4px;
    bottom: -1px;
    width: 17px;
    height: 17px;
    pointer-events: none;
  }

  .featured-thumb {
    width: 100%;
    height: 46px;
    border-radius: 8px;
    object-fit: cover;
    margin-bottom: 3px;
    flex-shrink: 0;
  }

  .featured-thumb-fallback {
    object-fit: contain;
    background: var(--brand-tint);
    padding: 4px;
  }

  .popup-thumb {
    width: 100%;
    height: 90px;
    border-radius: 8px;
    object-fit: cover;
    margin-bottom: 6px;
  }

  .popup-thumb-fallback {
    object-fit: contain;
    background: var(--brand-tint);
    padding: 10px;
  }

  /* Wraps the popup photo so the "report this listing" button below can
     be positioned in its top-left corner, overlaid on the image itself. */
  .popup-thumb-wrap {
    position: relative;
  }

  /* Small circular info-icon button sitting on top of the popup photo -
     dark semi-transparent background regardless of dark/light mode since
     it needs to stay legible over an arbitrary photo, not just the page
     background like the other .popup-icon-btns. */
  .popup-thumb-report-btn {
    position: absolute;
    top: 6px;
    left: 6px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.45);
    color: #fff;
    cursor: pointer;
    transition: background 0.15s ease;
  }

  .popup-thumb-report-btn:hover { background: rgba(0, 0, 0, 0.68); }

  /* Facebook / Instagram links, overlaid on the opposite (bottom-right)
     corner of the same photo the report button sits on - keeps them from
     adding any extra vertical space to the card. Hidden entirely (see
     socialLinksHtml()) unless a place has that specific key set in
     places.json. */
  .popup-thumb-social-links {
    position: absolute;
    bottom: 6px;
    right: 6px;
    display: flex;
    gap: 6px;
  }

  /* Same dark semi-transparent treatment as .popup-thumb-report-btn above
     - needs to stay legible over an arbitrary photo regardless of dark/
     light mode. These are <a> tags (unlike the report button), and their
     icons use stroke="currentColor", so the color rule below needs
     !important to survive Leaflet's own higher-specificity default
     anchor color - same reasoning as .popup-delivery-link further down. */
  .popup-thumb-social-btn,
  .popup-thumb-social-btn:link,
  .popup-thumb-social-btn:visited {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: rgba(0, 0, 0, 0.45);
    color: #fff !important;
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s ease;
  }

  .popup-thumb-social-btn:hover { background: rgba(0, 0, 0, 0.68); }


  /* (place-card layout, continued - the shared thumbnail styles above sit
     between this and the rest of the place-card rules above) */
  .place-card-inner {
    display: flex;
    gap: 10px;
    align-items: flex-start;
  }

  .place-card-text {
    flex: 1;
    min-width: 0;
  }

  /* ============================================================
     MAP POPUP CONTENT - the card that opens when you tap a marker (the "map menu")
     ============================================================
     The rating/address/meta rows below each get a hairline bottom
     border (var(--divider), the same tone used between sidebar place
     cards) rather than relying on margin alone to separate them - reads
     as a light grid of rows instead of text just floating in a stack. */
  .popup-title {
    font-size: 15px;
    font-weight: 700;
    margin: 0 0 4px 0;
  }

  .popup-rating {
    color: var(--star);
    font-weight: 600;
    font-size: 13px;
    margin-bottom: 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--divider);
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }

  /* Facebook's own "% recommend" figure, shown in Facebook's brand blue
     so it reads as clearly distinct from the Google star rating (green/
     gold) right beside it rather than looking like part of the same
     rating scale. */
  .popup-facebook-review {
    display: inline-flex;
    align-items: center;
    gap: 3px;
    color: #1877f2;
    font-weight: 700;
  }

  .popup-facebook-review svg {
    flex-shrink: 0;
  }

  :root.dark .popup-facebook-review { color: #4599ff; }

  /* Same inline-flex icon+figure treatment as .popup-facebook-review
     above, just for the Google star rating - keeps the two review
     sources visually consistent (logo, then figure) instead of only
     Facebook's carrying a mark. */
  .google-rating-badge {
    display: inline-flex;
    align-items: center;
    gap: 3px;
  }

  .google-rating-badge svg {
    flex-shrink: 0;
  }

  .popup-delivery-links {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex-shrink: 0;
  }

  /* .popup-delivery-link is an <a>, like .popup-website-btn - needs its
     own !important color override for the same reason: Leaflet's own
     popup-content CSS sets a higher-specificity default anchor color
     that would otherwise win over .popup-icon-btn's plain color rule.
     The Uber Eats / SkipTheDishes icons themselves are drawn with fixed
     brand-colour fills rather than currentColor, so this doesn't change
     their appearance - it's here so the round button background/hover
     behaves exactly like its .popup-icon-btn siblings (bookmark, website,
     share). */
  .popup-delivery-link,
  .popup-delivery-link:link,
  .popup-delivery-link:visited {
    color: var(--icon) !important;
  }

  .popup-address {
    font-size: 12px;
    color: var(--muted);
    margin-bottom: 6px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--divider);
  }

  .popup-phone,
  .popup-phone:link,
  .popup-phone:visited {
    color: var(--icon) !important;
    font-weight: 600;
    text-decoration: none;
    display: inline-flex;
    align-items: center;
    gap: 3px;
    vertical-align: middle;
  }

  .popup-phone:hover { text-decoration: underline; }

  .popup-meta-row {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 8px;
    padding-bottom: 8px;
    border-bottom: 1px solid var(--divider);
    min-height: 24px;
  }

  .popup-distance {
    display: inline-block;
    font-size: 12px;
    background: var(--distance-bg);
    color: var(--distance-ink);
    font-weight: 700;
    padding: 2px 10px;
    border-radius: 10px;
  }

  /* Plain-text category label ("Restaurant", "Clothing Store", "Place of
     Worship") sitting in the leftover space between the distance pill and
     the bookmark/website/share icons - quiet metadata, not a chip, so it
     doesn't compete visually with the distance badge next to it. */
  .popup-category {
    font-size: 12px;
    color: var(--muted);
  }

  /* Dine-in / takeout / delivery availability badges - plain icons (no
     background chip, unlike the distance pill) so they read as quiet
     metadata rather than competing for attention. */
  .popup-diet-badge {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    color: var(--icon);
  }

  .popup-meta-actions {
    margin-left: auto;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    gap: 6px;
  }

  /* Shared by the bookmark/share <button>s and the website <a> - the
     anchor needs its own text-decoration reset since buttons don't have
     an underline to begin with. */
  .popup-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    border: none;
    border-radius: 50%;
    background: var(--bg-pill);
    color: var(--icon);
    cursor: pointer;
    text-decoration: none;
    transition: background 0.15s ease, color 0.15s ease;
  }

  .popup-icon-btn:hover { background: var(--hover-tint-gray); }

  /* .popup-website-btn is an <a>, unlike its sibling bookmark/share
     <button>s - Leaflet's own CSS sets a higher-specificity default
     color on anchors inside popup content, which otherwise wins over
     .popup-icon-btn's color above and turns this one blue instead of
     matching the others. */
  .popup-website-btn,
  .popup-website-btn:link,
  .popup-website-btn:visited {
    color: var(--icon) !important;
  }

  .popup-bookmark-btn.is-bookmarked { color: #e0245e; }
  .popup-bookmark-btn.is-bookmarked svg path { fill: currentColor; }

  .popup-actions {
    display: flex;
    gap: 8px;
    margin-top: 2px;
  }

  /* The Google Maps link is an <a>, the Apple Maps one is a <button> (see
     the HTML/JS that build these) - without an explicit appearance/
     line-height reset, browsers (Safari in particular) apply their own
     form-control font metrics to the <button> on top of its font-size,
     so it rendered visibly larger/heavier than the <a> next to it
     despite sharing this exact same class. appearance:none plus an
     explicit line-height makes the two render identically regardless of
     which element they are. */
  .popup-link,
  .popup-link:link,
  .popup-link:visited {
    flex: 1;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    background: transparent;
    color: var(--brand-dark) !important;
    border: 1.5px solid var(--brand-dark);
    text-decoration: none;
    font-weight: 600;
    font-size: 11px;
    line-height: 1.3;
    padding: 6px 8px;
    border-radius: 6px;
    cursor: pointer;
    font-family: inherit;
    -webkit-appearance: none;
    appearance: none;
    white-space: nowrap;
    transition: background 0.15s ease;
  }

  .popup-link:hover, .popup-link:visited:hover { background: rgba(var(--brand-rgb), 0.12); }

  .popup-link-apple,
  .popup-link-apple:link,
  .popup-link-apple:visited {
    color: var(--ink) !important;
    border-color: var(--ink);
    /* Belt-and-suspenders on top of the appearance:none reset above -
       Safari in particular still let the <button>'s own form-control
       font metrics leak through and render this larger/heavier than
       the <a> next to it despite both sharing .popup-link, so restate
       the exact same values here explicitly rather than relying on
       .popup-link's cascade to win. */
    font-size: 11px !important;
    font-weight: 600 !important;
    line-height: 1.3 !important;
  }

  .popup-link-apple:hover, .popup-link-apple:visited:hover { background: rgba(var(--ink-tint-rgb), 0.1); }

  /* On mobile, the map popup (at Leaflet's default ~300px width, plus a
     90px-tall photo, a 5-line description, and full-size text/buttons)
     was eating a large share of a small screen. Shrinking the width,
     most font sizes, and the photo height keeps the whole thing compact
     enough to leave real room for the map underneath it - 230px rather
     than the original 210px, though, to give the now-boxed description
     (see .popup-details) enough width that it doesn't wrap into just
     two or three words per line. */
  @media (max-width: 768px) {
    .leaflet-popup-content {
      width: 230px !important;
    }

    .popup-thumb {
      height: 60px;
    }

    .popup-title {
      font-size: 13px;
    }

    .popup-rating {
      font-size: 11px;
      padding-bottom: 5px;
      margin-bottom: 5px;
    }

    .popup-address {
      font-size: 11px;
      padding-bottom: 5px;
      margin-bottom: 5px;
    }

    .popup-phone {
      font-size: 12px;
    }

    .popup-details {
      font-size: 12px;
      padding: 7px 8px;
      margin: 6px 0;
      /* Wider than the old 210px popup (see above) and boxed rather than
         plain text, so this can afford to show more of the description
         than the old 3-line clamp before it gets cut off. */
      -webkit-line-clamp: 5;
    }

    .popup-distance {
      font-size: 11px;
      padding: 1px 8px;
    }

    .popup-meta-row {
      flex-wrap: wrap;
      row-gap: 6px;
      padding-bottom: 6px;
      margin-bottom: 6px;
    }

    .popup-icon-btn {
      width: 34px;
      height: 34px;
    }

    /* The icon SVGs (bookmark/website/share, and the Google/Apple Maps
       buttons below) carry their own hardcoded width/height attributes
       sized for desktop - CSS still overrides the actual rendered size
       regardless, so this scales them up without touching the shared
       JS-side icon markup used on desktop too. */
    .popup-icon-btn svg {
      width: 19px;
      height: 19px;
    }

    .popup-thumb-report-btn,
    .popup-thumb-social-btn {
      width: 34px;
      height: 34px;
    }

    .popup-thumb-report-btn svg,
    .popup-thumb-social-btn svg {
      width: 19px;
      height: 19px;
    }

    .popup-link {
      font-size: 10px;
      padding: 8px;
    }

    .popup-link-apple {
      font-size: 10px !important;
    }

    .popup-link svg {
      width: 19px;
      height: 19px;
    }

    /* The visible text ("Open Google Maps"/"Open Apple Maps") doesn't
       fit two-across in a 210px popup even at a smaller font, since
       .popup-link's white-space:nowrap won't let it wrap or shrink
       below its content width - go icon-only instead. Each link/button
       already carries a title (and the Apple one an aria-label too)
       from earlier accessibility work, so the accessible name survives
       even with the visible label hidden. */
    .popup-link span {
      display: none;
    }
  }

  /* Leaflet's default popup shadow is a plain dark drop-shadow. Give it the
     same green glow used on featured-card hover, so the map popup ('the
     map menu') visually matches the rest of the app's brand accent instead
     of looking like an unstyled library default. */
  .leaflet-popup-content-wrapper {
    border-radius: 10px;
    box-shadow: 0 8px 24px rgba(var(--brand-rgb), 0.25), 0 2px 8px rgba(var(--ink-rgb), 0.12);
  }

  /* ============================================================
     PLACE MODAL - replaces Leaflet's own popup for showing a place's
     details when a pin is tapped. A real Leaflet popup auto-pans the
     map to keep itself in view, which made the whole map visibly shift
     every time a pin near an edge was clicked. This modal instead
     floats centered over the map area itself - it's a sibling inside
     #map-wrapper, not fixed to the whole viewport, so the sidebar next
     to it is untouched and the map underneath never moves. Styled like
     .app-modal (centered card) but scoped to #map-wrapper's own bounds,
     and deliberately without .app-modal's dark/blurred backdrop - the
     map stays fully visible (and the still-animating open pin visible
     on it) behind the card instead of being dimmed out. Reuses the same
     .popup-* content classes (title/rating/address/etc.) as before via
     buildPopupHtml().
     ============================================================ */
  .place-modal-backdrop {
    position: absolute;
    inset: 0;
    /* Leaflet's own controls (the zoom +/- buttons in particular, see
       L.control.zoom({ position: 'topright' }) in the script) render at
       their library-default z-index of 1000 - comfortably above this
       used to sit at (900), which let the zoom control paint on top of
       (and swallow clicks meant for) the modal's own close button
       whenever the two visually overlapped, most noticeably on mobile
       where the modal card nearly fills the map area and its close
       button pokes out right into the zoom control's corner. A modal
       dialog should always be topmost over the map it's covering, so
       this now clears every Leaflet control layer with room to spare. */
    z-index: 1200;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 24px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.2s ease;
  }

  .place-modal-backdrop.visible {
    opacity: 1;
    pointer-events: auto;
  }

  /* overflow stays visible here (not auto) - the close button below is
     positioned just outside this box's corner, and an "overflow: auto"
     on the box it's sitting in clips anything poking past its edge,
     which was cutting the button off entirely. Scrolling for long
     content happens one level in, on .place-modal-content, instead. */
  .place-modal {
    position: relative;
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border-radius: 14px;
    /* Bigger than the map popup this replaced (320px) - more breathing
       room on desktop for the larger fonts/buttons below (see the
       .place-modal-content overrides further down), and there's ample
       spare width over the map to use. */
    width: 440px;
    max-width: 100%;
    max-height: 100%;
    overflow: visible;
    box-shadow: 0 20px 50px rgba(var(--ink-rgb), 0.3), 0 8px 24px rgba(var(--brand-rgb), 0.2);
    /* Slides/"shoots" up from below the map area on open, and back down
       on close (this same rule runs in reverse once .visible is removed
       from the backdrop) - a bigger vertical throw and a snappy ease-out
       curve read as more of a deliberate motion than the old subtle
       10px nudge. */
    transform: translateY(70px);
    opacity: 0;
    transition: transform 0.35s cubic-bezier(0.16, 1, 0.3, 1), opacity 0.25s ease;
  }

  .place-modal-backdrop.visible .place-modal {
    transform: translateY(0);
    opacity: 1;
  }

  /* Scrolls internally once its content is taller than the modal's own
     max-height, rather than the whole card growing past the map area or
     (as .place-modal itself used to do) clipping the floating close
     button that sits just outside its corner. No bottom padding here -
     .popup-actions below supplies its own, since it's a sticky footer
     rather than part of the scrolling flow. */
  .place-modal-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    padding: 14px 16px 0;
  }

  /* Pinned to the bottom of the scrolling content instead of scrolling
     away with it - the Google/Apple Maps buttons stay reachable no
     matter how far down the description you've scrolled, rather than
     ending up cut off past the visible edge of the card. The solid
     background (matching the card) covers whatever's scrolled behind
     it, and the upward shadow reads as it floating a little above that
     content instead of just overlapping it. */
  .place-modal-content .popup-actions {
    position: sticky;
    bottom: 0;
    background: var(--surface);
    padding: 10px 0 16px;
    margin-top: 4px;
    box-shadow: 0 -8px 10px -8px rgba(var(--ink-rgb), 0.18);
  }

  /* Same green-border treatment the old featured-popup className gave
     the Leaflet popup wrapper. */
  .place-modal.featured-place-modal {
    border: 2px solid var(--brand);
  }

  /* Same floating-pill treatment as #sidebar-collapse-btn - sitting just
     outside the card's corner rather than overlaid on top of its content,
     with the same surface background/icon color/shadow so it reads as
     the same family of control. */
  .place-modal-close {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: -16px;
    right: -16px;
    z-index: 5;
    width: 32px;
    height: 32px;
    background: var(--surface);
    border: none;
    color: var(--icon);
    font-size: 18px;
    line-height: 1;
    border-radius: 50%;
    cursor: pointer;
    box-shadow: 0 2px 10px rgba(var(--ink-rgb), 0.25);
    transition: background 0.15s ease, box-shadow 0.15s ease;
  }

  .place-modal-close:hover {
    background: var(--bg-hover);
    box-shadow: 0 4px 14px rgba(var(--ink-rgb), 0.3);
  }

  /* On mobile the modal takes up almost all of the available map area
     instead of floating as a small card - just enough margin so it still
     reads as a card sitting over the map rather than a full-bleed page,
     but otherwise as much of the screen as the map itself occupies. Also
     needs to be wide enough that the close button (which pokes out 16px
     past the card's own top-left corner - see .place-modal-close below)
     stays fully visible with a little breathing room around it, instead
     of being cropped against the screen edge the old 4px margin left it
     pinned to. */
  @media (max-width: 768px) {
    .place-modal-backdrop {
      padding: 20px;
    }

    .place-modal {
      width: 100%;
      height: 100%;
      /* Full bottom-sheet throw on mobile - starts entirely below the
         visible map area, rather than the smaller 70px nudge used for
         the floating desktop card. */
      transform: translateY(100%);
    }

    /* On mobile the card fills nearly the whole map area, so this
       button's usual top-right spot (see the desktop rule above) lands
       right on top of the zoom +/- control (L.control.zoom's
       position: 'topright' in the script) - overlapping controls are
       hard to aim a thumb at even with the stacking-order fix above.
       Moving it to the top-left instead sidesteps the zoom control
       entirely, since nothing else claims that corner while a place
       modal is open. */
    .place-modal-close {
      right: auto;
      left: -16px;
    }
  }

  /* ============================================================
     MAP MARKERS - the "you are here" dot and the featured-place pulse
     ============================================================ */
  .user-marker-wrap {
    position: relative;
    width: 22px;
    height: 22px;
  }

  .user-dot {
    position: relative;
    z-index: 2;
    width: 22px;
    height: 22px;
    background: var(--user-location);
    border: 3px solid var(--surface);
    border-radius: 50%;
    box-shadow: 0 0 0 2px rgba(66,133,244,0.4);
  }

  .user-pulse-ring {
    position: absolute;
    z-index: 1;
    left: 50%;
    top: 50%;
    width: 22px;
    height: 22px;
    margin-left: -11px;
    margin-top: -11px;
    border-radius: 50%;
    border: 3px solid rgba(66,133,244,0.9);
    background: rgba(66,133,244,0.25);
    animation: user-pulse-loop 1.6s cubic-bezier(0.2,0.6,0.4,1) infinite;
    pointer-events: none;
  }

  .user-pulse-ring.ring-2 {
    animation-delay: 0.8s;
  }

  @keyframes user-pulse-loop {
    0% { transform: scale(0.9); opacity: 1; }
    100% { transform: scale(4.5); opacity: 0; }
  }

  .marker-pin {
    position: relative;
    width: 30px;
    height: 42px;
  }

  .marker-pin svg {
    display: block;
  }

  /* Wraps the svg (instead of animating/filtering the <svg> element
     itself) - a plain HTML element is a much safer, more consistent
     target for CSS transform animations and filters across browsers, so
     the bob animation below actually plays everywhere instead of
     silently no-oping in some of them. */
  .marker-icon-shape {
    position: relative;
    z-index: 2;
    display: block;
    /* Grounds the pin with a soft floor shadow instead of the flat, no-
       shadow look it had before - reads as sitting slightly above the
       map rather than printed flat onto it. */
    filter: drop-shadow(0 3px 3px rgba(0, 0, 0, 0.28));
  }

  /* Gentle up/down hover, only on whichever single pin is currently
     open/highlighted in the sidebar (see .is-open in createMarkerIcon) -
     the pin itself bobs while .active-ring below stays anchored at its
     actual map position, so it reads as the pin floating just above its
     own spot rather than the whole icon drifting. */
  .marker-pin.is-open .marker-icon-shape {
    animation: marker-bob 1.6s ease-in-out infinite;
  }

  @keyframes marker-bob {
    0%, 100% { transform: translateY(0); }
    50% { transform: translateY(-7px); }
  }

  /* The pulsing ring, now shown only around the currently open/
     highlighted pin instead of constantly flashing on every featured
     place (which read as distracting rather than helpful) - same visual
     language, just reserved for a single, more meaningful marker at a
     time, and recoloured to match that pin's own bright "open" green.
     Slightly bigger/bolder than the old featured version so it reads
     clearly against the map. */
  .active-ring {
    position: absolute;
    z-index: 1;
    left: 15px;
    top: 42px;
    width: 18px;
    height: 18px;
    margin-left: -9px;
    margin-top: -9px;
    border-radius: 50%;
    border: 3px solid rgba(0, 230, 118, 0.95);
    background: rgba(0, 230, 118, 0.32);
    animation: pulse-loop 1.8s cubic-bezier(0.2,0.6,0.4,1) infinite;
    /* Was pointer-events: none - but this ring visually grows (via the
       animation's scale transform) well past the marker's actual 30x42px
       icon box, and it's the most visually obvious part of the open
       pin. With pointer-events left at "none" here, clicks/hovers
       landing on that expanded halo fell straight through to the map
       underneath instead of bubbling up to the marker's own click/
       mouseover handlers (which are bound higher up the DOM tree, on the
       icon wrapper) - so clicking it often silently did nothing on
       desktop unless you happened to hit the small static pin shape
       exactly. Since this element has no click/hover handlers of its
       own, letting it receive pointer events is safe: any click or
       hover on it just bubbles up to the marker as normal. */
    pointer-events: auto;
  }

  .active-ring.ring-2 {
    animation-delay: 0.9s;
  }

  @keyframes pulse-loop {
    0% { transform: scale(0.8); opacity: 1; }
    100% { transform: scale(3.4); opacity: 0; }
  }

  /* ============================================================
     DISTANCE BADGE - shared by sidebar cards, featured cards, and popups
     ============================================================ */
  .place-distance {
    display: inline-block;
    background: var(--distance-bg);
    color: var(--distance-ink);
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 10px;
  }

  .featured-distance {
    display: inline-block;
    background: var(--distance-bg);
    color: var(--distance-ink);
    font-weight: 700;
    padding: 1px 8px;
    border-radius: 10px;
  }

  /* ============================================================
     LOADING / SPLASH SCREEN
     ============================================================ */
  #loading-overlay {
    position: fixed;
    inset: 0;
    background: radial-gradient(circle at 50% 42%, var(--surface) 0%, var(--loading-glow) 100%);
    z-index: 3000;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: opacity 0.4s ease;
    /* CSS-only safety net for when JS never runs at all (scripting
       disabled/blocked): maybeHideLoadingOverlay() in the main script
       normally adds .hidden itself, once both the border-draw animation
       and the data fetch finish - usually within a couple of seconds -
       but with no JS, neither of those ever happens, and the overlay
       would otherwise sit there forever, permanently covering the
       content underneath. This animation runs unconditionally, needs no
       JS, and does the same job .hidden does (opacity 0, pointer-events
       none) once its delay elapses.
       The delay (6s - double the 3s border-draw animation) is chosen
       to comfortably outlast a normal JS-driven hide, which almost
       always finishes well before that; on the rare page where JS is
       genuinely unavailable, this is what removes the overlay instead. */
    animation: loading-overlay-auto-hide 0.4s ease 6s forwards;
  }

  #loading-overlay.hidden {
    opacity: 0;
    pointer-events: none;
  }

  @keyframes loading-overlay-auto-hide {
    to {
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
    }
  }

  #loading-border {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 3001;
    pointer-events: none;
  }

  #loading-border-rect {
    x: 5px;
    y: 5px;
    width: calc(100% - 10px);
    height: calc(100% - 10px);
    fill: none;
    stroke: var(--brand-grad-end);
    stroke-width: 10px;
    stroke-linecap: round;
    stroke-dasharray: 100 100;
    stroke-dashoffset: 100;
    animation: loading-border-draw 3s cubic-bezier(0.45, 0, 0.4, 1) forwards;
  }

  @keyframes loading-border-draw {
    to { stroke-dashoffset: 0; }
  }

  #loading-content {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 6px;
    max-width: 90vw;
    padding: 0 16px;
    text-align: center;
  }

  #loading-prayer-illustration {
    width: 220px;
    max-width: 70vw;
    height: auto;
    filter: drop-shadow(0 6px 20px rgba(var(--brand-rgb), 0.2));
    /* A slow "breathing" scale - deliberately calm rather than a
       bounce/spin, since this illustration depicts people in prayer,
       but with enough amplitude to actually read as a pulse rather
       than disappear entirely. */
    animation: prayer-illustration-breathe 3s ease-in-out infinite;
  }

  @keyframes prayer-illustration-breathe {
    0%, 100% { transform: scale(1); }
    50% { transform: scale(1.06); }
  }

  #prayer-glow {
    transform-origin: 150px 70px;
    animation: prayer-glow-pulse 3s ease-in-out infinite;
  }

  @keyframes prayer-glow-pulse {
    0%, 100% { opacity: 0.3; transform: scale(0.85); }
    50% { opacity: 1; transform: scale(1.2); }
  }

  #loading-title {
    font-size: clamp(32px, 9vw, 56px);
    font-weight: 800;
    color: var(--ink);
    letter-spacing: 0.2px;
    margin-top: 8px;
  }

  #loading-subtitle {
    font-size: clamp(20px, 6vw, 32px);
    font-weight: 600;
    color: var(--faint-2);
    letter-spacing: 0.1px;
    min-height: 1.2em;
  }

  #loading-feature-notice {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    margin-top: 10px;
    padding: 8px 20px;
    border-radius: 24px;
    background: rgba(232, 162, 12, 0.15);
    color: #a8690a;
    font-size: clamp(16px, 4.2vw, 22px);
    font-weight: 700;
  }

  :root.dark #loading-feature-notice {
    background: rgba(232, 162, 12, 0.18);
    color: #f0b23c;
  }

  /* ============================================================
     APP MODAL - confirmations, location-permission help, etc.
     ============================================================ */
  .app-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15,20,17,0.5);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    z-index: 2500;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow-y: auto;
    padding: 40px 16px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }

  .app-modal-backdrop.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .app-modal {
    position: relative;
    background: var(--surface);
    border-radius: 16px;
    max-width: 320px;
    width: 90%;
    margin: auto 0;
    padding: 22px 22px 18px;
    box-shadow: 0 20px 50px rgba(var(--ink-rgb), 0.25);
    transform: translateY(10px) scale(0.97);
    opacity: 0;
    transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.3, 1.1), opacity 0.2s ease;
    overflow: hidden;
  }

  .app-modal-hero {
    display: none;
    margin: -22px -22px 16px;
  }

  .app-modal.has-hero .app-modal-hero {
    display: block;
  }

  .app-modal-hero svg {
    display: block;
    width: 100%;
    height: auto;
  }

  /* The hero variant reads more like a friendly welcome card than a
     plain alert - centered, a little larger, and without the generic
     pin emoji (the illustration itself carries the visual weight). */
  .app-modal.has-hero .app-modal-title {
    justify-content: center;
    text-align: center;
    font-size: 18px;
  }

  .app-modal.has-hero #app-modal-icon {
    display: none;
  }

  .app-modal.has-hero .app-modal-message {
    text-align: center;
  }

  .app-modal-backdrop.visible .app-modal {
    transform: translateY(0) scale(1);
    opacity: 1;
  }

  .app-modal-title {
    font-size: 16px;
    font-weight: 700;
    color: var(--ink);
    margin-bottom: 8px;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .app-modal-message {
    font-size: 13px;
    color: var(--muted);
    line-height: 1.5;
    margin-bottom: 18px;
    white-space: pre-line;
  }

  .app-modal-message a {
    color: var(--link-ink);
    font-weight: 600;
  }

  .app-modal-btn {
    background: var(--brand-gradient);
    color: var(--brand-ink);
    font-weight: 700;
    font-size: 13px;
    border: none;
    padding: 9px 16px;
    border-radius: 9px;
    cursor: pointer;
    width: 100%;
    box-shadow: 0 2px 8px rgba(var(--brand-rgb), 0.3);
    transition: transform 0.15s ease, box-shadow 0.15s ease;
  }

  .app-modal-btn:hover {
    transform: translateY(-1px);
    box-shadow: 0 4px 12px rgba(var(--brand-rgb), 0.4);
  }
  .app-modal-btn:active { transform: translateY(0); }

  .app-modal-btn.secondary {
    background: transparent;
    color: var(--muted);
    margin-top: 6px;
    box-shadow: none;
    font-weight: 600;
  }

  .app-modal-btn.secondary:hover {
    background: var(--bg-hover);
    transform: none;
    box-shadow: none;
  }

  /* ============================================================
     SUBMIT A PLACE / GET FEATURED MODAL
     ============================================================ */
  .submit-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(15,20,17,0.5);
    backdrop-filter: blur(3px);
    -webkit-backdrop-filter: blur(3px);
    z-index: 2500;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    overflow-y: auto;
    padding: 40px 16px;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.25s ease;
  }

  .submit-modal-backdrop.visible {
    opacity: 1;
    pointer-events: auto;
  }

  .submit-modal {
    position: relative;
    background: var(--surface);
    border-radius: 16px;
    max-width: 420px;
    width: 90%;
    margin: auto 0;
    padding: 24px 22px 20px;
    box-shadow: 0 20px 50px rgba(var(--ink-rgb), 0.25);
    transform: translateY(10px) scale(0.97);
    opacity: 0;
    transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.3, 1.1), opacity 0.2s ease;
  }

  .submit-modal-backdrop.visible .submit-modal {
    transform: translateY(0) scale(1);
    opacity: 1;
  }

  .submit-modal-close {
    position: absolute;
    top: 12px;
    right: 12px;
    width: 28px;
    height: 28px;
    border: none;
    border-radius: 50%;
    background: var(--bg-pill);
    color: var(--icon);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
  }

  .submit-modal-close:hover { background: var(--hover-tint-gray); }

  .submit-modal-title {
    font-size: 18px;
    font-weight: 800;
    color: var(--ink);
    margin-bottom: 4px;
    padding-right: 24px;
  }

  .submit-modal-subtitle {
    font-size: 13px;
    color: var(--muted);
    line-height: 1.5;
    margin-bottom: 16px;
  }

  .submit-field {
    display: block;
    margin-bottom: 12px;
  }

  .submit-field span {
    display: block;
    font-size: 12px;
    font-weight: 700;
    color: var(--ink);
    margin-bottom: 4px;
  }

  .required-mark { color: var(--brand-dark); }

  .submit-field input,
  .submit-field textarea {
    width: 100%;
    border: 1px solid var(--border);
    border-radius: 9px;
    padding: 9px 12px;
    font-size: 14px;
    font-family: inherit;
    color: var(--ink);
    background: var(--surface);
    outline: none;
    box-sizing: border-box;
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
  }

  .submit-field textarea { resize: vertical; min-height: 60px; }

  .submit-field input:focus,
  .submit-field textarea:focus {
    border-color: var(--brand);
    box-shadow: 0 0 0 3px rgba(var(--brand-rgb), 0.15);
  }

  .submit-recaptcha-notice {
    margin: 4px 0 14px;
    font-size: 11px;
    line-height: 1.4;
    color: var(--faint);
    text-align: center;
  }

  .submit-recaptcha-notice a {
    color: inherit;
    text-decoration: underline;
  }

  .submit-modal-error {
    font-size: 12px;
    font-weight: 600;
    color: #c0392b;
    margin-bottom: 10px;
    min-height: 1em;
  }

  .submit-modal-btn {
    background: var(--brand-gradient);
    color: var(--brand-ink);
    font-weight: 700;
    font-size: 14px;
    border: none;
    padding: 10px 16px;
    border-radius: 9px;
    cursor: pointer;
    width: 100%;
    box-shadow: 0 2px 8px rgba(var(--brand-rgb), 0.3);
    transition: transform 0.15s ease, box-shadow 0.15s ease, opacity 0.15s ease;
  }

  .submit-modal-btn:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(var(--brand-rgb), 0.4); }
  .submit-modal-btn:active { transform: translateY(0); }
  .submit-modal-btn:disabled { opacity: 0.6; cursor: default; transform: none; box-shadow: none; }

  .submit-modal-result { display: none; }
  .submit-modal-result.inline { margin-bottom: 14px; }

  /* ---- Success animation: location emoji + confetti ---- */
  .success-emoji {
    font-size: 64px;
    line-height: 1;
    display: block;
    text-align: center;
    margin: 0 auto 10px;
    animation: emoji-bob 1.4s ease-in-out infinite;
  }

  @keyframes emoji-bob {
    0%, 100% { transform: translateY(0); }
    50%      { transform: translateY(-14px); }
  }

  .confetti-container {
    position: fixed;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 2600;
  }

  .confetti-piece {
    position: absolute;
    top: -20px;
    border-radius: 2px;
    opacity: 0.95;
    animation-name: confetti-fall;
    animation-timing-function: cubic-bezier(0.35, 0, 0.65, 1);
    animation-fill-mode: forwards;
  }

  @keyframes confetti-fall {
    0%   { transform: translate(0, 0) rotate(var(--rotate-start)); opacity: 1; }
    100% { transform: translate(var(--drift), 105vh) rotate(calc(var(--rotate-start) + 480deg)); opacity: 0.25; }
  }

  /* Contained variant of the confetti burst above, scoped to sit inside
     a modal card (e.g. the "enable location" prompt) instead of the
     whole viewport - see launchModalConfetti(). Positioned absolutely
     within .app-modal (now position: relative) and clipped by that
     modal's own overflow: hidden, so pieces never spill outside its
     rounded corners. Falls a fixed pixel distance set inline per burst
     (--fall-distance, computed from the modal's actual height) rather
     than the viewport-relative 105vh the full-screen version uses. */
  .modal-confetti-container {
    position: absolute;
    inset: 0;
    pointer-events: none;
    overflow: hidden;
    z-index: 5;
  }

  .modal-confetti-container .confetti-piece {
    animation-name: confetti-fall-modal;
  }

  @keyframes confetti-fall-modal {
    0%   { transform: translate(0, 0) rotate(var(--rotate-start)); opacity: 1; }
    100% { transform: translate(var(--drift), var(--fall-distance)) rotate(calc(var(--rotate-start) + 480deg)); opacity: 0.25; }
  }

  /* Respect reduced-motion preferences: keep the success state readable,
     just skip the bounce/spin/fall. */
  @media (prefers-reduced-motion: reduce) {
    .success-emoji { animation: none; }
    .confetti-piece { display: none; }
    #welcome-banner { animation: none; }
    #loading-prayer-illustration { animation: none; }
    #prayer-glow { animation: none; }
  }

  /* ============================================================
     WELCOME BANNER (first-visit callout)
     Lives inside #sidebar now, directly above #sidebar-header's toolbar
     - not as a full-width strip above both the sidebar and the map/
     featured column - so it's left-aligned to the sidebar's own width
     instead of centered across the whole page, and only the sidebar's
     content shifts down for it (see #main's comment above). #sidebar is
     a flex column with this as a fixed, non-scrolling row above
     #place-list (see #sidebar's own comment) - previously this used
     position:sticky to stay in view while the place list scrolled
     underneath it, which worked but made #sidebar's own scrollbar span
     the banner/header too; flex-shrink:0 here gets the same "always
     visible" result without that.
     ============================================================ */
  #welcome-banner {
    flex-shrink: 0;
    overflow: hidden;
    background: linear-gradient(90deg, var(--welcome-bg-start) 0%, var(--welcome-bg-end) 100%);
    border-bottom: 1px solid var(--brand-tint-strong);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-start;
    gap: 10px 18px;
    padding: 10px 16px;
    text-align: left;
    transform-origin: top;
    animation: welcome-banner-open 0.55s cubic-bezier(0.34, 1.56, 0.64, 1) both;
  }

  @keyframes welcome-banner-open {
    from { transform: scaleY(0); opacity: 0; }
    to { transform: scaleY(1); opacity: 1; }
  }

  /* Subtle scattered star field behind the message - a nod to Islamic
     geometric art, kept faint enough not to fight the text. A single
     wide, hand-placed layout of differently-sized/rotated stars
     (viewBox 0 0 1400 60, preserveAspectRatio="none") rather than a
     tiled <pattern> - it stretches to fill whatever width the banner
     actually is, and since the stars are scattered (not a rigid grid),
     it doesn't matter that some get clipped at the edges. */
  #welcome-banner-bg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    z-index: 0;
    pointer-events: none;
  }

  #welcome-banner-content,
  #welcome-banner-btn {
    position: relative;
    z-index: 1;
  }

  #welcome-banner-content {
    display: flex;
    /* flex-start (not center) - once the icon stops wrapping onto its own
       line below (see flex-wrap: nowrap below), centering it against text
       that wraps onto two lines pulled it noticeably below the first
       line's baseline instead of sitting level with it. */
    align-items: flex-start;
    justify-content: flex-start;
    gap: 10px;
    /* nowrap here (the text span itself still wraps its own words just
       fine) - wrap previously let the icon and text drop onto separate
       lines whenever they didn't both fit on one, which read as the
       wave emoji floating alone above the message instead of beside it. */
    flex-wrap: nowrap;
    min-width: 0;
  }

  #welcome-banner-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    width: 28px;
    height: 28px;
    border-radius: 50%;
    background: rgba(var(--brand-rgb), 0.16);
    font-size: 15px;
    line-height: 1;
  }

  #welcome-banner-text {
    font-size: 13px;
    line-height: 1.4;
    color: var(--welcome-ink);
  }

  #welcome-banner-text strong {
    font-weight: 800;
  }

  /* Same solid, filled-pill treatment as #reset-bar ("Reset map view")
     instead of a transparent outline - the outline version was too close
     in color to the banner's own light green gradient background to read
     clearly at a glance. */
  #welcome-banner-btn {
    flex-shrink: 0;
    display: inline-block;
    background: var(--brand-gradient);
    color: var(--brand-ink);
    border: none;
    padding: 7px 18px;
    border-radius: 20px;
    font-size: 12px;
    font-weight: 700;
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;
    box-shadow: 0 2px 8px rgba(var(--ink-rgb), 0.18);
    transition: box-shadow 0.15s ease, transform 0.15s ease;
  }

  #welcome-banner-btn:hover {
    box-shadow: 0 4px 12px rgba(var(--ink-rgb), 0.24);
    transform: translateY(-1px);
  }

  #welcome-banner-btn:active { transform: translateY(0); }

  /* On mobile, keep the whole banner to a single row - the message
     shrinks and truncates with an ellipsis instead of wrapping to a
     second line, so it doesn't eat up extra space at the top of the
     (already narrow, overlay-style) mobile sidebar. */
  @media (max-width: 768px) {
    #welcome-banner {
      flex-wrap: nowrap;
      justify-content: flex-start;
      gap: 8px;
      padding: 8px 12px;
    }

    #welcome-banner-content {
      flex: 1 1 auto;
      flex-wrap: nowrap;
      justify-content: flex-start;
      min-width: 0;
      gap: 8px;
    }

    #welcome-banner-icon {
      width: 22px;
      height: 22px;
      font-size: 12px;
    }

    #welcome-banner-text {
      flex: 1 1 auto;
      min-width: 0;
      font-size: 11.5px;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    #welcome-banner-btn {
      font-size: 11px;
      padding: 6px 12px;
    }
  }

  /* ============================================================
     ARTICLES
     #articles-view is always rendered server-side (see the PHP "ARTICLES
     VIEW" block after #topbar) but only visible when the body carries
     "articles-mode" - #main (map + sidebar) gets the opposite treatment,
     so the two are mutually exclusive without either one needing to
     change how it initializes.
     ============================================================ */
  #articles-view { display: none; }
  body.articles-mode #articles-view { display: block; }
  body.articles-mode #main { display: none; }
  /* html/body's own overflow:hidden (see "GLOBAL RESET" above) is
     load-bearing for the map app - it's a fixed one-viewport layout that
     manages its own internal scroll regions. The articles pages are
     normal long-form content instead, so body needs to actually scroll
     here; html stays overflow:hidden and still clips to the viewport,
     body becomes the scrolling element within that same boundary. */
  body.articles-mode {
    overflow-y: auto;
    overflow-x: hidden;
  }
  /* Best/Top rated/Closest only ever sort the map's place list - there's
     no map on the articles pages for them to act on, so they'd just be
     dead-looking buttons rather than a real sort control. Same reasoning
     for the search box: it searches places.json, which isn't even fetched
     on these pages (see IS_ARTICLES_VIEW in the main script). */
  body.articles-mode #filter-group,
  body.articles-mode #search-wrap { display: none; }

  /* With the search box gone, #topbar's third column has nothing to hold
     it out to the edge - #topbar-right auto-places into the wide "1fr"
     middle column and aligns to its START, leaving the prayer widget and
     dark-mode toggle stranded mid-bar. Collapse to two columns and pin
     the right-hand group to the end so it hugs the right edge instead. */
  body.articles-mode #topbar { grid-template-columns: auto 1fr; }
  body.articles-mode #topbar-right { justify-self: end; }

  /* Below 768px the map page swaps the full #prayer-widget for the compact
     #prayer-widget-m copy sitting beside the logo, because that topbar is
     genuinely cramped there (logo + search + sort pills). Neither the
     search box nor the pills exist on these pages, so there's room to keep
     the real widget where it belongs - on the right, next to the dark-mode
     toggle - at every width, instead of it jumping to the far left. */
  body.articles-mode #prayer-widget { display: flex; }
  body.articles-mode #prayer-widget-m { display: none; }

  .articles-wrap {
    max-width: 860px;
    margin: 0 auto;
    padding: 40px 20px 80px;
  }

  /* ============================================================
     ARTICLES HERO (list view only)
     Full-bleed banner with a muted, looping YouTube background behind
     the heading. The gradient below is the real background - the video
     layers on top of it - so the hero looks finished even when the embed
     never loads (no JS, reduced motion, blocked third-party frames, or
     simply before the iframe has buffered).
     ============================================================ */
  #articles-hero {
    position: relative;
    overflow: hidden;
    min-height: 300px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    background: linear-gradient(135deg, var(--brand-ink) 0%, #0b5c31 55%, var(--brand-dark) 100%);
  }

  /* Sized to COVER rather than fit. The exact box is computed in JS
     (sizeHeroVideo) from the hero's own width/height, because the usual
     pure-CSS trick mixes units - min-width in vh, min-height in % - and
     those only agree when the hero happens to be exactly viewport-tall.
     On a phone (short hero, tall viewport) they disagreed badly enough
     that the iframe stopped being 16:9, so YouTube letterboxed the video
     inside it and the hero's gradient showed through underneath as a
     green band. These are just the pre-JS defaults. */
  #articles-hero-video {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 100%;
    height: 100%;
    transform: translate(-50%, -50%);
    pointer-events: none;
    /* Faded in once playback has actually started - see revealHeroVideo().
       Without this the player's poster frame and its big play button are
       visible for a beat before autoplay kicks in, which flashes on load. */
    opacity: 0;
    transition: opacity .6s ease;
  }

  #articles-hero-video.is-playing { opacity: 1; }

  #articles-hero-video iframe {
    width: 100%;
    height: 100%;
    border: 0;
    display: block;
    pointer-events: none;
  }

  /* Keeps the heading readable over whatever frame the video is on, and
     ties the footage back to the site's green. */
  #articles-hero-scrim {
    position: absolute;
    inset: 0;
    background:
      linear-gradient(180deg, rgba(3,26,14,.42) 0%, rgba(3,26,14,.62) 100%),
      linear-gradient(135deg, rgba(5,52,28,.30), rgba(33,145,80,.16));
  }

  #articles-hero-content {
    position: relative;
    padding: 64px 20px;
    max-width: 720px;
  }

  #articles-hero-content h1 {
    font-size: 40px;
    font-weight: 800;
    color: #fff;
    margin-bottom: 10px;
    text-shadow: 0 2px 12px rgba(0,0,0,.35);
  }

  #articles-hero-content .articles-intro {
    color: rgba(255,255,255,.92);
    font-size: 16px;
    margin-bottom: 0;
    text-shadow: 0 1px 8px rgba(0,0,0,.35);
  }

  /* No autoplaying motion for anyone who has asked not to have it - the
     script also skips injecting the iframe entirely in that case, so this
     is just belt-and-braces if it somehow got in. */
  @media (prefers-reduced-motion: reduce) {
    #articles-hero-video { display: none; }
  }

  .articles-wrap > h1 {
    font-size: 30px;
    font-weight: 800;
    color: var(--ink);
    margin-bottom: 8px;
  }

  .articles-intro, .articles-empty {
    color: var(--muted);
    font-size: 15px;
    margin-bottom: 28px;
  }

  .articles-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: 24px;
  }

  .article-card {
    display: flex;
    flex-direction: column;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 16px;
    overflow: hidden;
    text-decoration: none;
    color: inherit;
    box-shadow: 0 2px 8px rgba(var(--ink-rgb), 0.05);
    transition: transform 0.18s ease, box-shadow 0.18s ease;
  }

  .article-card:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 28px rgba(var(--ink-rgb), 0.12);
  }

  .article-card-img {
    width: 100%;
    height: 160px;
    object-fit: cover;
    background: var(--bg-hover);
    display: block;
  }

  .article-card-body { padding: 16px 18px 20px; }

  .article-card-date {
    display: block;
    font-size: 12px;
    font-weight: 700;
    color: var(--meta);
    text-transform: uppercase;
    letter-spacing: 0.4px;
    margin-bottom: 6px;
  }

  /* h2 in the list view, h3 in the "Keep reading" block (where the section
     itself is the h2) - same card, same look, correct heading order. */
  .article-card-body h2,
  .article-card-body h3 {
    font-size: 17px;
    font-weight: 700;
    color: var(--ink);
    line-height: 1.3;
    margin-bottom: 6px;
  }

  .article-card-body p {
    font-size: 13.5px;
    color: var(--muted);
    line-height: 1.5;
  }

  /* ---- article detail ---- */
  .articles-back {
    display: inline-block;
    font-size: 13.5px;
    font-weight: 700;
    color: var(--link-ink);
    text-decoration: none;
    margin-bottom: 18px;
  }
  .articles-back:hover { text-decoration: underline; }

  .article-hero {
    width: 100%;
    max-height: 420px;
    object-fit: cover;
    border-radius: 16px;
    margin-bottom: 24px;
    background: var(--bg-hover);
  }

  .article-detail h1 {
    font-size: 32px;
    font-weight: 800;
    color: var(--ink);
    line-height: 1.2;
    margin-bottom: 10px;
  }

  .article-meta {
    font-size: 13.5px;
    color: var(--meta);
    margin-bottom: 20px;
  }
  .article-meta-sep { margin: 0 2px; }

  .article-share {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
    padding: 14px 0;
    border-top: 1px solid var(--divider);
    border-bottom: 1px solid var(--divider);
    margin-bottom: 28px;
  }

  .article-share-label {
    font-size: 13px;
    font-weight: 700;
    color: var(--muted);
    margin-right: 2px;
  }

  .article-share-btn {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    color: var(--ink);
    background: var(--bg-pill);
    border: none;
    border-radius: 16px;
    padding: 7px 13px;
    text-decoration: none;
    cursor: pointer;
    transition: background 0.18s ease;
  }
  .article-share-btn:hover { background: var(--hover-tint-gray); }

  /* Brand logos (Facebook/X/WhatsApp/YouTube) and the copy-link glyph,
     inlined as SVG - see the $icon* variables in the articles markup for
     why these aren't emoji. Sized in em so they track the button's font
     size, and filled with currentColor so they pick up hover/active
     colors for free. */
  .brand-icon {
    width: 1.05em;
    height: 1.05em;
    flex: none;
    display: block;
  }

  .article-share-btn.copied {
    background: var(--brand-tint);
    color: var(--brand-tint-ink);
  }

  .article-body {
    font-size: 16px;
    line-height: 1.75;
    color: var(--ink);
  }

  .article-body h2 {
    font-size: 22px;
    font-weight: 800;
    margin: 34px 0 12px;
    color: var(--ink);
  }

  .article-body h3 {
    font-size: 18px;
    font-weight: 700;
    margin: 26px 0 10px;
    color: var(--ink);
  }

  .article-body p { margin-bottom: 16px; }

  /* Social embeds sit as their own block in the prose, centered and
     capped so a 540px-wide Instagram card doesn't dictate the article's
     measure. Both SDKs rewrite these nodes in place once loaded (see the
     ARTICLE SOCIAL EMBEDS loader), so the min-height keeps the paragraph
     flow from jumping when that swap happens. */
  /* embed.js REPLACES the <blockquote> with an <iframe class="instagram-media">,
     so both have to be styled - targeting only the blockquote left the
     rendered iframe unstyled and collapsed to its natural 2px height until
     (or unless) Instagram's resize message arrived. */
  .article-body blockquote.instagram-media,
  .article-body iframe.instagram-media,
  .article-body .fb-page,
  .article-body .fb-post {
    margin: 26px auto !important;
    max-width: 540px;
  }

  .article-body blockquote.instagram-media,
  .article-body iframe.instagram-media {
    min-height: 600px;
    border-radius: 12px;
    display: block;
  }

  .article-body .fb-page,
  .article-body .fb-post {
    display: flex;
    justify-content: center;
    min-height: 130px;
  }

  .article-body ul, .article-body ol {
    margin: 0 0 16px;
    padding-left: 22px;
  }
  .article-body li { margin-bottom: 8px; }

  .article-body a {
    color: var(--link-ink);
    font-weight: 600;
    text-decoration: underline;
    text-decoration-color: rgba(var(--brand-rgb), 0.4);
  }
  .article-body a:hover { text-decoration-color: var(--link-ink); }

  .article-follow {
    margin-top: 40px;
    padding-top: 24px;
    border-top: 1px solid var(--divider);
    font-size: 14px;
    color: var(--muted);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
  }

  .article-follow-label { font-weight: 700; color: var(--ink); }

  .article-follow a {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    color: var(--link-ink);
    font-weight: 700;
    text-decoration: none;
    background: var(--brand-tint);
    padding: 7px 15px;
    border-radius: 15px;
  }
  .article-follow a:hover { background: var(--brand-tint-strong); }

  /* "Keep reading" - reuses .articles-grid/.article-card from the list
     view so a related card and a list card are literally the same
     component, just under a different heading. */
  .article-related {
    margin-top: 44px;
    padding-top: 28px;
    border-top: 1px solid var(--divider);
  }

  .article-related h2 {
    font-size: 22px;
    font-weight: 800;
    color: var(--ink);
    margin-bottom: 18px;
  }

  .article-related-all {
    display: inline-block;
    margin-top: 22px;
    font-size: 14px;
    font-weight: 700;
    color: var(--link-ink);
    text-decoration: none;
  }
  .article-related-all:hover { text-decoration: underline; }

  /* Marks the actual end of the page - the articles pages otherwise end on
     whatever the last card/paragraph happens to be, with no visual cue
     that there isn't more below (unlike #main's map, which fills the
     whole viewport and has no "end" to signal in the first place). */
  #articles-footer {
    text-align: center;
    font-size: 13px;
    color: var(--faint);
    padding: 28px 20px 40px;
  }

  @media (max-width: 640px) {
    .articles-wrap { padding: 24px 16px 60px; }
    .articles-wrap > h1 { font-size: 24px; }
    .article-detail h1 { font-size: 24px; }
    .article-hero { max-height: 240px; }
    .article-body { font-size: 15px; }
  }
