﻿/* ── Reset & base ── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  /* A single element overflowing its container (a long username, an
     unshrinkable flex item) must not turn the whole page into a
     horizontally-swipeable surface — iOS in particular treats any
     scrollable width as fair game for its edge-swipe/back gesture and
     rubber-banding, which reads as "the page slides left and right".

     This is set on html alone, not body: setting it on both forces both
     elements' overflow-y to the used value auto (the CSS overflow rule for
     a non-visible x with a visible y), which makes body itself a second
     scroll container nested inside html's. A position: sticky element
     inside body then sticks to that inner, never-scrolling container
     instead of the page — which is what pinned the navbar in place on
     paper while it still scrolled away on an actual phone. html's hidden
     x already clips anything body could overflow with, so body needs none
     of its own. */
  overflow-x: hidden;
}
body {
  /* The flags-only font main.jsx defines on Windows, which has no flag
     emoji of its own; undefined everywhere else, so skipped. */
  font-family: "Twemoji Country Flags", "Segoe UI", system-ui, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100dvh;
  /* Without this the light-on-dark text renders noticeably heavier than the
     same size does on white, which is most of why dense panels read as
     cluttered. */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  /* Keep the last row of a page above the home indicator. The navbar handles
     its own top inset, so only the bottom is paid here. See --safe-* below. */
  padding-bottom: env(safe-area-inset-bottom, 0px);
}

a { color: inherit; text-decoration: none; }

/* ── Safe areas ───────────────────────────────────────────────────────────
   In the phone app the web view runs edge to edge (viewport-fit=cover in the
   page head), so whatever sits at the very top of the page ends up underneath
   the status bar — the clock and Dynamic Island land on the hamburger button.
   Anything pinned to a screen edge pays the inset below. env() reports 0 on
   hardware without a notch or home indicator, and in a desktop browser, so
   every one of these is a no-op there rather than an iPhone-only special
   case. --safe-top is also subtracted from the "100vh minus the navbar"
   layouts, since the taller navbar eats into them. */
:root {
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-right: env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);

  /* How tall the navbar actually is, written here by the Navbar component
     from a ResizeObserver — the status-bar inset it carries included, so a
     layout that subtracts this must not subtract --safe-top as well. The
     number below is only the value before the first measurement lands (and
     on any page that renders no navbar): one row of links at desktop
     spacing. It is a poor guess for an admin, whose nav is everyone else's
     six links plus four console ones and wraps to a second row well before a
     laptop screen runs out — which is exactly why the panes that fill "the
     window minus the navbar" ask for the measurement instead. */
  --nav-h: 57px;

  /* Text size switch (see font-size.js / FontSizeSwitcher.jsx). Almost every
     font-size in this sheet is in rem, so scaling the root font-size scales
     the app's text along with it — [data-font-size] is what the switch
     actually writes to <html>, and this is the one place that reads it. */
  --font-scale: 1.2;
}
:root[data-font-size="small"] { --font-scale: 1; }
:root[data-font-size="large"] { --font-scale: 1.5; }
html { font-size: calc(16px * var(--font-scale)); }

/* ── Design tokens ────────────────────────────────────────────────────────
   The palette these name was already being used across both stylesheets and
   ~450 inline style objects in the React tree — it just had no name, so each
   new component re-picked its greys by eye and drifted. Everything below is
   the existing slate/violet/sky scheme written down once. Both the Jinja
   pages and the SPA load this file, so it is the one place the two can
   share a definition.

   Three accents coexist on purpose and mean different things:
     --brand   violet — identity: logo, map pins, the app's own colour
     --accent  sky    — "you can act on this": primary buttons, links
     --select  blue   — "this is the one you picked": current card/row/pin
   Keeping them distinct is what lets a selected card read differently from
   a hovered one without either shouting. */
:root {
  /* Surfaces — page ground up to raised cards */
  --bg:             #0f172a;
  --surface:        #1e293b;
  --surface-hi:     #273549;  /* hover state of a raised surface */
  --surface-lift:   #33455f;  /* hover of something already at --surface-hi */
  --surface-sunken: #0b1220;  /* inputs, wells, photo letterboxing */

  /* Lines */
  --border:         #334155;
  --border-strong:  #475569;
  --border-subtle:  #1e293b;  /* divider that should barely register */

  /* Text — five steps, so "secondary" always means the same grey */
  --text:           #e2e8f0;
  --text-strong:    #f1f5f9;
  --text-dim:       #94a3b8;
  --text-muted:     #64748b;
  --text-faint:     #475569;
  /* Prose the user wrote — an event's description and note. Still grey, but a
     step nearer --text than --text-dim, so a paragraph of it reads clearly
     against a card rather than receding like a date or a label. */
  --text-prose:     #cbd5e1;

  /* Brand (violet) */
  --brand:          #7c3aed;
  --brand-mid:      #8b5cf6;  /* violet that still separates from sky under
                                 red-green colour blindness — see chart series */
  --brand-soft:     #a78bfa;
  --brand-wash:     rgba(124,58,237,.12);

  /* Action (sky) */
  --accent:         #0ea5e9;
  --accent-hi:      #38bdf8;
  --accent-deep:    #0284c7;
  --accent-wash:    rgba(14,165,233,.15);

  /* Selection (blue) */
  --select:         #3b82f6;
  --select-hi:      #93c5fd;
  --select-wash:    rgba(59,130,246,.12);

  /* Semantic */
  --ok:             #22c55e;
  --ok-deep:        #16a34a;
  --warn:           #f59e0b;
  --danger:         #dc2626;
  --danger-soft:    #f87171;

  /* Chart series — the admin login plot, and its legend and stat swatches,
     which have to name the same three things in the same three colours.
     The total is the emphasis, so it wears ink rather than a hue; the two
     hues then mean "which kind of login" and nothing else. Sky and violet
     were checked as a pair against deuteranopia and protanopia rather than
     by eye: the softer violet this used to use came apart from the sky
     accent for full-colour vision and not at all without it, so the pair is
     stepped to the brighter sky and the mid violet, which hold apart under
     both. */
  --chart-total:    var(--text);
  --chart-member:   var(--accent-hi);
  --chart-guest:    var(--brand-mid);
  /* Deletions, on the accounts-growth chart's negative dip below the zero
     line — danger red, since it is the one series here that means "gone"
     rather than "gained". */
  --chart-deleted:  var(--danger);

  /* Light surfaces — the calendar grid, map popups and pin badges are
     deliberately light inside the dark app, so they need their own names.
     Without these the calendar ends up written as `color: var(--surface)`,
     which is the right colour for entirely the wrong reason and inverts the
     moment anyone touches the dark palette. */
  --paper:          #ffffff;
  --paper-alt:      #e9eef4;  /* other-month cells, inset wells */
  --paper-line:     #e2e8f0;  /* grid rules on paper */
  --paper-tint:     #e0f2fe;  /* today / selected day wash */
  --paper-star:     #fff7e0;  /* a day marked important */
  --ink:            #1e293b;  /* body text on paper */
  --ink-dim:        #475569;

  /* Radii */
  --r-xs:  4px;
  --r-sm:  6px;
  --r-md:  8px;
  --r-lg:  12px;
  --r-pill: 999px;

  /* Elevation — tinted toward the page ground rather than pure black, so a
     raised surface reads as lit instead of as a hole cut in the page. */
  --sh-sm: 0 1px 2px rgba(2,6,23,.4);
  --sh-md: 0 4px 14px rgba(2,6,23,.45);
  --sh-lg: 0 16px 40px rgba(2,6,23,.6);

  /* Focus rings — one width everywhere, colour swapped per context */
  --ring:        0 0 0 3px var(--accent-wash);
  --ring-brand:  0 0 0 3px var(--brand-wash);
  --ring-danger: 0 0 0 3px rgba(220,38,38,.22);

  /* The photo-viewing cursor — a magnifier in the app's own sky accent,
     glass and a "+" on a dark handle, standing in for the browser's own
     zoom-in glyph (a plain grey glass drawn differently on every platform).
     Hotspot sits on the lens centre, where the "+" is. Falls back to the
     native zoom-in glyph on anything that can't load a custom cursor image. */
  --cursor-zoom-in: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZyIgeDE9IjQiIHkxPSI0IiB4Mj0iMjIiIHkyPSIyMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM3ZGQzZmMiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDI4NGM3Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGZpbHRlciBpZD0icyIgeD0iLTYwJSIgeT0iLTYwJSIgd2lkdGg9IjIyMCUiIGhlaWdodD0iMjIwJSI+CiAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjAiIGR5PSIxLjUiIHN0ZERldmlhdGlvbj0iMS4zIiBmbG9vZC1jb2xvcj0iIzBjMWIyZSIgZmxvb2Qtb3BhY2l0eT0iMC41NSIvPgogICAgPC9maWx0ZXI+CiAgPC9kZWZzPgogIDxnIGZpbHRlcj0idXJsKCNzKSI+CiAgICA8bGluZSB4MT0iMTkuNCIgeTE9IjE5LjQiIHgyPSIyNy4yIiB5Mj0iMjcuMiIgc3Ryb2tlPSIjMGMxYjJlIiBzdHJva2Utd2lkdGg9IjQuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICA8bGluZSB4MT0iMTkuNiIgeTE9IjE5LjYiIHgyPSIyNi42IiB5Mj0iMjYuNiIgc3Ryb2tlPSIjN2RkM2ZjIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICA8Y2lyY2xlIGN4PSIxMyIgY3k9IjEzIiByPSI5IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuOTUpIiBzdHJva2U9InVybCgjZykiIHN0cm9rZS13aWR0aD0iMi42Ii8+CiAgICA8Y2lyY2xlIGN4PSIxMyIgY3k9IjEzIiByPSI5IiBmaWxsPSJub25lIiBzdHJva2U9IiMwYzFiMmUiIHN0cm9rZS13aWR0aD0iMC44IiBvcGFjaXR5PSIwLjM1Ii8+CiAgICA8cGF0aCBkPSJNNy41IDEwYTYuNSA2LjUgMCAwIDEgNi01IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuODUiLz4KICAgIDxsaW5lIHgxPSIxMyIgeTE9IjguMyIgeDI9IjEzIiB5Mj0iMTcuNyIgc3Ryb2tlPSIjMDI4NGM3IiBzdHJva2Utd2lkdGg9IjEuNyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICA8bGluZSB4MT0iOC4zIiB5MT0iMTMiIHgyPSIxNy43IiB5Mj0iMTMiIHN0cm9rZT0iIzAyODRjNyIgc3Ryb2tlLXdpZHRoPSIxLjciIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgogIDwvZz4KPC9zdmc+Cg==") 13 13, zoom-in;

  /* Its zoom-out twin — the same glass, handle and drop shadow, but the "+"
     loses its vertical stroke and becomes a "−". Used wherever a click backs
     the view out (closing the full-screen photo review) instead of opening
     it, so leaving is as deliberately drawn as arriving instead of falling
     back to the browser's own flat grey glyph. */
  --cursor-zoom-out: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIj4KICA8ZGVmcz4KICAgIDxsaW5lYXJHcmFkaWVudCBpZD0iZyIgeDE9IjQiIHkxPSI0IiB4Mj0iMjIiIHkyPSIyMiIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgogICAgICA8c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM3ZGQzZmMiLz4KICAgICAgPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDI4NGM3Ii8+CiAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPGZpbHRlciBpZD0icyIgeD0iLTYwJSIgeT0iLTYwJSIgd2lkdGg9IjIyMCUiIGhlaWdodD0iMjIwJSI+CiAgICAgIDxmZURyb3BTaGFkb3cgZHg9IjAiIGR5PSIxLjUiIHN0ZERldmlhdGlvbj0iMS4zIiBmbG9vZC1jb2xvcj0iIzBjMWIyZSIgZmxvb2Qtb3BhY2l0eT0iMC41NSIvPgogICAgPC9maWx0ZXI+CiAgPC9kZWZzPgogIDxnIGZpbHRlcj0idXJsKCNzKSI+CiAgICA8bGluZSB4MT0iMTkuNCIgeTE9IjE5LjQiIHgyPSIyNy4yIiB5Mj0iMjcuMiIgc3Ryb2tlPSIjMGMxYjJlIiBzdHJva2Utd2lkdGg9IjQuNCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICA8bGluZSB4MT0iMTkuNiIgeTE9IjE5LjYiIHgyPSIyNi42IiB5Mj0iMjYuNiIgc3Ryb2tlPSIjN2RkM2ZjIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgICA8Y2lyY2xlIGN4PSIxMyIgY3k9IjEzIiByPSI5IiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuOTUpIiBzdHJva2U9InVybCgjZykiIHN0cm9rZS13aWR0aD0iMi42Ii8+CiAgICA8Y2lyY2xlIGN4PSIxMyIgY3k9IjEzIiByPSI5IiBmaWxsPSJub25lIiBzdHJva2U9IiMwYzFiMmUiIHN0cm9rZS13aWR0aD0iMC44IiBvcGFjaXR5PSIwLjM1Ii8+CiAgICA8cGF0aCBkPSJNNy41IDEwYTYuNSA2LjUgMCAwIDEgNi01IiBmaWxsPSJub25lIiBzdHJva2U9IiNmZmZmZmYiIHN0cm9rZS13aWR0aD0iMS42IiBzdHJva2UtbGluZWNhcD0icm91bmQiIG9wYWNpdHk9IjAuODUiLz4KICAgIDxsaW5lIHgxPSI4LjMiIHkxPSIxMyIgeDI9IjE3LjciIHkyPSIxMyIgc3Ryb2tlPSIjMDI4NGM3IiBzdHJva2Utd2lkdGg9IjEuNyIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIi8+CiAgPC9nPgo8L3N2Zz4K") 13 13, zoom-out;

  /* Type scale — replaces the .7/.72/.75/.78/.8/.82rem spread that all
     meant "small text" */
  --t-xs:  .7rem;
  --t-sm:  .78rem;
  --t-md:  .85rem;
  --t-lg:  .95rem;
  --t-xl:  1.15rem;
  --t-2xl: 1.4rem;

  /* Motion */
  --ease: cubic-bezier(.4, 0, .2, 1);
  --fast: .12s;
  --base: .18s;

  /* Which native widget skin (date/time inputs, <select> dropdowns) matches
     this palette — read by the couple of `color-scheme` declarations that
     used to say "dark" outright. Swapped alongside everything else below. */
  --native-scheme: dark;
}

/* ── Light theme ──────────────────────────────────────────────────────────
   Toggled by adding data-theme="light" to <html> (see static/js/theme.js,
   loaded before this sheet so the attribute is already set on first paint).
   Only the tokens a page's chrome and surfaces are built from are swapped —
   brand/accent/select/semantic stay the same hex in both themes, since a
   button or a link is not supposed to change colour when the page around it
   does. --paper-* also stays put: those already name a surface that is
   deliberately light *inside* the dark app (the calendar grid, map popups),
   so under the light theme they simply stop being the odd one out.

   One thing outside this block stays pinned to the dark values on purpose
   and does not follow the toggle: the landing page (see body.landing-body,
   under "Landing page"), which is fixed dark chrome by design and not part
   of the app surface being switched. The navbar (below) follows the toggle
   like the rest of the app. */
:root[data-theme="light"] {
  --bg:             #f8fafc;
  --surface:        #ffffff;
  --surface-hi:     #f1f5f9;
  --surface-lift:   #e2e8f0;
  --surface-sunken: #eef2f7;

  --border:         #e2e8f0;
  --border-strong:  #cbd5e1;
  --border-subtle:  #f1f5f9;

  --text:           #1e293b;
  --text-strong:    #0f172a;
  --text-dim:       #475569;
  --text-muted:     #64748b;
  --text-faint:     #94a3b8;
  --text-prose:     #334155;

  /* Elevation reads as a hole cut in the page at dark-theme strength on a
     white ground, so it is toned down rather than reused verbatim. */
  --sh-sm: 0 1px 2px rgba(15,23,42,.08);
  --sh-md: 0 4px 14px rgba(15,23,42,.10);
  --sh-lg: 0 16px 40px rgba(15,23,42,.16);

  --native-scheme: light;
}

/* Respect the OS "reduce motion" setting — several surfaces below animate
   transform on hover, which is exactly what that setting is asking us not
   to do. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── Navbar ── */
.navbar {
  background: var(--surface);
  color: var(--text);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem 2rem;
  padding-top: calc(0.75rem + var(--safe-top));
  padding-left: calc(2rem + var(--safe-left));
  padding-right: calc(2rem + var(--safe-right));
  box-shadow: var(--sh-sm);
  border-bottom: 1px solid var(--border);
  /* Map/Calendar/My-Events pages fill exactly one viewport with no document
     scroll, so their navbar reads as pinned in place for free. Friends,
     Account and three of the four admin pages are plain scrolling
     containers (see Shell vs. MapShell in App.jsx) — without sticky, the
     navbar scrolled off the top with the rest of the page on those, on
     desktop just as on the phone widths below. */
  position: sticky;
  top: 0;
  z-index: 500;
}
.navbar .brand {
  font-size: 1.25rem;
  font-weight: 700;
  display: flex;
  align-items: center;
}
.navbar .brand .brand-text {
  background: linear-gradient(135deg, #38d7f5 0%, #2f9de8 50%, #3b6ef5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.navbar .nav-links { display: flex; gap: .25rem; align-items: center; }
/* Padding + a wash on hover gives each link its own hit target instead of
   only the glyphs changing colour under the pointer. Colours ride the same
   tokens as the rest of the app, so the navbar repaints with everything
   else when the theme toggle flips data-theme. */
.navbar .nav-links a:not(.btn-primary) {
  display: inline-flex; align-items: center; gap: .4rem;
  color: var(--text-dim);
  padding: .3rem .6rem; border-radius: var(--r-sm);
  transition: color var(--base) var(--ease), background var(--base) var(--ease);
}
/* Icons vary in natural width (globe vs. calendar vs. flag), which without a
   fixed box would nudge each label's baseline start by a few px and read as
   the row being unevenly spaced even though every gap is the same. */
.navbar .nav-links a > i,
.navbar .nav-username > i,
.navbar .btn-nav-link > i {
  width: 1em; text-align: center; flex: none;
}
.navbar .nav-links a:not(.btn-primary):hover { color: var(--text); background: var(--surface-hi); }
.navbar .nav-links a:not(.btn-primary):focus-visible {
  outline: none; color: var(--text);
  box-shadow: 0 0 0 2px var(--accent);
}
/* Sign Up rides the ordinary .btn-primary look (white text and icon on the
   accent gradient) — without the :not(.btn-primary) exclusions above, the
   generic link rules outranked it on specificity and painted the label
   var(--text-dim), a dim slate grey barely readable over the blue fill. */
/* The same size as every other link in the row — see .btn-nav-link below. */
.navbar .nav-username {
  color: var(--text-dim); font-size: inherit;
  display: flex; align-items: center; gap: .35rem;
  padding: .3rem .6rem; border-radius: var(--r-sm);
  transition: color var(--base) var(--ease), background var(--base) var(--ease);
}
.navbar .nav-username:hover { color: var(--text); background: var(--surface-hi); }
/* The account name, riding along behind the word "Account" in that row. The
   label carries the meaning — the row leads to account settings, deletion
   included — and this only answers "signed in as who", so it is quieter and
   it truncates: a guest name is `guest_` plus ten hex characters and would
   otherwise be the widest thing in the nav. */
.navbar .nav-username__name {
  color: var(--text-muted);
  font-size: .85em;
  max-width: 12ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* .btn-nav-link is defined generically further down (colour: var(--text-dim),
   padding: 0) for reuse outside the navbar; restated here so the Chat button
   gets the same padding + border-radius + hover wash as the `a` links beside
   it (Friends, Account) — otherwise its unpadded box reads as a smaller
   lateral gap around "Chat" than every other pair in the row, even though
   the flex gap between items is identical. Its .9rem font-size is dropped
   here too, along with the browser's own font for a <button>: Friends, Chat
   and Account read smaller, and in a different face, than Itinerary, Photo
   Map, My Events, Plan and Calendar beside them. */
.navbar .btn-nav-link {
  color: var(--text-dim);
  padding: .3rem .6rem; border-radius: var(--r-sm);
  font-family: inherit; font-size: inherit; line-height: inherit;
}
.navbar .btn-nav-link:hover { color: var(--text); background: var(--surface-hi); }

/* Admin-only rows in the nav — Places, Accounts, Reports, Config — carry the
   brand violet instead of the usual grey. Nobody but an admin is ever served
   these links, so the colour is not a permission hint to the person reading
   it; it is a reminder to the admin that the four pages behind them are the
   console and not the app everyone else sees. --brand-soft (a light violet)
   reads on the navbar's dark default; the light theme swaps it for the
   darker --brand instead, the same trade the rest of the app makes going
   from a dark to a paper-white surface. */
.navbar .nav-links a.nav-admin { color: var(--brand-soft); }
.navbar .nav-links a.nav-admin:hover,
.navbar .nav-links a.nav-admin:focus-visible {
  color: #c4b5fd;
  background: var(--brand-wash);
}
:root[data-theme="light"] .navbar .nav-links a.nav-admin { color: var(--brand); }
:root[data-theme="light"] .navbar .nav-links a.nav-admin:hover,
:root[data-theme="light"] .navbar .nav-links a.nav-admin:focus-visible {
  color: var(--brand-mid);
}

/* An admin's nav is everyone else's six links plus four console ones, and as
   one unwrapped row that is wider than a window much under 1050px — so the
   row ran off the right edge, and Config, sitting last of the four, was the
   link that went over it. It was not merely clipped: Calendar and the two map
   pages pin the document with overflow:hidden, so there was no scrolling it
   back into view, and the only way to reach Config was to open an admin page
   that does scroll — Reports — and find it there. The row now wraps instead,
   so every link, Config included, is on screen the moment a page loads. New
   rows are laid out from the right so they stay under the row above rather
   than drifting out towards the brand.

   Desktop only. Below 768px the nav is already the hamburger menu — a column
   whose height is animated open — and wrapping a column spills its rows into
   a second column beside the first. */
@media (min-width: 769px) {
  .navbar .nav-links { flex-wrap: wrap; justify-content: flex-end; row-gap: .3rem; }
}

/* Wrapping is the guarantee, not the goal: a second row costs the page ~30px
   of height, which the map pages measure themselves against. Between the
   hamburger and a window wide enough for the full row, the nav gives up the
   spacing it does not need — the outer padding, the gaps between links and a
   little of each link's own — which holds the admin row on one line down to
   about 1000px instead of losing it at 1050. */
@media (min-width: 769px) and (max-width: 1200px) {
  .navbar {
    padding-left: calc(1rem + var(--safe-left));
    padding-right: calc(1rem + var(--safe-right));
  }
  .navbar .nav-links { gap: .1rem; font-size: .9rem; }
  .navbar .nav-links a,
  .navbar .nav-username,
  .navbar .btn-nav-link { padding: .3rem .35rem; }
}

/* ── Buttons ──────────────────────────────────────────────────────────────
   All four variants share one geometry and one motion curve; only the fill
   changes. The 1px lift on hover and the press back down on :active are what
   make them feel like objects rather than coloured rectangles — the effect
   is deliberately small enough that it reads as responsiveness, not as
   animation. */
.btn-primary, .btn-secondary, .btn-danger, .btn-nav {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .45rem .9rem; border-radius: var(--r-sm);
  font-size: .9rem; font-weight: 600; border: none; cursor: pointer;
  line-height: 1.2;
  transition: background var(--base) var(--ease),
              box-shadow var(--base) var(--ease),
              transform var(--fast) var(--ease),
              opacity var(--base) var(--ease);
}
.btn-primary:hover, .btn-secondary:hover, .btn-danger:hover, .btn-nav:hover {
  transform: translateY(-1px);
}
.btn-primary:active, .btn-secondary:active, .btn-danger:active, .btn-nav:active {
  transform: translateY(0);
  transition-duration: 0s;
}
/* Keyboard focus gets a visible ring; mouse clicks do not. */
.btn-primary:focus-visible, .btn-secondary:focus-visible,
.btn-danger:focus-visible, .btn-nav:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
}
.btn-primary[disabled], .btn-secondary[disabled],
.btn-danger[disabled], .btn-nav[disabled] {
  opacity: .5; cursor: not-allowed; transform: none; box-shadow: none;
}

.btn-primary {
  background: linear-gradient(135deg, var(--accent-hi), var(--accent));
  color: #fff;
  box-shadow: var(--sh-sm);
}
.btn-primary:hover {
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 4px 14px rgba(14,165,233,.3);
}
.btn-secondary {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border);
}
.btn-secondary:hover {
  background: var(--surface-hi); border-color: var(--border-strong);
  box-shadow: var(--sh-sm);
}
/* One size down, same colours. For a button that is in the dialog because it
   has to be somewhere, not because it is what you came to do — a delete in a
   footer beside Save. Being smaller than the primary action is the point:
   the footer should read Save first and offer the destructive one second. */
.btn-primary.btn-compact, .btn-secondary.btn-compact,
.btn-danger.btn-compact, .btn-nav.btn-compact {
  padding: .3rem .65rem;
  font-size: .82rem;
}

.btn-danger { background: var(--danger); color: #fff; box-shadow: var(--sh-sm); }
.btn-danger:hover {
  background: #b91c1c;
  box-shadow: 0 4px 14px rgba(220,38,38,.3);
}
.btn-danger:focus-visible { box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--danger-soft); }
.btn-nav {
  background: var(--accent-wash); color: #7dd3fc;
  padding: .4rem .75rem; border: 1px solid rgba(14,165,233,.25);
}
.btn-nav:hover { background: rgba(14,165,233,.25); border-color: rgba(14,165,233,.45); }
/* Same trade as .btn-today above: #7dd3fc reads on the dark surface this
   button (Log Out, chiefly) wears, but washes out on the light theme's pale
   wash fill, so it steps down to the same deep sky accent there. */
:root[data-theme="light"] .btn-nav { color: var(--accent-deep); }
.btn-inline {
  padding: .2rem .55rem; font-size: .75rem;
  vertical-align: middle; margin: 0 .4rem;
}
.btn-nav-link {
  background: none; border: none; color: var(--text-dim); font-size: .9rem;
  cursor: pointer; padding: 0; font-weight: 400; transition: color .2s;
  display: inline-flex; align-items: center; gap: .4rem;
  text-decoration: none;
}
.btn-nav-link:hover { color: var(--text); }

/* ── Container ── */
.container { max-width: 1400px; margin: 2rem auto; padding: 0 1.5rem; }

/* ── Flash alerts ── */
/* A 3px colour bar on the leading edge makes the kind of alert readable at a
   glance without relying on the fill colour alone. */
.alert {
  padding: .75rem 1rem; border-radius: var(--r-sm);
  margin-bottom: 1rem; font-size: .9rem;
  border-left-width: 3px; border-left-style: solid;
  box-shadow: var(--sh-sm);
}
.alert-success { background: #14532d; color: #bbf7d0; border: 1px solid #166534; border-left: 3px solid var(--ok); }
.alert-error   { background: #450a0a; color: #fca5a5; border: 1px solid #991b1b; border-left: 3px solid var(--danger-soft); }
.alert-warning { background: #422006; color: #fde68a; border: 1px solid #854d0e; border-left: 3px solid var(--warn); }

/* ── Export/Import toolbar row ── */
.toolbar-row {
  display: flex; align-items: flex-start; gap: .4rem;
  flex-direction: row;
  margin-bottom: .75rem;
  width: 100%;
}

/* ── Import / Export bars ── */
.import-bar, .export-bar {
  display: flex; align-items: flex-start; gap: .3rem;
  flex: 1; flex-direction: column; flex-wrap: nowrap;
  padding: .3rem .5rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: .4rem;
  font-size: .82rem; color: var(--text-dim);
}
.import-bar .bar-label, .export-bar .bar-label {
  display: flex; align-items: center; gap: .35rem;
  font-weight: 600; color: var(--text); white-space: nowrap;
  cursor: pointer; user-select: none;
  width: 100%; flex-shrink: 0; justify-content: space-between;
}
/* Buttons inside each bar share equal width */
.import-bar .btn-export, .export-bar .btn-export {
  flex: 1; justify-content: center; text-align: center;
}
.bar-content {
  display: flex; flex-wrap: wrap;
  align-items: center; gap: .3rem; width: 100%;
}
.bar-fold-icon { display: inline-block; margin-left: auto; transition: transform .2s; }
.import-bar.bar-folded .bar-content,
.export-bar.bar-folded .bar-content { display: none; }
.import-bar.bar-folded .bar-fold-icon,
.export-bar.bar-folded .bar-fold-icon { transform: rotate(-90deg); }
.btn-export {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .28rem .75rem;
  background: var(--accent); color: #fff;
  border: none; border-radius: .3rem;
  font-size: .82rem; font-weight: 600; font-family: inherit; line-height: 1.4;
  cursor: pointer; transition: background .2s; white-space: nowrap;
}
.btn-export:hover { background: var(--brand-soft); }
.import-file-name { font-style: italic; color: var(--text-muted); }
.import-file-name:empty { display: none; }


/* Themed file input: hide the native control, use a regular button as the
   trigger (clicking the wrapping <label> still opens the file dialog) plus
   a separate text span for the chosen filename, since the native file
   input's "Choose File" button/text can't be recolored to fit the theme. */
.file-input-row { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; }
.file-input-btn { cursor: pointer; }
.file-input-btn input[type="file"] { display: none; }
.file-input-name {
  font-size: .85rem; color: var(--text-dim); font-style: italic;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px;
}

/* ── Calendar header ── */
.calendar-header {
  display: flex; align-items: center; justify-content: center;
  gap: 1.5rem; margin-bottom: .5rem;
}
.cal-picker-group { display: flex; align-items: center; gap: .5rem; }
.cal-select {
  font-size: 1.05rem; font-weight: 700; color: #7dd3fc;
  background: var(--surface);
  border: 1.5px solid var(--border);
  border-radius: .5rem;
  padding: .3rem .6rem;
  cursor: pointer;
  appearance: auto;
  transition: border-color .18s, background .18s;
}
/* Of everything wearing this class only some are really a <select> — the month
   and year on the calendar, the day span on a shared itinerary; the plan
   picker's button and the rename input borrow the look. For the real ones the
   browser still draws two parts we don't: the menulist arrow, which the OS
   paints black on our dark fill, and the list that opens, which comes back
   white. Naming the colour scheme hands both to the dark palette, and costs no
   width — the calendar's header is measured to the pixel (see "Fitting the
   toolbelt to the calendar"), so a rule that made room for a chevron of our
   own would have to be paid for out of that. The shared itinerary, which has
   the room, draws its own — see .sh-desc's neighbour in share.html. */
select.cal-select { color-scheme: var(--native-scheme); }

.cal-select:hover { background: var(--border); border-color: var(--accent); }
.cal-select:focus { outline: none; border-color: var(--accent); background: #0c1a2e; box-shadow: 0 0 0 3px rgba(14,165,233,.25); }

.btn-today {
  font-size: .82rem; font-weight: 600;
  padding: .3rem .75rem;
  border-radius: .4rem;
  background: var(--surface); color: #7dd3fc;
  border: 1px solid var(--border);
  text-decoration: none; transition: background .2s;
}
.btn-today:hover { background: var(--border); }
/* Light theme: #7dd3fc above is picked to read on the dark surface this button
   sits on, and against the light theme's white one it all but disappears, so
   Today and Actions both step down to the deep end of the same sky accent —
   the trade the admin nav links already make going light. The location pages'
   "Back to Friends" button keeps the dark theme's #7dd3fc instead (below):
   it's asked for by name to match across themes. */
:root[data-theme="light"] .btn-today { color: var(--accent-deep); }
:root[data-theme="light"] .loc-share-head .btn-today,
:root[data-theme="light"] .loc-all-head .btn-today { color: #7dd3fc; }
/* The Jinja calendar's Today only. It still marks the month you are on with a
   filled button; the React page dropped the mark, because its Today sits beside
   an Actions button wearing this same box and a fill made the two read as
   different kinds of control. Held on the id, which only the server-rendered
   page's button carries. */
#btn-today.btn-today--active { background: var(--accent); color: #fff; border-color: var(--accent); }

/* ── Actions menu ─────────────────────────────────────────────────────────────
   One button holding everything you can do to what the page is showing, opened
   as a list. The calendar page has carried it since it became a React page
   (components/ActionMenu.jsx); the rules below are that component's, so the
   admin pages still rendered from Jinja can wear the same control rather than
   a toolbar that happens to hold the same actions. Both files draw from the
   same tokens, so the two menus come out identical.

   The button is .btn-today's outlined box plus the flex row a text-only button
   has no need of. Not a filled .btn-primary: it outranks nothing on the line
   it sits on, and the rows inside are where the emphasis belongs. */
.action-menu { position: relative; display: inline-flex; flex: 0 0 auto; }
.action-menu-btn {
  display: inline-flex; align-items: center; gap: .35rem;
  white-space: nowrap; cursor: pointer;
}
.action-menu-caret { font-size: .65rem; opacity: .8; transition: transform .2s; }
.action-menu-btn.is-open .action-menu-caret { transform: rotate(180deg); }

.action-menu-pop {
  position: absolute; top: calc(100% + .3rem);
  min-width: 13rem; max-width: 90vw;
  z-index: 1200;
  max-height: min(28rem, 70vh); overflow-y: auto;
  padding: .25rem;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--r-md); box-shadow: var(--sh-lg);
}
/* Which end the panel hangs from — the end its button sits at, so a 13rem
   panel opens into the page rather than off the edge of it. */
.action-menu-pop.align-right { right: 0; }
.action-menu-pop.align-left  { left: 0; }

/* A hairline between groups instead of a heading for each: the first group
   (make something) and the last (delete it) need no naming, and a label over
   one row is more furniture than help. */
.action-menu-group + .action-menu-group {
  margin-top: .25rem; padding-top: .25rem; border-top: 1px solid var(--border);
}
.action-menu-group-label {
  padding: .3rem .55rem .15rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
}

.action-menu-item {
  display: flex; align-items: center; gap: .55rem;
  width: 100%; text-align: left; cursor: pointer;
  padding: .45rem .55rem; border: none; border-radius: var(--r-sm);
  background: none; color: var(--text);
  font-family: inherit; font-size: .88rem; font-weight: 600;
  white-space: nowrap;
}
/* Fixed width so the labels line up in a column whatever their icons are. */
.action-menu-icon { width: 1rem; text-align: center; flex-shrink: 0; opacity: .85; }
.action-menu-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
/* One highlight, whether the pointer or the keyboard put it there, so the two
   cannot disagree about what Enter would take. */
.action-menu-item.at-cursor { background: var(--surface-lift); }
/* The row that makes something, in the colour the button it replaced wore. */
.action-menu-item.is-accent { color: var(--accent-hi); }
.action-menu-item.is-accent.at-cursor { background: var(--accent-wash); }
/* Deleting keeps the colour it wore as a button of its own, and the highlight
   fills red rather than grey — the one row where the pointer passing over it
   is worth noticing. */
.action-menu-item.is-danger { color: var(--danger-soft); }
.action-menu-item.is-danger.at-cursor { background: var(--danger); color: #fff; }
.action-menu-item:disabled { opacity: .45; cursor: default; }
.action-menu-item:disabled.at-cursor { background: none; }

/* ── Calendar table ── */
.calendar-table {
  width: 100%; border-collapse: collapse;
  background: #ffffff; border-radius: .5rem; overflow: hidden;
  box-shadow: 0 1px 4px rgba(0,0,0,.15);
  color: var(--ink);
}
.calendar-table th {
  background: #111827; color: #fff;
  padding: .6rem; text-align: center; font-size: .85rem;
}
.cal-weekday-header {
  display: grid; grid-template-columns: repeat(7, 1fr);
  flex-shrink: 0;
  background: #111827; border-radius: .4rem .4rem 0 0;
}
.cal-weekday-header span {
  color: #fff; padding: .6rem; text-align: center; font-size: .85rem; font-weight: 700;
}
/* Light theme: the header bar (Day 1…Day 7 on the plan page, Sunday…Saturday
   on the calendar) swaps its dark-chrome fill for the same light-blue wash
   the today/selected cells already use, so the bar reads as part of the
   paper-white grid rather than a leftover dark-theme strip. */
:root[data-theme="light"] .calendar-table th,
:root[data-theme="light"] .cal-weekday-header {
  background: var(--paper-tint);
}
:root[data-theme="light"] .calendar-table th,
:root[data-theme="light"] .cal-weekday-header span {
  color: #0369a1;
}
.calendar-table td { vertical-align: top; }

.day-cell {
  min-height: 90px; padding: .5rem;
  border: 1px solid var(--paper-line); position: relative;
  background: var(--paper);
  transition: min-width .2s, box-shadow var(--base) var(--ease),
              background var(--fast) var(--ease);
}
/* Hovering a day should show it is a target without moving anything — the
   grid is dense enough that any size change would ripple across the row. */
.day-cell:hover { background: #fafcff; }
.day-cell.col-focused {
  background: var(--paper-tint) !important;
  box-shadow: inset 0 0 0 2px var(--accent);
}
.day-cell.other-month { background: var(--paper-alt); color: var(--text-dim); }
.day-cell.other-month .day-number { opacity: .45; }
/* Days already spent step back: the grid is for what is still ahead, and a
   month is easier to read when the part of it you can still act on is the
   part that carries colour. Everything stays clickable — past days hold the
   record of the trip — so pointing at one brings it back to full strength. */
.day-cell.day-past { background: var(--paper-alt); }
.day-cell.day-past .day-number { opacity: .5; }
.day-cell.day-past .event-chip,
.day-cell.day-past .more-events,
.day-cell.day-past .route-btn { opacity: .5; }
.day-cell.day-past:hover .event-chip,
.day-cell.day-past:hover .more-events,
.day-cell.day-past:hover .route-btn { opacity: 1; }
.day-cell.today { background: var(--paper-tint); }
/* Today is a ring around the number rather than a filled disc. Solid accent
   is what a selected day wears; today wearing it too made the two markers
   argue about which day you had actually picked. */
.day-cell.today .day-number {
  background: transparent; color: var(--accent);
  border: 2px solid var(--accent); box-sizing: border-box;
  border-radius: 50%; width: 1.6rem; height: 1.6rem;
  display: inline-flex; align-items: center; justify-content: center;
}
/* The ring outranks .day-clickable:hover on specificity, so today needs its
   own hover or it would be the one day that doesn't respond. */
.day-cell.today .day-number.day-clickable:hover { background: var(--accent); color: #fff; }
.day-cell.day-selected {
  box-shadow: inset 0 0 0 2px var(--accent);
}
/* The widened column — the day you are working in. It has the room the other
   six don't, so its text comes up to a size you can read at a glance rather
   than the one the grid squeezes six days into. */
.day-cell.day-wide .day-number  { font-size: 1rem; }
.day-cell.day-wide .event-chip  { font-size: .88rem; padding: .22rem .45rem; }
.day-cell.day-wide .more-events { font-size: .78rem; }
.day-cell.day-wide .route-btn   { font-size: .8rem; padding: .24rem .45rem; }
/* The itinerary reads at the same size as the chips above it: both are the
   day's events, one listed as a route, and two sizes made them look like two
   kinds of thing. */
.day-cell.day-wide .route-info  { font-size: .88rem; line-height: 1.25; }
/* On a desktop screen the widened column doesn't need that much type to be
   legible — a phone does, which is why this is a floor raised only above the
   768px breakpoint rather than a change to the sizes above. A full day's
   itinerary is long, and the smaller step keeps more of it on screen without
   scrolling the column. */
@media (min-width: 769px) {
  .day-cell.day-wide .day-number  { font-size: .9rem; }
  .day-cell.day-wide .event-chip  { font-size: .78rem; padding: .18rem .4rem; }
  .day-cell.day-wide .more-events { font-size: .72rem; }
  .day-cell.day-wide .route-btn   { font-size: .72rem; padding: .2rem .4rem; }
  .day-cell.day-wide .route-info  { font-size: .78rem; }
}
/* Every column but the one you have open, on a screen this size.

   A seven-column grid on a 390px phone gives each day 55px before anything is
   picked, and 28px once a day has taken its 220px. Neither is as wide as the
   words in it, so a title broke to a character or two a line and a time snapped
   mid-digit — and being squeezed by a widened neighbour only made a narrow
   column narrower. It was never the widening that made these unreadable.

   So the chip turns, in every column that isn't the wide one. A column too
   narrow to read across is tall enough to read down, and writing-mode does that
   without touching the markup. The split is text-orientation's default: Latin
   and digits rotate a quarter turn and run down the column, while Chinese stays
   upright and reads the way it already reads, which is the direction it has
   always been able to go. A language that can be set vertically is not one to
   rotate.

   The wide column is the exception, and the only one it needs: 220px is room to
   read a line of text across, which is the whole reason a day is widened.

   Above 560px nothing turns — a tablet's columns are wide enough to read. Both
   grids share .day-cell and mark their open day .day-wide, so this is one set
   of rules for the two pages. */
@media (max-width: 560px) {
  .day-cell:not(.day-wide) .event-chip,
  .day-cell:not(.day-wide) .more-events {
    writing-mode: vertical-rl;
    text-orientation: mixed;   /* the default, said out loud: CJK upright, the rest turned */
    /* One line, and the box is one line wide.

       Turned, a wrap no longer costs height — it costs width, because the next
       line stacks beside this one. So a title longer than the cell is tall put
       a second column of text next to the first, and a third next to that,
       until the chip was wider than the day it belongs to and its words were
       sitting over the neighbouring days. Refusing to wrap keeps a chip exactly
       one line wide however long its title is; the length goes downward, into
       the cell, which grows to hold it.

       max-width is the floor under that: physical on purpose, because what is
       being capped is how far right the box may reach, and on a 320px screen
       one line plus its padding is already wider than the 16px such a column
       gets. Clipped there rather than printed over the next day. */
    white-space: nowrap;
    max-width: 100%;
    overflow: hidden;
  }

  /* Logical, not physical, so the chip's own proportions survive the turn.
     Written as top/bottom and left/right these land on the wrong axes: the
     .4rem meant to sit either side of the words would become .8rem of the
     28px the column has, and the bar marking where a chip starts would sit
     down its side rather than across its head. */
  .day-cell:not(.day-wide) .event-chip {
    padding-block: .15rem;
    padding-inline: .4rem;
    border-left: 0;
    border-inline-start: 3px solid currentColor;
  }
  /* Same reason: the gap after the time belongs along the line of text, which
     is now the way down the column. */
  .day-cell:not(.day-wide) .chip-time,
  .day-cell:not(.day-wide) .cal-stop-num { margin-right: 0; margin-inline-end: .25rem; }

  /* An icon is a picture, not a letter — a turned warning triangle or camera
     reads as a glyph that has fallen over. */
  .day-cell:not(.day-wide) .event-chip i { text-orientation: upright; }

  /* No room beside a column of text for the things that stand next to a chip.
     The tick box and the star both leave their mark on the cell anyway — a
     done chip is struck through, a starred day carries its wash — and the
     chip menu was hover-only on a page with no pointer. */
  .day-cell:not(.day-wide) .done-box,
  .day-cell:not(.day-wide) .day-star-btn,
  .day-cell:not(.day-wide) .chip-menu-btn { display: none; }
}

.day-cell.day-selected::after {
  content: '';
  position: absolute; top: 0; left: 0; right: 0;
  height: 3px; background: var(--accent);
}
/* A day marked important. The wash is what makes the mark legible from the
   far side of the month — a star small enough to sit beside the number is
   not, at the size a seven-column grid leaves a cell. It is written after
   today/past/hover on purpose: those all set a background too, and the day
   you flagged is the one that should still be showing its own colour. */
.day-cell.day-starred { background: var(--paper-star); }
.day-cell.day-starred:hover { background: #fff2cc; }
.day-star-btn {
  background: none; border: 0; padding: 0 .1rem; margin-left: .15rem;
  cursor: pointer; line-height: 1; vertical-align: baseline;
  font-size: .72rem; color: var(--text-muted);
  opacity: 0;
  transition: opacity var(--fast) var(--ease), color var(--fast) var(--ease);
}
/* An empty star is an invitation, and thirty-five of them at once is noise —
   so it appears on the day you are pointing at or working in. A star you
   have actually set is the mark itself and never hides. */
.day-cell:hover .day-star-btn,
.day-cell.day-wide .day-star-btn,
.day-star-btn:focus-visible,
.day-star-btn.is-starred { opacity: 1; }
.day-star-btn:hover,
.day-star-btn.is-starred { color: var(--warn); }
/* Nothing hovers on a touchscreen, so there the outline sits at a weight
   that can be found without being mistaken for a day already marked. */
@media (hover: none) {
  .day-star-btn { opacity: .4; }
}

.day-number { font-size: .85rem; font-weight: 600; display: inline-block; margin-bottom: .3rem; color: var(--ink); }
.day-clickable { cursor: pointer; border-radius: 50%; padding: .05rem .3rem; }
.day-clickable:hover { background: var(--accent); color: #fff; }

.day-delete-all-btn {
  display: none;
  position: absolute; top: 4px; right: 4px;
  background: rgba(255,255,255,.9);
  border: none; border-radius: .25rem; cursor: pointer;
  width: 18px; height: 18px;
  align-items: center; justify-content: center;
  font-size: .62rem; color: var(--text-muted);
  padding: 0;
  z-index: 1;
}
/* The server-rendered calendar puts this on every day with events, so it stays
   hover-only there. The SPA renders it for the widened column alone, where
   hiding it until hover would only make it hard to find. */
.day-cell:hover .day-delete-all-btn { display: flex; }
.day-delete-all-btn.is-pinned { display: flex; }
.day-delete-all-btn:hover { background: #fee2e2; color: var(--danger); }

.event-chip {
  /* A chip shows the whole title, however many lines it takes — the cell grows
     to fit it. It used to clamp at two lines, which read as a complete title
     right up until the one time the end of it was what you needed. */
  display: block;
  word-break: break-word;
  background: var(--paper-tint); color: #0369a1;
  /* A colour bar on the leading edge: chips stack two or three deep in a
     ~90px cell, and the bar is what separates them at a glance when the fill
     colours are this close together. currentColor rather than a fixed hue so
     each .day-chip-N variant below gets a bar in its own colour for free. */
  border-left: 3px solid currentColor;
  border-radius: var(--r-xs); padding: .15rem .4rem;
  font-size: .75rem; margin-top: .2rem;
  font-weight: 500;
  transition: background var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
/* Only where hover is real. iOS keeps :hover on whatever was last tapped, so
   an event picked and then let go of stayed blue as though still picked. */
@media (hover: hover) {
  .event-chip:hover { background: #bae6fd; }
}
/* Past days read back a step so the eye lands on what is still ahead. */
.event-chip.chip-past { opacity: .62; }
/* The count standing in for a folded day's remaining chips. Small accent text
   that underlines on hover — the same shape every other inline action in the
   app takes (.lf-edit-btn, .lf-delete-btn) — rather than a fourth chip, since
   it isn't an event but the way back to the ones being held. The button resets
   are here because the SPA grid renders it as a <button>. */
.more-events {
  font-size: .7rem; color: var(--accent); margin-top: .2rem; display: block;
  cursor: pointer; font-weight: 600;
  width: 100%; text-align: left;
  background: none; border: none; padding: 0; font-family: inherit;
}
.more-events:hover { text-decoration: underline; }

/* extra chips hidden by default; shown when parent cell is expanded */
.chip-extra { display: none; }
.day-cell.expanded .chip-extra { display: block; }

.route-btn {
  display: flex; align-items: center; gap: .25rem;
  width: 100%; border: none; cursor: pointer;
  border-radius: .3rem; padding: .18rem .4rem;
  font-size: .72rem; font-weight: 600;
  margin-top: .35rem; opacity: 1;
  /* Just enough darkening to separate it from the chips above it — it used to
     be .72, which turned the day's pale colour to a muddy grey and made the
     button read as disabled. */
  filter: brightness(0.95);
  transition: filter .2s;
  font-family: inherit;
}
.route-btn:hover { filter: brightness(0.86); }

.optimize-btn {
  display: flex; align-items: center; gap: .25rem;
  width: 100%; border: none; cursor: pointer;
  border-radius: .3rem; padding: .18rem .4rem;
  font-size: .72rem; font-weight: 600;
  margin-top: .2rem;
  background: #059669; color: #fff;
  transition: background .2s;
  font-family: inherit;
}
.optimize-btn:hover { background: #047857; }

.route-info {
  display: none;
  font-size: .75rem;   /* same as .event-chip — see the note on .day-wide */
  line-height: 1.1;
  color: var(--text-faint);
  margin-top: .25rem;
  padding: .25rem .5rem;
  border-left: 2px solid currentColor;
  background: var(--text-strong);
  border-radius: 0 .25rem .25rem 0;
  word-break: break-word;
}
.route-info.open { display: block; cursor: pointer; }
.route-info.open:hover { background: var(--text); }
.ri-stop { display: block; font-weight: 600; color: var(--ink); margin-top: .3rem;
           background: #f8fafc; border-radius: .22rem; padding: .12rem .3rem .14rem; }
.ri-stop:first-child { margin-top: 0; }
.ri-stop.dragging { opacity: .35; }
.ri-stop--hovered { background: #bae6fd; outline: 2px solid var(--accent-hi); }
/* Amber, matching its dot on the map, so the stop you picked is the same
   colour in both places. Listed after --hovered and repeated on the pair so
   pointing at the picked stop doesn't recolour it out from under you. */
.ri-stop--picked,
.ri-stop--picked.ri-stop--hovered { background: #fde68a; outline: 2px solid #f59e0b; }
.ri-stop-placeholder {
  height: 1.6rem;
  margin-top: .3rem;
  border: 2px dashed var(--warn);
  border-radius: .3rem;
  background: #fef9c3;
}
.ri-stop-title { display: flex; align-items: flex-start; gap: .1rem; }
.ri-stop-idx {
  flex: 0 0 auto;
  min-width: .8rem;
  text-align: left;
}
.ri-stop-name {
  flex: 1 1 auto;
  min-width: 0;
}
.ri-stop-time  { display: block; padding-left: .9rem; margin-top: .05rem; }
.ri-stay-wrap  { display: block; font-size: .72rem; color: var(--text-dim); margin-top: .05rem; }
.ri-arrive {
  display: inline-block;
  margin-top: .08rem;
  font-size: .66rem;
  color: var(--ok);
  font-weight: 600;
}
.ri-leave {
  display: inline-block;
  margin-top: .12rem;
  font-size: .66rem;
  color: #ca8a04;
  font-weight: 600;
}
.ri-stay-input {
  width: 3.5rem; border: 1px solid #cbd5e1; border-radius: 3px;
  font-size: .72rem; padding: 0 .15rem; text-align: center; font-family: monospace;
  vertical-align: middle; background: var(--paper); color: var(--ink);
}
.ri-stay-input:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
.ri-time { font-variant-numeric: tabular-nums; color: inherit; font-weight: 600; }
.ri-leg  { display: block; color: var(--text-dim); font-size: .65rem; font-style: italic; padding-left: .5rem; margin-top: .04rem; margin-bottom: 0; line-height: 1; }
.ri-leg:hover { color: #7dd3fc; text-decoration: underline; }
.ri-leg-manual {
  font-style: normal;
  color: var(--text-muted);
  margin-top: -0.25rem;
  line-height: 1;
}
.ri-leg-manual:hover {
  color: var(--text-muted);
  text-decoration: none;
}
.ri-travel-input {
  width: 3.4rem;
  border: 1px solid #cbd5e1;
  border-radius: 3px;
  font-size: .72rem;
  padding: 0 .15rem;
  text-align: center;
  font-family: monospace;
  vertical-align: middle;
  margin: 0 .2rem;
  background: var(--paper); color: var(--ink);
}
.ri-travel-input:focus { outline: 1px solid var(--accent); border-color: var(--accent); }
/* The same leg, printed between the chips of an open day — a plan's or the
   calendar's — and the same control it is in the itinerary panel: pointing at
   one draws that hop's road on the map, clicking one keeps it there. It
   inherits .ri-leg's hover underline to say so. The left pad clears the chips'
   colour bar. */
.chip-leg {
  padding-left: .85rem;
  margin-bottom: .1rem;
  cursor: pointer;
}

.ri-leg--active {
  color: #ea580c !important;
  font-weight: 700;
  font-style: normal;
  text-decoration: none;
  background: #422006;
  border-radius: .2rem;
  padding: .05rem .25rem;
}
/* .ri-leg--active sets its own padding, for a line that sits flush in the
   itinerary panel. Between chips the indent has to survive being lit, or the
   line hops left the moment you point at it. */
.chip-leg.ri-leg--active { padding-left: .85rem; }
.ri-stop--active { color: #ea580c !important; }
.ri-total {
  display: block; margin-top: .35rem;
  font-size: .72rem; font-weight: 700; color: var(--text);
  border-top: 1px solid var(--border); padding-top: .25rem;
}
.ri-total-row {
  display: grid;
  grid-template-columns: .28rem 1fr;
  align-items: start;
  margin-top: .08rem;
}
.ri-total-row:first-child { margin-top: 0; }
.ri-total-icon {
  display: inline-flex;
  justify-content: center;
  width: .28rem;
}
.ri-total-text {
  color: var(--text-dim);
  font-weight: 600;
}
.ri-total-row:first-child .ri-total-text {
  color: var(--text);
  font-weight: 700;
}
.ri-map-link {
  display: inline-flex; align-items: center; gap: .3rem;
  margin-top: .4rem; font-size: .7rem; font-weight: 600;
  color: var(--accent); text-decoration: none;
}
.ri-map-link:hover { text-decoration: underline; }

.seg-mini-popup .leaflet-popup-content-wrapper {
  border-radius: 8px;
  padding: 2px 3px;
}
.seg-mini-popup .leaflet-popup-content {
  margin: 14px 16px;
  font-size: .74rem;
  line-height: 1.25;
  min-width: 210px;
}
.seg-mini-popup-content { color: #1f2937; }
.seg-mini-title { font-weight: 700; }
.seg-mini-meta { margin-top: 2px; color: var(--text-faint); }

/* ── Map wrapper ── */
.map-wrapper {
  width: 100%; height: 600px;
  border-radius: .5rem; overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,.15); margin-bottom: 1.5rem;
}
.map-wrapper iframe { width: 100%; height: 100%; border: none; }

/* ── Map / Events split view ── */
.map-events-split {
  display: flex; gap: 0; height: calc(100vh - 240px - var(--safe-top)); min-height: 300px; margin-bottom: 0;
  border-radius: .5rem; overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,.15);
}
.events-side-panel {
  flex: 1; min-width: 0; overflow-y: auto;
  background: var(--surface); border-left: 1px solid var(--border); padding: .4rem;
}
.events-side-panel-title {
  font-size: .75rem; font-weight: 600; color: var(--text-dim);
  padding: .3rem .4rem .5rem; text-transform: uppercase; letter-spacing: .04em;
  border-bottom: 1px solid var(--border); margin-bottom: .3rem;
}
.event-thumb-grid {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px;
}
.event-thumb-card {
  display: flex; gap: .45rem; padding: .4rem;
  border-radius: 5px; cursor: pointer;
  border: 1px solid transparent; margin-bottom: .3rem;
  transition: background .12s, border-color .12s; align-items: flex-start;
}
.event-thumb-card:hover { background: var(--border); border-color: var(--border-strong); }
.event-thumb-img { width: 128px; height: 96px; object-fit: cover; border-radius: 4px; flex-shrink: 0; }
.event-thumb-info { display: flex; flex-direction: column; gap: .1rem; min-width: 0; overflow: hidden; }
.event-thumb-title {
  font-size: .78rem; font-weight: 600;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--text);
}
.event-thumb-date { font-size: .7rem; color: var(--text-dim); }
.event-thumb-user { font-size: .68rem; color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

/* ── World map photo grid overrides ── */
.world-map-layout .events-side-panel {
  background: var(--bg); border-left: 1px solid var(--border-subtle);
}
.world-map-layout .events-side-panel-title { color: var(--text-dim); border-bottom-color: var(--border-subtle); }
.world-map-layout .event-thumb-card {
  position: relative; aspect-ratio: 1/1; padding: 0; margin-bottom: 0;
  overflow: hidden; border: 2px solid transparent;
  transition: border-color .15s, transform .15s;
}
.world-map-layout .event-thumb-card:hover { background: transparent; border-color: var(--accent); transform: scale(1.03); z-index: 1; }
.world-map-layout .event-thumb-img { width: 100%; height: 100%; border-radius: 0; }
.world-map-layout .event-thumb-info { display: none; }
.world-map-layout .event-thumb-grid { grid-template-columns: 1fr 1fr 1fr; }

/* ── Map filter form (All Events Map) ── */
.map-filter-form {
  display: flex;
  flex-wrap: nowrap;
  gap: .75rem;
}
.map-filter-form .btn-primary,
.map-filter-form .btn-secondary {
  text-decoration: none;
  display: flex; align-items: center; justify-content: center;
  text-align: center; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
  box-sizing: border-box;
}
#map-filter-mobile { display: none; }
#map-add-mobile { display: none; }

/* ── Page headers ── */
.page-header-top { margin-bottom: .75rem; }
.page-header {
  display: flex; align-items: center; gap: 1rem;
  margin-bottom: 1.5rem;
}
.page-header h2 { font-size: 1.4rem; }
.page-subtitle { color: var(--text-dim); font-size: .85rem; margin-top: .15rem; }
.badge {
  background: var(--accent-wash); color: #7dd3fc;
  border: 1px solid rgba(14,165,233,.25);
  border-radius: var(--r-pill); padding: .25rem .75rem; font-size: var(--t-md);
}

/* ── Event list ── */
.event-list { list-style: none; display: flex; flex-direction: column; gap: .5rem; }
.event-list-item {
  background: var(--surface); border-radius: var(--r-md);
  border: 1px solid var(--border-subtle);
  padding: .75rem 1rem;
  box-shadow: var(--sh-sm);
  transition: background var(--base) var(--ease),
              border-color var(--base) var(--ease),
              box-shadow var(--base) var(--ease);
}
.event-list-item:hover {
  background: var(--surface-hi); border-color: var(--border);
  box-shadow: var(--sh-md);
}
.event-meta { font-size: .85rem; color: var(--text-dim); display: block; margin-top: .2rem; }
.located { color: var(--ok-deep); }
.unlocated { color: var(--danger); }
.section-title { margin: 1.5rem 0 .75rem; font-size: 1.1rem; }

/* ── Event detail ── */
.detail-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: 1rem;
}
.detail-header h2 { font-size: 1.4rem; }
.detail-actions { display: flex; gap: .5rem; }

.detail-card {
  background: var(--surface); border-radius: var(--r-lg);
  border: 1px solid var(--border-subtle);
  padding: 1.25rem; margin-bottom: 1.5rem;
  box-shadow: var(--sh-md);
  display: flex; flex-direction: column; gap: .6rem;
}

/* ── Event form ── */
.event-form {
  background: var(--surface); border-radius: var(--r-lg);
  border: 1px solid var(--border-subtle);
  padding: 2rem; max-width: 640px;
  box-shadow: var(--sh-md);
}
.form-group { margin-bottom: 1.1rem; display: flex; flex-direction: column; gap: .3rem; }
.pw-wrap    { position: relative; display: flex; }
.pw-wrap input { flex: 1; padding-right: 2.4rem; }
.pw-toggle  { position: absolute; right: .5rem; top: 50%; transform: translateY(-50%);
              background: none; border: none; cursor: pointer; color: var(--text-dim);
              font-size: .9rem; padding: .2rem; line-height: 1; }
.pw-toggle:hover { color: var(--text-faint); }
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
/* A grid item's automatic minimum size is its content, so a column narrower
   than what is in it doesn't squeeze the field — the field hangs out of the
   track, and on a phone that means out of the modal. Sizing the track with
   minmax(0, …) is only half of it: this is the other half, and both are
   needed. It bit the time picker first because a browser's own time widget
   has a wide intrinsic size, where a text input will shrink to nothing. */
.form-row > * { min-width: 0; }
label { font-weight: 600; font-size: .9rem; }
.required { color: var(--danger); }
input[type="text"], input[type="email"], input[type="url"], input[type="date"], input[type="time"], input[type="password"], input[type="number"], textarea {
  padding: .55rem .75rem; border: 1px solid var(--border);
  border-radius: var(--r-sm); font-size: var(--t-lg);
  font-family: inherit;
  /* Stated rather than left to each widget. Padding, border and size were
     already shared, so a row of fields looked like it should line up — but
     with the height still coming from whatever box the browser builds
     inside, a text field and a time picker beside it sat a few pixels
     apart. This is the last term in that sum: same font size, same padding,
     same line box, so every control in a form is the same height. */
  line-height: 1.35;
  /* ...and each term a whole number of pixels. The root is 19.2px at the
     default text size, so 1.35 lines plus .55rem of padding came to a field
     47.74px tall: every box the same height on paper, but each snapped to
     the screen by where it happened to start, so a title drew a pixel
     shorter than the date and time under it. Rounded, the sum is whole and
     every field draws the same. The plain values above stay as the fallback
     for a browser without round(); em, so a field that sets its own size
     still gets 1.35 of its own lines. */
  line-height: round(1.35em, 1px);
  padding-block: round(.55rem, 1px);
  transition: border-color var(--base) var(--ease),
              box-shadow var(--base) var(--ease),
              background var(--base) var(--ease);
  /* Sunken rather than level with the card it sits on — the one-step-darker
     fill is what tells you a field is editable before you click it. */
  background: var(--surface-sunken); color: var(--text);
}
input[type="text"]:hover, input[type="email"]:hover, input[type="url"]:hover,
input[type="password"]:hover, input[type="number"]:hover, textarea:hover {
  border-color: var(--border-strong);
}
input:focus, textarea:focus {
  outline: none; border-color: var(--accent);
  box-shadow: var(--ring);
  background: var(--bg);
}
input::placeholder, textarea::placeholder { color: var(--text-faint); }

/* ── Number stepper ───────────────────────────────────────────────────────
   The browser draws <input type="number">'s spin box in its own chrome: a
   pale sliver of arrows welded to the right edge that ignores the palette
   entirely, so on the sunken dark fields it is the one bit of every form
   that still looks like a 2011 desktop app. The native arrows are hidden
   here and replaced with two real buttons, which puts the control back in
   the token system — surface fill, --border rules, sky on press.

   The markup is produced in two places that must agree: enhanceNumberInputs()
   in static/js/app.js wraps the Jinja pages' fields on load, and
   <NumberStepper> does the same for the SPA. Both emit the DOM below. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none; appearance: none; margin: 0;
}

.num-stepper { position: relative; display: flex; }
.num-stepper > input[type="number"] {
  /* No min-width:0 here on purpose: some of these fields are a flex item in a
     two-up row, where letting the input collapse below its content would hand
     the whole row to the field beside it. */
  flex: 1; width: 100%;
  /* The buttons sit inside the field's own border, so the text has to stop
     short of them rather than run underneath. */
  padding-right: 2.2rem;
}
.num-stepper-btns {
  /* Inset by the field's 1px border so the column tucks inside it. */
  position: absolute; top: 1px; right: 1px; bottom: 1px;
  width: 1.8rem;
  display: flex; flex-direction: column;
  border-left: 1px solid var(--border);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  overflow: hidden;
  /* Only the buttons themselves take the pointer, so the pixels the rounded
     corner clips away fall through to the input rather than swallowing a
     click that hit the field's edge. */
  pointer-events: none;
}
.num-stepper-btn {
  pointer-events: auto;
  flex: 1; display: flex; align-items: center; justify-content: center;
  padding: 0; border: none;
  /* A step above the card the field sits on, not level with it — at --surface
     the column disappears into the modal behind it and only the divider rule
     is left to say the arrows are pressable. */
  background: var(--surface-hi); color: var(--text-dim);
  font-size: .62rem; line-height: 1; cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.num-stepper-btn + .num-stepper-btn { border-top: 1px solid var(--border); }
.num-stepper-btn:hover  { background: var(--surface-lift); color: var(--text); }
.num-stepper-btn:active { background: var(--accent); color: var(--text-strong); }
.num-stepper-btn:disabled {
  background: var(--surface-hi); color: var(--text-faint); cursor: default;
}
.modal-select {
  padding: .55rem .75rem; border: 1px solid var(--border);
  border-radius: var(--r-sm); font-size: var(--t-lg);
  font-family: inherit;
  transition: border-color var(--base) var(--ease), box-shadow var(--base) var(--ease);
  background: var(--surface-sunken); color: var(--text); cursor: pointer;
}
.modal-select:hover { border-color: var(--border-strong); }
.modal-select:focus {
  outline: none; border-color: var(--accent);
  box-shadow: var(--ring);
}
small { color: var(--text-dim); font-size: .8rem; }
.form-actions { display: flex; gap: .75rem; margin-top: 1.5rem; }

/* ── Auth (login/register) ── */
.auth-form { margin: 2rem auto; max-width: 400px; }
.auth-switch { margin-top: 1rem; font-size: .9rem; color: var(--text-dim); }
.auth-switch a { color: var(--accent); font-weight: 600; }

/* ── Admin accounts table ── */
.admin-settings-grid {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 1rem; margin-bottom: 1.5rem;
}
.admin-setting-card { margin: 0; max-width: none; padding: 1.25rem; }
.admin-setting-card h3 {
  font-size: 1rem; font-weight: 700; color: var(--text-strong); margin: 0 0 .6rem;
  display: flex; align-items: center; gap: .4rem;
}
.admin-setting-card p { font-size: .83rem; color: var(--text-dim); margin: 0 0 .75rem; }
.admin-setting-card .form-actions { margin-top: 1rem; }
.admin-setting-price-row { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; }
/* A grid track's automatic minimum is its content's min-content width, and an
   <input>'s min-content is the default ~20-character box the browser gives it.
   Two of those side by side are wider than a phone, so the price card pushed
   the whole Config page sideways — and, being the widest thing in the outer
   grid, took its column with it. Both levels are told the tracks may shrink,
   and the boxes to fill whatever track they land in. */
.admin-settings-grid > *,
.admin-setting-price-row > * { min-width: 0; }
.admin-setting-price-row input { width: 100%; box-sizing: border-box; }
.admin-setting-status { font-size: .78rem; min-height: 1rem; margin-top: .3rem; }

.admin-accounts-toolbar {
  display: flex; align-items: center; justify-content: space-between;
  margin: .75rem 0 .5rem; flex-wrap: wrap; gap: .5rem;
}
.admin-accounts-title { font-size: 1rem; font-weight: 700; color: var(--text-strong); margin: 0; }
.admin-accounts-actions { display: flex; gap: .5rem; flex-wrap: wrap; }
.admin-inline-form { display: inline; }

.admin-filter-row {
  display: flex; gap: .5rem; margin-bottom: .75rem;
  align-items: center; flex-wrap: wrap;
}
.admin-filter-select, .admin-filter-search {
  padding: .4rem .6rem; border: 1px solid var(--border); border-radius: 6px;
  font-size: .9rem; background: var(--bg); color: var(--text);
}
.admin-filter-select { cursor: pointer; white-space: nowrap; }
.admin-filter-search { flex: 1; min-width: 180px; box-sizing: border-box; }

/* The border/radius live here, one level up from the scrolling element, on
   purpose: Safari clips a `position: sticky` header to a glitchy sliver when
   the scrolling ancestor itself has both `overflow` and `border-radius`, and
   the accounts table's sticky thead hit exactly that. Giving the radius to a
   non-scrolling wrapper and letting .admin-table-wrap itself stay
   square-cornered sidesteps the bug while keeping the same rounded card
   look. */
.admin-list-scroll-container {
  position: relative; overflow: hidden;
  border: 1px solid var(--border); border-radius: 8px;
}
.admin-table-wrap {
  max-height: 420px; overflow-y: auto;
  /* Columns that don't fit the viewport (phone, iPad) scroll sideways inside
     this box, not the page — without this the table spilled past its own
     border and dragging it dragged the whole card/page along with it. */
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  scrollbar-width: thin; scrollbar-color: var(--border-strong) var(--surface);
}
.admin-table-wrap .admin-table { margin-bottom: 0; }
/* `position: sticky` on <thead> itself is unreliable across browsers (table
   row groups don't consistently participate in sticky positioning) — putting
   it on each <th> instead is the robust form, and works everywhere. */
.admin-table-wrap thead th { position: sticky; top: 0; z-index: 1; }
.admin-table-wrap::-webkit-scrollbar { width: 8px; }
.admin-table-wrap::-webkit-scrollbar-track { background: var(--surface); }
.admin-table-wrap::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
.admin-table-wrap::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* Custom scroll slider: hidden by default (desktop relies on the native
   scrollbar above); switched on for mobile, where native scrollbars
   auto-hide, in the 768px media query below. */
.admin-scroll-track { display: none; }
.admin-actions-cell { display: flex; gap: .4rem; flex-wrap: wrap; align-items: center; }
.acct-type-select {
  font-size: .82rem; padding: .25rem .4rem; border: 1px solid var(--border);
  border-radius: 5px; background: var(--bg); color: var(--text); cursor: pointer;
}
.admin-chart-card {
  background: var(--surface); border-radius: .5rem; padding: 1rem 1.25rem 1.25rem;
  box-shadow: 0 1px 4px rgba(0,0,0,.3); margin-bottom: 1.25rem;
}
.admin-chart-card h3 { margin: 0; font-size: .95rem; color: var(--text); }
.admin-chart-header {
  display: flex; align-items: center; justify-content: space-between;
  margin-bottom: .75rem;
}
/* The chart's range picker: a preset dropdown and, beside it, a pair of date
   boxes on the app's own small calendar. It wraps rather than shrinks — the
   header shares its line with a heading, and squeezing eight controls onto
   one row on a narrow screen makes every one of them too small to hit. */
.chart-range-form {
  display: flex; align-items: center; flex-wrap: wrap; gap: .5rem;
  font-size: .85rem; color: var(--text-dim);
}
.chart-range-form select,
.chart-range-form .chart-range-date {
  padding: .3rem .5rem; border: 1px solid var(--border); border-radius: .35rem;
  font-size: .85rem; background: var(--surface); color: var(--text);
}
/* Wide enough for YYYY-MM-DD and its placeholder, and no wider: these sit in
   a header that already has a heading on it. The extra room over the 7.5rem
   this used to be is the calendar glyph's: the boxes are components/DateInput
   now (the same picker the rest of the SPA hangs off a date field), and it
   reserves 2.1rem on the right for the glyph it draws there — which was
   clipping the last character off "YYYY-MM-DD". */
.chart-range-form .chart-range-date {
  width: 9.6rem; font-variant-numeric: tabular-nums;
}
.chart-range-form .chart-range-date::placeholder { color: var(--text-muted); }
.chart-range-form .chart-range-date:focus {
  outline: none; border-color: var(--accent); background: var(--surface);
}
/* A label and the box it names stay together when the row wraps. */
.chart-range-field { display: inline-flex; align-items: center; gap: .35rem; }
/* The seam between the two ways of picking a range, so the date boxes read as
   an alternative to the dropdown rather than as a filter on top of it. */
.chart-range-or {
  color: var(--text-muted); font-size: .75rem; text-transform: uppercase;
  letter-spacing: .04em; padding: 0 .1rem;
}
/* A button rather than a link since the Accounts page became a React route
   (pages/AdminUsersPage.jsx): going back to the preset window rewrites the
   route rather than fetching a page. Buttons arrive with the browser's own
   grey, border and font, so those are spelled out here. */
.chart-range-clear {
  font-size: .8rem; color: var(--text-dim); text-decoration: underline;
  background: none; border: 0; padding: 0; font-family: inherit; cursor: pointer;
}
.chart-range-clear:hover { color: var(--text); }
.chart-range-note { margin: -.25rem 0 .5rem; color: var(--text-dim); }
/* The logins-per-day plot. Three lines over one shared day axis — the day's
   total and the registered/guest split under it — replacing the stacked
   columns this used to be. A line only needs a position per day, not a
   column wide enough to print three numbers in, so the whole range fits the
   card at any length instead of scrolling sideways past the right edge.

   The plot is two layers on one box, both placed in percentages: an SVG
   stretched to the box for the lines, and absolutely positioned elements for
   the dots, labels and hover targets. Stretching an SVG would squash a round
   dot into an ellipse and text with it, which is why only the lines live in
   it — and their stroke widths are pinned with vector-effect so the stretch
   cannot thin or fatten them either. */
.login-plot {
  --plot-h: 190px;
  display: flex; align-items: flex-start;
  /* Room above the axis top for the peak day's label, which sits on top of
     its point and would otherwise be clipped by the card. */
  padding-top: 1.1rem; margin-bottom: .5rem;
}
.login-plot-yaxis {
  position: relative; flex: 0 0 auto; width: 2.1rem; height: var(--plot-h);
}
.login-plot-ytick {
  position: absolute; right: .45rem; bottom: 0; transform: translateY(50%);
  font-size: .65rem; color: var(--text-dim); font-variant-numeric: tabular-nums;
}
.login-plot-main { flex: 1 1 auto; min-width: 0; }
.login-plot-area { position: relative; height: var(--plot-h); }
.login-plot-grid { position: absolute; inset: 0; pointer-events: none; }
/* Hairline and solid: the gridlines are there to be measured against, not
   to be looked at, and dashes read as data. */
.login-plot-gridline {
  position: absolute; left: 0; right: 0; border-top: 1px solid var(--border);
}
.login-plot-gridline.is-base { border-top-color: var(--border-strong); }
/* overflow: visible so a point sitting exactly on the axis top keeps its
   full stroke rather than losing the outer half of it to the box edge. */
.login-plot-lines {
  position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible;
}
.login-plot-line { stroke-width: 2px; }
/* The total is the line the section is named after, so it is the emphasis:
   drawn a touch heavier and in ink rather than in a series hue, which leaves
   the two hues to mean "which kind of login" and nothing else. */
.login-plot-line-total  { stroke: var(--chart-total); stroke-width: 2.5px; }
/* A quarter of days packs four times the turns into the same width, so the
   lines thin out to keep the shape of the range readable rather than
   smearing into three bands of colour. */
.login-plot.is-dense .login-plot-line { stroke-width: 1.5px; }
.login-plot.is-dense .login-plot-line-total { stroke-width: 2px; }
.login-plot-line-member { stroke: var(--chart-member); }
.login-plot-line-guest  { stroke: var(--chart-guest); }
.login-plot-line-deleted { stroke: var(--chart-deleted); stroke-dasharray: 4 3; }
/* Dots carry a ring in the card's own colour rather than a border, so they
   stay legible where two series cross without adding ink that isn't data. */
.login-plot-dot {
  position: absolute; width: 8px; height: 8px; border-radius: 50%;
  transform: translate(-50%, 50%); box-shadow: 0 0 0 2px var(--surface);
}
.login-plot-dot-total  { background: var(--chart-total); }
.login-plot-dot-member { background: var(--chart-member); }
.login-plot-dot-guest  { background: var(--chart-guest); }
.login-plot-dot-deleted { background: var(--chart-deleted); }
/* The one direct label on the plot, on the busiest day. It sits on its point
   and grows upward from it; the two outer thirds of the range anchor it to
   the side they are nearest so it cannot hang off the card. */
.login-plot-peak {
  position: absolute; margin-bottom: 9px; transform: translateX(-50%);
  font-size: .7rem; font-weight: 700; color: var(--text-strong);
  white-space: nowrap;
}
.login-plot-peak.is-start { transform: translateX(0); }
.login-plot-peak.is-end   { transform: translateX(-100%); }
/* One hover target per day, each reaching halfway to its neighbours, so
   every pixel of the plot belongs to exactly one day and the pointer never
   falls between two of them. Everything the hover shows is a child of the
   target, which is what keeps this working without a line of JavaScript. */
.login-plot-band { position: absolute; top: 0; bottom: 0; }
.login-plot-cursor {
  position: absolute; top: 0; bottom: 0;
  border-left: 1px solid var(--border-strong); opacity: 0;
}
.login-plot-hit {
  position: absolute; width: 11px; height: 11px; border-radius: 50%;
  transform: translate(-50%, 50%); background: var(--chart-total);
  box-shadow: 0 0 0 2px var(--surface); opacity: 0;
}
.login-plot-tip {
  position: absolute; top: 0; z-index: 3; min-width: 9.5rem;
  padding: .45rem .55rem; border: 1px solid var(--border); border-radius: .4rem;
  background: var(--surface-hi); box-shadow: var(--sh-md);
  font-size: .72rem; color: var(--text);
  /* Pinned to the top of the plot and opening to one side of the crosshair,
     so it clears the point it is describing rather than sitting on top of
     it, and clears the card's edges at both ends of the range. */
  opacity: 0; visibility: hidden; pointer-events: none; transition: opacity .12s;
}
.login-plot-tip.is-right { transform: translateX(0);    margin-left: 10px; }
.login-plot-tip.is-left  { transform: translateX(-100%); margin-left: -10px; }
.login-plot-band:hover .login-plot-cursor,
.login-plot-band:hover .login-plot-hit { opacity: 1; }
.login-plot-band:hover .login-plot-tip { opacity: 1; visibility: visible; }
.login-plot-tip-date {
  font-weight: 700; margin-bottom: .25rem; color: var(--text-strong);
  font-variant-numeric: tabular-nums;
}
.login-plot-tip-row {
  display: flex; align-items: center; justify-content: space-between;
  gap: .9rem; line-height: 1.65;
}
.login-plot-tip-key {
  display: inline-flex; align-items: center; gap: .35rem; color: var(--text-dim);
}
.login-plot-tip-val { font-weight: 700; font-variant-numeric: tabular-nums; }
.login-plot-xaxis { position: relative; height: 1rem; margin-top: .45rem; }
.login-plot-xtick {
  position: absolute; top: 0; transform: translateX(-50%);
  font-size: .62rem; color: var(--text-dim); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.login-plot-xtick.is-start { transform: translateX(0); }
.login-plot-xtick.is-end   { transform: translateX(-100%); }
/* The numbers behind the lines, for a touch screen with no hover to offer,
   for a screen reader, and for anyone after an exact value. */
.login-plot-table-toggle { font-size: .78rem; color: var(--text-dim); margin-bottom: .75rem; }
.login-plot-table-toggle > summary { cursor: pointer; padding: .25rem 0; width: fit-content; }
.login-plot-table-wrap {
  max-height: 15rem; overflow: auto; margin-top: .45rem;
  border: 1px solid var(--border); border-radius: .4rem;
}
.login-plot-table { width: 100%; border-collapse: collapse; font-size: .75rem; }
.login-plot-table th, .login-plot-table td {
  padding: .3rem .7rem; text-align: right; font-variant-numeric: tabular-nums;
  border-bottom: 1px solid var(--border-subtle);
}
.login-plot-table tbody th[scope="row"], .login-plot-table tfoot th[scope="row"] {
  text-align: left; font-weight: 500; color: var(--text-dim);
}
.login-plot-table thead th {
  position: sticky; top: 0; background: var(--surface-hi); color: var(--text-dim);
  font-weight: 600;
}
.login-plot-table thead th:first-child { text-align: left; }
.login-plot-table tbody td { color: var(--text); }
.login-plot-table tr.is-empty th, .login-plot-table tr.is-empty td { color: var(--text-muted); }
.login-plot-table tfoot th, .login-plot-table tfoot td {
  font-weight: 700; color: var(--text); border-top: 1px solid var(--border-strong);
  border-bottom: 0;
}
.chart-legend {
  display: flex; gap: 1rem; align-items: center; flex-wrap: wrap;
  font-size: .75rem; color: var(--text-dim);
}
.chart-legend-item { display: inline-flex; align-items: center; gap: .35rem; }
.chart-legend-note { font-style: italic; }
.chart-swatch { width: .7rem; height: .7rem; border-radius: 2px; display: inline-block; }
.chart-swatch-total { background: var(--chart-total); }
.chart-swatch-member { background: var(--chart-member); }
.chart-swatch-guest { background: var(--chart-guest); }
.chart-swatch-deleted { background: var(--chart-deleted); }
.chart-caption { margin: 0 0 .75rem; }
/* Two strips of tiles above the plot: the accounts that exist right now, and
   the logins the selected range adds up to. Side by side while there is room
   for both, stacked on a phone. */
.chart-stat-groups {
  display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 1rem;
}
.chart-stat-group { flex: 1 1 18rem; min-width: 0; }
.chart-stat-group-title {
  margin: 0 0 .35rem; font-size: .72rem; font-weight: 700; color: var(--text-dim);
  text-transform: uppercase; letter-spacing: .04em;
}
.chart-stats { display: flex; flex-wrap: wrap; gap: .5rem; }
.chart-stat {
  flex: 1 1 6.5rem; display: flex; flex-direction: column; gap: .15rem;
  padding: .45rem .6rem; border: 1px solid var(--border); border-radius: .4rem;
  background: var(--surface);
}
.chart-stat-label {
  display: inline-flex; align-items: center; gap: .35rem;
  font-size: .72rem; color: var(--text-dim);
}
.chart-stat-value {
  font-size: 1.05rem; font-weight: 700; color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* The total tile wears the same ink the plot's total line does. It used to
   wear the sky accent, which is now what "registered" means two tiles to its
   left — the same colour naming two different things in one strip. */
.chart-stat-total .chart-stat-value { color: var(--text-strong); }
.chart-stat-sub { font-size: .68rem; color: var(--text-dim); }
/* No border-radius/overflow here: the rounded corners already come from
   .admin-list-scroll-container, and giving the table itself an `overflow`
   other than visible would make it (not .admin-table-wrap) the nearest
   scroll-container ancestor for the sticky thead th's below — since the
   table itself never scrolls, that silences the sticky header entirely
   instead of just clipping it. */
.admin-table {
  width: 100%; border-collapse: collapse; background: var(--surface);
  box-shadow: 0 1px 4px rgba(0,0,0,.3);
}
.admin-table th, .admin-table td {
  padding: .65rem 1rem; text-align: left; font-size: .9rem;
  border-bottom: 1px solid var(--border);
}
.admin-table th { background: var(--bg); font-weight: 700; color: var(--text); }
.admin-table th.sortable {
  cursor: pointer; user-select: none; white-space: nowrap;
}
.admin-table th.sortable:hover { color: var(--accent); }
.admin-table th.sortable.sorted { color: var(--accent); }
.admin-table th .sort-icon { margin-left: .35rem; font-size: .78rem; opacity: .55; }
.admin-table th.sortable.sorted .sort-icon { opacity: 1; }
.admin-table code { font-size: .78rem; color: var(--text-dim); }
.admin-table-note { font-size: .82rem; color: var(--text-dim); font-style: italic; }
.status-badge {
  display: inline-block; padding: .15rem .55rem; border-radius: 1rem;
  font-size: .78rem; font-weight: 600;
}
.status-active  { background: #14532d; color: #86efac; }
.status-blocked { background: #450a0a; color: #fca5a5; }

/* ── Friends page ── */
.friend-search-wrap { position: relative; margin-bottom: .5rem; }
.friend-search-icon {
  position: absolute; left: .65rem; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: .8rem; pointer-events: none;
}
/* Two classes so this beats the global input[type="text"] padding. */
.friend-search-wrap .friend-search-input {
  width: 100%; box-sizing: border-box;
  padding: .4rem 2rem .4rem 2rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: .85rem; font-family: inherit;
}
.friend-search-wrap .friend-search-input:focus { outline: none; border-color: var(--accent); }
.friend-search-clear {
  position: absolute; right: .4rem; top: 50%; transform: translateY(-50%);
  background: none; border: none; color: var(--text-muted);
  cursor: pointer; font-size: .8rem; line-height: 1;
  padding: .2rem .35rem; border-radius: 4px;
}
.friend-search-clear:hover { color: var(--text); background: var(--border); }

#friends-list {
  max-height: 340px; overflow-y: auto;
  scrollbar-width: thin; scrollbar-color: var(--border-strong) var(--surface);
}
#friends-list::-webkit-scrollbar { width: 8px; }
#friends-list::-webkit-scrollbar-track { background: var(--surface); }
#friends-list::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; }
#friends-list::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
.friend-item.is-hidden { display: none !important; }

/* ── A location-share row (friends page) ──────────────────────────────────────
   One share, incoming or outgoing: a status chip, who it is and what it is
   doing, and the buttons that end it. pages/FriendsPage.jsx used to draw these
   with an inline display:flex per row, and the React port dropped it — the row
   fell back to block flow, so the name, the countdown and the buttons ran
   together into one unbroken line with no gap between them. The layout is a
   class now, in one place, instead of inline on every row.

   Two things the old flat row never handled: the name and its status line are
   stacked rather than competing for the same line — at phone width a Chinese
   or German label plus a countdown plus two buttons has no room, and the
   countdown ends up welded to the username — and the buttons move as one
   group, so they wrap to a second line together instead of a button at a
   time. */
.loc-share-row {
  display: flex; align-items: center; flex-wrap: wrap;
  gap: .4rem .6rem;
  padding: .5rem .6rem;
  margin-bottom: .4rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
/* The status hue is carried by the icon's own chip and not by the row, so a
   list of shares still reads as one list rather than a stack of coloured
   alerts. */
.loc-share-row__icon {
  flex: none; align-self: flex-start;
  width: 1.75rem; height: 1.75rem;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: var(--r-pill);
  font-size: .8rem;
}
.loc-share-row__icon--live    { color: var(--ok);     background: rgba(34,197,94,.15); }
.loc-share-row__icon--pending { color: var(--warn);   background: rgba(245,158,11,.15); }
.loc-share-row__icon--out     { color: var(--accent); background: var(--accent-wash); }

/* min-width:0 is what lets a long username wrap instead of shoving the buttons
   off the row: a flex item will not shrink below its content width without it.
   The 8rem basis decides when the actions drop to their own line — below that
   there is no useful row left to share with them. */
.loc-share-row__body { flex: 1 1 8rem; min-width: 0; }
.loc-share-row__name {
  display: block;
  font-size: .85rem; line-height: 1.35; color: var(--text);
  overflow-wrap: anywhere;
}
.loc-share-row__name strong { font-weight: 700; color: var(--text-strong); }
.loc-share-row__meta {
  display: block;
  font-size: .72rem; line-height: 1.35; color: var(--text-dim);
  overflow-wrap: anywhere;
}
/* margin-left:auto keeps the group against the right edge on the shared line
   and on the wrapped one alike. */
.loc-share-row__actions {
  flex: none; display: flex; align-items: center; gap: .35rem;
  margin-left: auto;
}
/* Every control in a row is one size whatever tag it is — the map link is an
   <a> and its neighbours are <button>s, and they used to be sized one by one
   inline in the JSX with paddings that did not quite agree. Labels never
   break: "Map" and "Stop" are two characters wide in Chinese and wrapping
   them buys nothing. */
.loc-share-row__actions .btn-primary,
.loc-share-row__actions .btn-secondary,
.loc-share-row__actions .btn-danger {
  font-size: .75rem; padding: .3rem .6rem;
  min-height: 2rem; box-sizing: border-box;
  white-space: nowrap; text-decoration: none;
}
/* Icon-only buttons get a square box rather than a label-shaped one, so the
   glyph sits centred instead of adrift in a button built for words. The fixed
   height is shared with the labelled buttons above so the group is one band
   whatever mix of the two a row happens to hold — and so an icon that fails
   to load leaves a button rather than a red sliver. */
.loc-share-row__actions .loc-share-row__icon-btn {
  padding: 0; width: 2rem; justify-content: center;
}
/* Hiding a row must not touch the display:flex above, or the row collapses to
   block layout and the buttons pile up on the left. */
.loc-share-row.is-hidden { display: none !important; }

/* What this device will do for a location share once TimeMap is no longer in
   front — see renderShareOutlook() in friends.html. Worn by two elements: the
   hint inside the share dialog and the banner over a share already running.
   The tone modifier is the whole difference between them, so that "it keeps
   going in your pocket" and "it stops when you lock the screen" cannot be
   mistaken for each other at a glance. */
.loc-outlook {
  font-size: .75rem; line-height: 1.45; margin: 0;
  border-radius: 6px;
}
/* The plain case carries no box: it is the dialog's ordinary explanatory
   line, and boxing every state would make the ones that matter ordinary. */
.loc-outlook--info { color: var(--text-muted); }
.loc-outlook--ok,
.loc-outlook--warn,
.loc-outlook--bad { padding: .5rem .6rem; border: 1px solid; }
.loc-outlook--ok   { border-color: #166534; background: #0d1f14; color: #bbf7d0; }
.loc-outlook--warn { border-color: #78350f; background: #241a10; color: #fde68a; }
.loc-outlook--bad  { border-color: #7f1d1d; background: #2a1414; color: #fecaca; }
.loc-outlook__settings {
  display: inline-block; margin-top: .45rem;
  background: none; border: 1px solid currentColor; color: inherit;
  border-radius: 5px; padding: .25rem .55rem;
  font-size: .72rem; font-family: inherit; cursor: pointer;
}
.loc-outlook__settings:hover { background: rgba(255, 255, 255, .07); }
/* Takes the button's place when neither route to the settings page worked.
   Same inset as the button it replaces, so the box does not change shape
   under the reader's thumb the moment they tap it. */
.loc-outlook__manual {
  display: inline-block; margin-top: .45rem;
  padding: .25rem .55rem;
  border: 1px dashed currentColor; border-radius: 5px;
  font-size: .72rem;
}

/* ── Friend groups (friends page) ── */
.group-hint {
  color: var(--text-muted); font-size: .78rem; line-height: 1.4;
  margin: 0 0 .5rem;
}
.group-hint a { color: var(--accent); }

.group-new-form { display: flex; gap: .4rem; margin-bottom: .45rem; }
.group-new-form input {
  flex: 1; min-width: 0; box-sizing: border-box;
  padding: .35rem .55rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: .85rem; font-family: inherit;
}
.group-new-form input:focus { outline: none; border-color: var(--accent); }

.group-card {
  border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface);
  padding: .55rem .6rem; margin-bottom: .5rem;
}
.group-card-head { display: flex; align-items: center; gap: .4rem; }
.group-name {
  flex: 1; min-width: 0; font-size: .88rem; font-weight: 700;
  color: var(--text); overflow-wrap: anywhere;
}
.group-count { font-size: .72rem; color: var(--text-muted); white-space: nowrap; }
/* Small enough to sit on one row with the name, big enough to hit on a
   phone — deleting the wrong group is not a typo. */
.group-btn {
  font-size: .72rem; padding: .25rem .45rem; flex-shrink: 0;
  white-space: nowrap;
}
.group-rename-input {
  flex: 1; min-width: 0; box-sizing: border-box;
  padding: .25rem .45rem;
  background: var(--bg); border: 1px solid var(--accent); border-radius: 6px;
  color: var(--text); font-size: .85rem; font-family: inherit;
}
.group-rename-input:focus { outline: none; }

.group-members { display: flex; flex-wrap: wrap; gap: .3rem; margin-top: .45rem; }
.group-chip {
  display: inline-flex; align-items: center; gap: .3rem;
  padding: .18rem .28rem .18rem .45rem;
  border: 1px solid var(--border-strong); border-radius: 999px;
  background: var(--bg); color: var(--text);
  font-size: .75rem; max-width: 100%; overflow-wrap: anywhere;
}
.group-chip-x {
  border: 0; background: transparent; color: inherit;
  min-width: 1.3rem; height: 1.3rem; border-radius: 50%;
  font-size: .95rem; line-height: 1; cursor: pointer; opacity: .7;
}
.group-chip-x:hover { opacity: 1; background: rgba(239,68,68,.2); color: var(--danger-soft); }

.group-empty { margin-top: .4rem; color: var(--text-muted); font-size: .75rem; }

.group-add { display: flex; gap: .35rem; margin-top: .45rem; }
.group-add-select {
  flex: 1; min-width: 0; box-sizing: border-box;
  padding: .3rem .45rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
  color: var(--text); font-size: .8rem; font-family: inherit;
}
.group-add-select:focus { outline: none; border-color: var(--accent); }

/* ── Modal time input ── */
.time-select-group {
  display: flex; align-items: center; gap: .4rem;
  border: 1px solid var(--border); border-radius: .4rem;
  background: var(--bg); padding: 0 .65rem;
}
.time-select-group:focus-within {
  border-color: var(--accent); box-shadow: 0 0 0 3px rgba(14,165,233,.15);
}
.time-select-group .time-input-icon { color: var(--text-dim); font-size: .85rem; }
.time-select-sep { color: var(--text-dim); font-weight: 600; }
.time-select {
  border: none; background: transparent; font-size: .95rem;
  font-family: inherit; padding: .55rem .2rem; cursor: pointer;
  color: var(--text);
}
.time-select:focus { outline: none; }
.time-select-meridiem { margin-left: auto; font-weight: 600; color: #7dd3fc; }

/* ── Modal location status ── */
.modal-loc-row {
  display: flex; gap: .4rem;
}
.modal-loc-row input { flex: 1; min-width: 0; }
/* Narrow screens: the search + randomise buttons don't fit next to the input,
   so give the input the whole first line and let the buttons wrap under it
   instead of overflowing the modal. */
@media (max-width: 560px) {
  .modal-loc-row { flex-wrap: wrap; }
  .modal-loc-row input { flex: 1 1 100%; }
}
.modal-loc-search-btn {
  flex: 0 0 auto;
  padding: 0 .75rem;
  background: rgba(14,165,233,.2); color: #fff;
  border: none; border-radius: .375rem;
  cursor: pointer; font-size: .85rem;
  display: inline-flex; align-items: center;
}
.modal-loc-search-btn:hover { background: rgba(14,165,233,.35); }
.modal-loc-status {
  font-size: .78rem; margin-top: .2rem; min-height: 1rem;
}
.modal-loc-status--ok       { color: var(--ok-deep); }
.modal-loc-status--warn     { color: var(--warn); }
.modal-loc-status--err      { color: var(--danger); }
.modal-loc-status--searching { color: #6b7280; font-style: italic; }

/* ── Empty state ── */
.empty-state {
  text-align: center; padding: 4rem 2rem; color: var(--text-dim);
}
.empty-state p { margin-top: 1rem; font-size: 1rem; }
.empty-state a { color: var(--accent); }
.empty-state a.btn-primary { color: #fff; display: inline-flex; margin-top: 1rem; }

/* ── Event photo gallery ── */
.photo-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: .9rem;
}
.photo-gallery-item-wrap { position: relative; }
.photo-gallery-item {
  display: block; width: 100%; border-radius: .5rem; overflow: hidden;
  box-shadow: 0 1px 4px rgba(0,0,0,.12);
  aspect-ratio: 1 / 1;
  border: none; background: none; padding: 0; cursor: pointer; font: inherit;
}
.photo-gallery-item img, .photo-gallery-item video {
  width: 100%; height: 100%; object-fit: cover;
  display: block; transition: transform .15s ease;
}
.photo-gallery-item:hover img,
.photo-gallery-item:hover video { transform: scale(1.05); }
.gallery-video-badge {
  position: absolute; bottom: .4rem; left: .4rem;
  background: rgba(15,23,42,.7); color: #fff; border-radius: 50%;
  width: 1.8rem; height: 1.8rem; display: flex; align-items: center; justify-content: center;
  font-size: .75rem; pointer-events: none;
}
.photo-delete-btn {
  position: absolute; top: .4rem; right: .4rem;
  width: 1.9rem; height: 1.9rem; border-radius: 50%;
  background: rgba(15,23,42,.65); color: #fff; border: none;
  display: flex; align-items: center; justify-content: center;
  font-size: .8rem; cursor: pointer; transition: background .15s;
}
.photo-delete-btn:hover { background: var(--danger); }

/* ── Photo lightbox ── */
.lightbox-overlay {
  position: fixed; inset: 0;
  background: rgba(0,0,0,.9);
  display: flex; align-items: center; justify-content: center;
  z-index: 10000;
}
.lightbox-img {
  max-width: 90vw; max-height: 85vh;
  border-radius: .4rem;
  box-shadow: 0 8px 32px rgba(0,0,0,.5);
}
.lightbox-close {
  position: absolute; top: 1rem; right: 1.25rem;
  background: none; border: none; color: #fff;
  font-size: 2rem; line-height: 1; cursor: pointer;
  opacity: .85; padding: 0;
}
.lightbox-close:hover { opacity: 1; }
.lightbox-delete {
  position: absolute; top: 1.25rem; left: 1.25rem;
  background: rgba(220,38,38,.85); border: none; color: #fff;
  font-size: .85rem; font-weight: 600; padding: .4rem .75rem;
  border-radius: .4rem; cursor: pointer; display: flex; align-items: center; gap: .4rem;
}
.lightbox-delete:hover { background: var(--danger); }
.lightbox-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  background: rgba(255,255,255,.12); border: none; color: #fff;
  font-size: 1.4rem; width: 3rem; height: 3rem; border-radius: 50%;
  cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: background .15s;
}
.lightbox-nav:hover { background: rgba(255,255,255,.25); }
.lightbox-prev { left: 1rem; }
.lightbox-next { right: 1rem; }
.lightbox-counter {
  position: absolute; bottom: 1rem; left: 50%; transform: translateX(-50%);
  color: #fff; font-size: .85rem; background: rgba(0,0,0,.5);
  padding: .25rem .75rem; border-radius: 999px;
}

/* ── Split layout ── */
.split-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 1.5rem;
  align-items: start;
}
.calendar-pane { min-width: 0; overflow: hidden; }
#show-cal-map-btn { display: none; flex-shrink: 0; margin-bottom: .5rem; width: 100%; justify-content: center; }

/* ── The toolbelt ────────────────────────────────────────────────────────────
   One line of chrome above the grid: on the calendar the month controls and the
   Actions menu; on a plan the picker, the view select and the same menu. All of
   it used to stack — two or three rows of things you touch twice a session,
   sitting on top of the thing you came to read. Folded, a bar is a word and a
   chevron, and the rest are a word or an icon each, so it fits on one line at
   any width worth having.

   The toolbar row inside is display:contents, which promotes the bars
   themselves onto this line. They have to be items of it to sit on it, and to
   be able to leave it — see the opened bar at the foot of this block.

   One class, two pages. Both toolbars were built from the same pieces — an
   .import-bar, an .export-bar, a button belonging to neither — so laying them
   out is the same job twice, and only the two headers need rules of their own.

   Sizes come from the two custom properties, which the responsive block turns
   down for a phone. */
.toolbelt {
  --ctl-t: .9rem;      /* one type size for every control on the line */
  --ctl-h: 2.15rem;    /* one height */
  --ctl-gap: .4rem;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: var(--ctl-gap); margin-bottom: .5rem;
}
.toolbelt .toolbar-row { display: contents; }

/* One size for all of them. They arrived from three different places wearing
   three different sizes — the nav arrows and Map at .9rem, Today and the bars
   at .82rem, the month and year and the plan picker larger again — which was
   fine while each sat on a row of its own and reads as a mistake the moment
   they are side by side. Nothing on this line outranks anything else, so
   nothing is set larger. */
.toolbelt,
.toolbelt .cal-select,
.toolbelt .plan-picker-btn,
.toolbelt .btn-nav,
.toolbelt .btn-today,
.toolbelt .import-bar,
.toolbelt .export-bar,
.toolbelt .btn-export,
.toolbelt .offer-badge,
.toolbelt .btn-secondary { font-size: var(--ctl-t); }

/* Padding evened up, so the boxes are as regular as the type in them. */
.toolbelt .cal-select      { padding: .25rem .5rem; }
.toolbelt .plan-picker-btn { padding: .25rem .5rem; }
.toolbelt .btn-nav         { padding: .3rem .6rem; }
.toolbelt .btn-secondary   { padding: .3rem .75rem; }

/* And one height. Even at one type size these came out a few pixels apart — a
   1.5px border here against 1px there, .25rem of padding against .3rem, an
   icon-only button measured on an icon — which is invisible in a stack and is
   the whole of what you see in a row, where the eye reads the tops and bottoms
   of the boxes as a line and every miss as a wobble in it.

   A floor rather than a fixed height, so nothing can be squeezed shorter than
   its contents; --ctl-h clears the tallest, so in practice they all sit at
   exactly it. Border-box, or the ones with a border would answer the same
   number 2px taller than the ones without. */
.toolbelt .plan-picker-btn,
.toolbelt .cal-select,
.toolbelt .btn-nav,
.toolbelt .btn-today,
.toolbelt .btn-export,
.toolbelt .offer-badge,
.toolbelt .btn-secondary {
  min-height: var(--ctl-h);
  box-sizing: border-box;
}
/* A bar is a box around a label, so its own padding would sit outside the
   height rather than inside it. The label centres in the box instead. Only
   while folded: opened, the bar is a line of its own with its content under the
   label, and that padding is what keeps the two apart. */
.toolbelt .import-bar.bar-folded,
.toolbelt .export-bar.bar-folded {
  min-height: var(--ctl-h);
  box-sizing: border-box;
  justify-content: center;
  padding-top: 0;
  padding-bottom: 0;
}

.toolbelt .import-bar,
.toolbelt .export-bar,
.toolbelt .btn-export,
.toolbelt .offer-badge { flex: 0 0 auto; width: auto; }

/* The calendar's header stays a box: its four controls are one instrument —
   the arrows mean nothing apart from the month between them, and a line break
   through the middle of that would read as three separate controls.

   It takes only the width it needs, and packs left, so the month controls stay
   together at the start of the line. */
.cal-toolbelt .calendar-header {
  flex: 0 1 auto; justify-content: flex-start;
  gap: var(--ctl-gap); margin-bottom: 0;
}

/* The slack falls between the two groups, which puts whatever ends the line
   flush with the calendar's right edge. .toolbar-row is display:contents, so
   this is an auto margin on the first of the trailing items and everything
   after it — the offers badge — comes along.

   This was tried once before and rejected, when the line ended in the Import
   and Export bars: two wide boxes against the far right sat half a screen from
   the controls they belonged beside, and read as adrift rather than as
   aligned. What changed is both halves of that. The toolbar ends in one button
   now, which is near enough nothing to strand, and it lands on an edge the eye
   can actually see — the pane's, the one the grid's scrollbar runs down —
   instead of on empty space. Alignment needs something to align to, and until
   that edge was the thing on the other side of the gap, there wasn't one.

   The plan page gets the same result without this rule, because its picker
   grows to eat the slack. The calendar's header is a fixed box, so the slack
   has to be put somewhere by hand. */
.cal-toolbelt .action-menu { margin-left: auto; }

/* A step down from the navbar. The calendar runs under MapShell, which drops
   the container's margin, so this line sat flush against the navbar's bottom
   edge where Itinerary, Photo Map and My Events have their top bar's .4rem of
   padding between the two. Same .4rem here, so the pages start at one height. */
.cal-toolbelt { padding-top: .4rem; }

/* ── Fitting the toolbelt to the calendar ─────────────────────────────────────
   The sizes above are a desktop's, and the phone block near the foot of this
   file turns them down at a 768px window. Between those two there was nothing,
   and the gap is where the calendar actually lives: the pane is half the
   window on this page, so a 1000px desktop draws the calendar in 470px — a
   good deal narrower than the phone the small sizes were written for — with
   every one of those rules still switched off. The line overflowed, and
   .cal-pane clips rather than scrolls, so what came off the end was simply
   gone: Today, then the Actions button, then the Map button.

   So the controls read the pane instead of the window, and read it as a
   measurement rather than as a threshold. Nothing here is a new size: each
   line runs from the phone value at a 375px pane to the desktop value at
   640px and is clamped at both, so the two tunings that already existed are
   exactly what you get at either end and the range between them — the range
   that had no answer at all — is filled in continuously. No step to land on
   the wrong side of, and the same behaviour whether the pane is narrow
   because the window is or because the map is beside it.

   Wrapped in @container so it applies only where there is a pane to measure:
   the plan page has no such container and keeps the sizes above, and a browser
   too old for the query drops the block rather than mis-sizing on it. Selected
   through .cal-pane so it outranks the phone block regardless of source
   order. */
@container pane (min-width: 0px) {
  .cal-pane .cal-toolbelt {
    --ctl-t:   clamp(.82rem, .707rem + .48cqi, .9rem);
    --ctl-h:   clamp(2rem,   1.788rem + .91cqi, 2.15rem);
    --ctl-gap: clamp(.35rem, .279rem + .3cqi,  .4rem);
    /* The one horizontal padding most of the rest are measured from, so the
       controls thin down together instead of each keeping its own idea of
       snug. The multipliers land on today's values at both ends. */
    --ctl-px:  clamp(.35rem, .138rem + .91cqi, .5rem);
  }
  .cal-pane .cal-toolbelt .cal-select      { padding: .25rem var(--ctl-px); }
  .cal-pane .cal-toolbelt .btn-nav         { padding: .3rem  calc(var(--ctl-px) * 1.2); }
  .cal-pane .cal-toolbelt .btn-export      { padding: .28rem calc(var(--ctl-px) * 1.5); }
  .cal-pane .cal-toolbelt .btn-secondary   { padding: .3rem  calc(var(--ctl-px) * 1.5); }
  /* A ramp of its own rather than a multiple of --ctl-px, because this is the
     last thing on the line and so the one that goes over the edge first, and a
     phone was tuned to .45rem to stop exactly that. A multiplier that honoured
     the phone would have narrowed the desktop.
     Written for the Today button, which the class was named after; Today is a
     row in the Actions menu at every width now, and the menu's own button wears
     .btn-today, so this went on describing the thing in that position. */
  .cal-pane .cal-toolbelt .btn-today       { padding: .3rem  clamp(.45rem, .026rem + 1.81cqi, .75rem); }
  /* The month and year selects close up with the line they sit on, so the group
     reads as part of it rather than as a wider island in it. */
  .cal-pane .cal-toolbelt .cal-picker-group { gap: var(--ctl-gap); }

  /* And they give width back when the line runs short, so that what runs out
     of room is the month's name rather than the Actions button.

     A select is sized by its widest option and, as a flex item, its automatic
     minimum is that same width — so the group could not be shrunk at all, the
     header could not be shrunk through it, and the only thing left with any
     give was the toolbelt itself, which gave by wrapping Actions onto a row of
     its own. min-width:0 is what makes the group answerable to the line's
     width; the floor below it is what stops the answer being nothing.

     The floor is the same for both: the year is four digits and never anything
     else, and the month is trimmed to three letters on a phone (MONTH_ABBR in
     CalendarPage), so neither has far to fall. Ellipsis for the case the floor
     still cannot cover, so a squeezed picker reads as trimmed rather than as
     mis-drawn. */
  .cal-pane .cal-toolbelt .calendar-header  { min-width: 0; }
  .cal-pane .cal-toolbelt .cal-picker-group { min-width: 0; }
  .cal-pane .cal-toolbelt .cal-select {
    min-width: 3.4rem;
    text-overflow: ellipsis;
  }

  /* And when even that is not enough, the header breaks rather than running
     off the edge. It could only wrap under the phone's window query before,
     which is the one case where the pane is the window and so the last case
     that needed the help. */
  .cal-pane .cal-toolbelt .calendar-header { flex-wrap: wrap; }
}

/* The plan's header goes the other way and dissolves into the line. Its four
   controls are four separate things — which plan, rename it, a new one, one
   written for you — so there is nothing to keep together, and held as a box it
   was a full line on its own that pushed the toolbar onto a second. As loose
   items they pack with the rest and break wherever the width runs out. The
   picker takes the slack and is the only one that can afford to: a trip name
   truncates and is still readable, and the dropdown gives it back in full.
   min-width:0 is what lets it shrink past its own text at all. */
.toolbelt .plan-header { display: contents; }
.toolbelt .plan-select { flex: 1 1 8rem; min-width: 0; max-width: none; }

/* Opened, a bar takes a line to itself. Its buttons want the width, and
   squeezed into the space a folded label needs they would stack one per row
   down a column two words wide. Folding it puts it back on the line. */
.toolbelt .import-bar:not(.bar-folded),
.toolbelt .export-bar:not(.bar-folded) { flex: 1 0 100%; }

/* mobile fullscreen overlay for calendar map */
.map-pane.cal-map-fullscreen-overlay {
  /* A column, as the SPA's overlay is (spa.css): the header keeps its line and
     the map takes what is left. Sized to the whole screen on its own, the map
     ran a header's height past the bottom edge and took the Map/Satellite
     switch and the attribution with it. */
  display: flex !important; flex-direction: column;
  position: fixed !important;
  inset: 0 !important; z-index: 9998 !important;
  /* Covers the screen, so the map keeps clear of the status bar and the home
     indicator rather than putting its zoom controls under them. */
  height: 100dvh !important;
  padding: var(--safe-top) 0 var(--safe-bottom) !important;
  background: var(--bg);
}
.map-pane.cal-map-fullscreen-overlay #calendar-map {
  flex: 1 1 0; height: auto !important;
  min-height: 0 !important; border-radius: 0 !important;
}
.map-pane.cal-map-fullscreen-overlay .map-pane-header { padding: .6rem .75rem; }

/* table always fills container width */
.calendar-table { table-layout: fixed; width: 100%; }
.map-pane { position: sticky; top: 1rem; }
.map-pane-header {
  display: flex; align-items: baseline; justify-content: space-between;
  margin-bottom: .5rem; font-weight: 600;
}
.map-hint { font-size: .78rem; color: var(--text-dim); font-weight: 400; }
#calendar-map {
  height: calc(100vh - 150px - var(--safe-top));
  min-height: 400px;
  border-radius: .5rem;
  box-shadow: 0 2px 8px rgba(0,0,0,.15);
}
.map-overview-control {
  border: 0 !important;
  box-shadow: none !important;
  margin-left: 11.5px !important;
}
.map-overview-btn {
  display: block;
  text-align: center;
  width: 32px;
  height: 30px;
  line-height: 30px;
  padding: 0;
  background: var(--surface);
  color: var(--text);
  border: 0;
  border-radius: 0;
  font-size: .82rem;
  text-decoration: none;
  box-shadow: 0 1px 5px rgba(0, 0, 0, .45);
}
.map-overview-btn i {
  display: inline-block;
  vertical-align: middle;
  line-height: 1;
}
.map-overview-btn:hover {
  background: var(--border);
  color: var(--text-strong);
}

/* ── The map control stack ────────────────────────────────────────────────────
   Fit all, locate me, box zoom, the places filter and the places search, down
   the left of a map under Leaflet's own zoom control. The SPA writes this box
   as a style object (frontend/src/components/mapControlStyle.js) because its
   controls are React components positioned one by one; the numbers here are
   that object, and the two must stay the same — a map should not tell you
   which template rendered it. static/js/map-controls.js builds the stack for
   the pages Flask renders. */
.map-ctrl-stack {
  display: flex; flex-direction: column; gap: 2px;
  border: 0 !important; box-shadow: none !important;
}
.map-ctrl-wrap { position: relative; }
.map-ctrl-btn {
  display: flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; padding: 0;
  background: #fff;
  border: 2px solid rgba(0,0,0,.25);
  border-radius: 4px;
  box-shadow: 0 1px 4px rgba(0,0,0,.2);
  color: var(--accent);
  font-size: 14px;
  cursor: pointer;
}
/* Armed and waiting on you — the box-zoom tool holding out for its rectangle,
   or a panel standing open — takes the blue and turns its glyph white. */
.map-ctrl-btn.is-armed { background: var(--accent); color: #fff; }

/* The Map/Satellite switch (static/js/map-style.js): the same square, widened
   to fit a word. */
.map-ctrl-btn.map-style-btn {
  width: auto; gap: .35rem; padding: 0 .6rem;
  font-size: .8rem; font-weight: 600; white-space: nowrap;
}
/* The switch sits bottom-left and the attribution bottom-right. On a phone
   the satellite credit is longer than the screen is wide, and left alone it
   spread across the whole bottom edge on top of the switch — the one way back
   to the street map. Kept to the right of it, it wraps instead. Covers the
   SPA's switch (BaseMapLayer.jsx) as well, which loads this file too. */
.leaflet-container:has(.map-style-ctrl, .tm-map-style-toggle) .leaflet-bottom.leaflet-right {
  max-width: calc(100% - 8.5rem);
}

/* Locate-friend follow toggle: the opposite way round from every other
   map-ctrl-btn — blue while actively following (the common, "all good"
   state you glance past), and it flips to the white call-to-action look
   only once following has lapsed and needs a tap to resume. */
#loc-follow-btn { background: var(--accent); color: #fff; }
#loc-follow-btn.is-armed { background: #fff; color: var(--accent); }

/* A place of interest on the map: the category's disc, and the teardrop a
   search drops on what it found. Both wear the colour as an inline style —
   that is data, not theme. */
.map-places-pin {
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: #fff; font-size: 10px;
  border: 1.5px solid #fff;
  box-shadow: 0 1px 4px rgba(0,0,0,.35);
}
.map-places-found-pin {
  width: 24px; height: 24px;
  border-radius: 50% 50% 50% 0; transform: rotate(-45deg);
  display: flex; align-items: center; justify-content: center;
  color: #fff;
  border: 2px solid #fff;
  box-shadow: 0 2px 6px rgba(0,0,0,.4);
}
.map-places-found-pin i { transform: rotate(45deg); font-size: 11px; }

/* What a pin says when it is opened. Leaflet's popup supplies the card; this
   is only the type inside it. */
.map-places-popup { min-width: 160px; max-width: 250px; line-height: 1.4; }
.map-places-popup-name { font-weight: 700; font-size: .88rem; }
.map-places-popup-kind {
  font-size: .72rem; color: var(--text-dim);
  text-transform: capitalize; margin-bottom: .2rem;
}
.map-places-popup-desc { font-size: .78rem; margin-bottom: .2rem; }
.map-places-popup-addr { font-size: .75rem; color: var(--text-dim); margin: .2rem 0; }
.map-places-popup-coords { font-size: .72rem; color: var(--text-dim); margin-bottom: .3rem; }
.map-places-link { display: block; font-size: .75rem; margin-bottom: .2rem; }
.map-places-link--site { color: var(--accent-hi); }
/* Putting the search's pin away again. Quiet, because it undoes something
   rather than doing anything. */
.map-places-popup-remove {
  width: 100%; margin-top: .25rem; padding: .25rem .7rem;
  background: none; border: none; cursor: pointer;
  color: var(--text-dim); font-family: inherit; font-size: .72rem;
}

/* ── Places of interest: the filter and search dropdowns ──────────────────────
   The two panels that open from the control stack. They were written inline in
   white and slate, from before there was a dark surface for them to sit on,
   and were the last dropdowns on either page still looking like somebody
   else's map — a white card over dark tiles, with slate text nothing else on
   the page uses.

   They take .plan-picker-pop's shape now: the app's surface, border, radius
   and shadow, which is what every other list you can open already wears.
   Written as classes rather than style objects so the rows can carry hover and
   off states, which inline styles have no way to express — the filter rows
   were click targets with nothing to say they were, and the search results the
   same.

   Here rather than in spa.css because the shared-itinerary page opens the same
   two panels from plain Leaflet, and one look drawn twice is one look that
   drifts. */
.map-places-pop {
  position: absolute; left: 34px; top: 0; z-index: 1000;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--sh-lg);
  color: var(--text);
  font-family: inherit;
  overflow: hidden;
}
/* Capped so a long list of categories scrolls inside the panel rather than
   running off the bottom of the map, where there is nothing to scroll it
   with — the search panel's results have had their own scroller all along.
   Only the categories scroll: Show all / Hide all stay pinned under them.
   When the whole panel scrolled, a list just over the cap (eleven categories
   at the Medium text size on an iPad) pushed the two buttons below its edge,
   and iOS shows no scrollbar to say there was anything down there. */
.map-places-pop--filter {
  min-width: 165px; padding: 5px 6px 6px;
  max-height: min(400px, 50vh);
  display: flex; flex-direction: column;
}
.map-places-filter-rows {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
}
.map-places-pop--filter > .map-places-sep,
.map-places-pop--filter > .map-places-actions { flex-shrink: 0; }
/* Wider than it was, and capped against the viewport: its rows carry an
   address under the name now, and an address needs the room. */
.map-places-pop--search { min-width: 272px; max-width: calc(100vw - 60px); }

/* A category, and whether it is being shown. The whole row is the switch. */
.map-places-row {
  display: flex; align-items: center; gap: 6px;
  padding: 3px 4px; border-radius: var(--r-xs);
  cursor: pointer; user-select: none;
}
.map-places-row:hover { background: var(--surface-hi); }
.map-places-row.is-off { opacity: .5; }

/* The category's own colour, the same disc its pins wear on the map. It stays
   an inline background — it is data, not theme. */
.map-places-dot {
  width: 16px; height: 16px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.map-places-dot i { font-size: 8px; color: #fff; }

.map-places-name { flex: 1; font-size: .73rem; text-transform: capitalize; }
.map-places-tick { font-size: .65rem; color: var(--ok-deep); }
.map-places-row.is-off .map-places-tick { opacity: .15; }

.map-places-sep { border-top: 1px solid var(--border); margin: 5px 0 4px; }
.map-places-actions { display: flex; gap: 4px; }
.map-places-actions button {
  flex: 1; padding: 2px 0;
  font-family: inherit; font-size: .68rem;
  color: var(--text-dim);
  background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--r-xs);
  cursor: pointer;
}
.map-places-actions button:hover { background: var(--surface-hi); color: var(--text); }

/* The search field, built like the picker's: a magnifier and a bare input on
   the panel's own surface, rather than a bordered box inside a bordered box. */
.map-places-search {
  display: flex; align-items: center; gap: .4rem;
  padding: 6px 10px; border-bottom: 1px solid var(--border);
  color: var(--text-muted);
}
.map-places-search input {
  flex: 1; min-width: 0;
  background: none; border: none; outline: none;
  color: var(--text); font-family: inherit; font-size: .8rem;
}
.map-places-search input::placeholder { color: var(--text-muted); }
/* Emptying the box by hand. It only appears with something to clear, so it
   never sits there as a dead control. */
.map-places-clear {
  flex-shrink: 0; padding: 0 2px;
  background: none; border: none; cursor: pointer;
  color: var(--text-muted); font-size: .8rem; line-height: 1;
}
.map-places-clear:hover { color: var(--text); }

/* Which of the three kinds of answer the rows below are: a position typed in,
   a place already saved, or somewhere found out in the world. Without them a
   search reads as one flat list in which a saved café and a city on another
   continent look like the same sort of thing. */
.map-places-group {
  padding: 5px 10px 3px;
  font-size: .62rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-muted);
  background: var(--surface-sunken);
  border-top: 1px solid var(--border);
}
.map-places-group:first-child { border-top: none; }

.map-places-list { max-height: 300px; overflow-y: auto; }
.map-places-result {
  display: flex; align-items: center; gap: 7px;
  padding: 5px 10px; cursor: pointer; font-size: .78rem;
  border-top: 1px solid var(--border);
}
.map-places-result:first-child { border-top: none; }
/* A group header is a divider in its own right, so the row under it doesn't
   need a second line drawn just below the first. */
.map-places-group + .map-places-result { border-top: none; }
.map-places-result:hover,
.map-places-result.is-active { background: var(--surface-hi); }
.map-places-result-text { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.map-places-result-name,
.map-places-result-sub {
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Where the place is, under what it is called — the half of a result that says
   which of the world's many Central Parks this one is. */
.map-places-result-sub { font-size: .68rem; color: var(--text-muted); }
.map-places-result-cat {
  font-size: .68rem; color: var(--text-muted);
  text-transform: capitalize; flex-shrink: 0;
  max-width: 5.5rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.map-places-none { padding: 8px 10px; font-size: .78rem; color: var(--text-muted); text-align: center; line-height: 1.4; }

/* The same list, dropped open under a modal's location field rather than off a
   control on the map. Only the panel differs — the rows inside it are the map's
   own, so a search result looks the same wherever you meet one.

   Above the mini-map below it: Leaflet's own controls sit at z-index 800, and
   the results would otherwise open behind the zoom buttons. */
.loc-search-pop {
  position: absolute; left: 0; right: 0; top: calc(100% + 3px);
  z-index: 1200;
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  box-shadow: var(--sh-lg);
  color: var(--text);
  overflow: hidden;
}
.loc-search-pop .map-places-list { max-height: 260px; }

/* ── Calendar stop number badge (shown when route is focused) ── */
.cal-stop-num {
  font-size: .6rem; font-weight: 700;
  background: rgba(0,0,0,.12);
  border-radius: .2rem;
  padding: 0 .2rem;
  margin-right: .2rem;
  vertical-align: middle;
}

/* ── Chip drag-and-drop reorder placeholder ── */
.chip-placeholder {
  height: 1.6rem;
  margin-top: .2rem;
  border: 2px dashed var(--warn);
  border-radius: .3rem;
  background: #fef9c3;
}

/* ── Drag and drop ── */
.event-chip.dragging,
.route-btn.dragging {
  opacity: .35;
  cursor: grabbing;
}
.day-cell.drag-over {
  background: rgba(14,165,233,.1) !important;
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}
/* The copy of a chip that follows a finger during a touch drag. Body-level and
   fixed, so no ancestor's overflow can clip it as it crosses the grid, and
   pointer-events:none so the hit test reads the day underneath rather than
   the thing being dragged. */
.touch-drag-ghost {
  position: fixed;
  z-index: 4000;
  margin: 0 !important;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(1.06) rotate(-1.5deg);
  opacity: .93;
  box-shadow: var(--sh-md);
}
/* iOS answers a long press on a link with its own callout and text selection,
   which would eat the gesture before it started. Touch pointers only: on a
   desktop this would make event titles unselectable for no reason. */
@media (pointer: coarse) {
  .event-chip, .ri-stop, .route-btn {
    -webkit-touch-callout: none;
    -webkit-user-select: none;
    user-select: none;
  }
}
.day-cell.drag-over-time {
  background: #422006 !important;
  outline: 2px dashed var(--warn);
  outline-offset: -2px;
}

/* ── Chip wrapper ── */
/* Flex row so the done box sits beside the chip rather than on top of it —
   in a narrow column an overlay would cover the first word of the title.
   The absolutely positioned chip menu is out of flow and unaffected. */
.chip-wrapper {
  position: relative; margin-top: .2rem;
  display: flex; align-items: flex-start; gap: .18rem;
}
.chip-wrapper .event-chip { margin-top: 0; flex: 1 1 auto; min-width: 0; }

/* ── "Done" tick box (chips and itinerary stops) ── */
.done-box {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: .85rem; height: .85rem;
  /* Both hosts start their text a hair below the box's natural top; this
     drops it onto the first line rather than above it. */
  margin-top: .18rem;
  border: 1.5px solid var(--paper-line); border-radius: var(--r-xs);
  background: var(--paper);
  color: var(--ok-deep); font-size: .58rem; line-height: 1;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.done-box:hover  { border-color: var(--ok-deep); background: #f0fdf4; }
.done-box:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.done-box.is-done { border-color: var(--ok-deep); background: #dcfce7; }

/* A finished stop steps back but stays readable — it is still part of the
   day's route, just no longer something to act on. */
.event-chip.chip-done { opacity: .55; text-decoration: line-through; }
.ri-stop--done .ri-stop-name { text-decoration: line-through; opacity: .6; }
.ri-stop--done .ri-stop-time { opacity: .6; }
.ri-stop-title .done-box { margin-top: .1rem; }

/* ── Chip actions menu (edit / copy / delete) ── */
.chip-menu-btn {
  display: none;
  position: absolute; top: 1px; right: 2px;
  background: rgba(15,23,42,.88);
  border: none; border-radius: .2rem; cursor: pointer;
  width: 16px; height: 16px;
  align-items: center; justify-content: center;
  font-size: .58rem; color: var(--text-dim);
  padding: 0;
  z-index: 2;
}
/* Only where hover is real. iOS Safari hovers on a finger's first tap, and a
   tap that makes something appear is taken as spent on the hover: the click
   never comes, so picking an event took two taps. A finger gets the ⋮ on the
   event it has picked instead. */
@media (hover: hover) {
  .chip-wrapper:hover .chip-menu-btn { display: flex; }
}
@media (hover: none) {
  .chip-wrapper:has(.chip-focused) .chip-menu-btn { display: flex; }
}
.chip-menu-btn:hover { background: var(--bg); color: var(--text); }

.chip-menu {
  position: absolute; top: 18px; right: 2px;
  display: flex; flex-direction: column;
  min-width: 116px;
  background: var(--surface); border: 1px solid var(--border); border-radius: .35rem;
  box-shadow: 0 4px 14px rgba(0,0,0,.4);
  overflow: hidden;
  z-index: 50;
}
/* Flipped by fitChipMenu when dropping down would run out of the day cell. */
.chip-menu.chip-menu--up { top: auto; bottom: calc(100% - 1px); }
.chip-menu-item {
  display: flex; align-items: center; gap: .4rem;
  background: none; border: none; cursor: pointer;
  padding: .35rem .55rem;
  font-size: .72rem; color: var(--text); text-align: left;
}
.chip-menu-item:hover { background: var(--border); }
.chip-menu-item.is-active { color: var(--warn); }
.chip-menu-item--danger { color: var(--danger-soft); }
.chip-menu-item--danger:hover { background: #450a0a; color: #fca5a5; }

/* ── Undo toast ── */
.undo-toast {
  position: fixed; bottom: calc(24px + var(--safe-bottom)); left: calc(24px + var(--safe-left));
  background: var(--surface); color: var(--text-strong);
  padding: .65rem .5rem .65rem 1rem; border-radius: .5rem;
  box-shadow: 0 6px 20px rgba(0,0,0,.35);
  display: flex; align-items: center; gap: .75rem;
  font-size: .85rem; z-index: 10000;
}
/* Out of the way while a dialog is open. It sits above the overlay (9999) and
   never times out, and on a phone it spans the bottom edge — right where a
   dialog's footer is — so after one delete it sat over Delete / Cancel / Save
   of every dialog opened after it and ate their taps, leaving only the × at
   the top that worked. It comes back when the dialog closes. */
body:has(.modal-overlay, .plan-modal-overlay) .undo-toast { display: none !important; }
.undo-toast-btn {
  background: transparent; border: none; color: #7dd3fc;
  font-weight: 700; cursor: pointer; font-size: .85rem;
  display: inline-flex; align-items: center; gap: .35rem;
  padding: .3rem .5rem; border-radius: .3rem;
}
.undo-toast-btn:hover { background: rgba(255,255,255,.1); }
.undo-toast-close {
  background: transparent; border: none; color: var(--text-dim);
  cursor: pointer; font-size: 1rem; padding: 0 .3rem;
}
.undo-toast-close:hover { color: var(--text-strong); }

/* ── Modal overlay ── */
.modal-overlay {
  position: fixed; inset: 0;
  /* Deeper and blurred: the old flat 45% black left the busy map underneath
     legible enough to compete with the dialog on top of it. */
  background: rgba(2,6,23,.68);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
  z-index: 9999;
  overscroll-behavior: none;
  touch-action: none;
  animation: modal-overlay-in var(--base) var(--ease);
}
.modal-box {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  width: min(480px, 95vw);
  box-shadow: var(--sh-lg);
  display: flex; flex-direction: column;
  max-height: 90vh;
  animation: modal-box-in var(--base) var(--ease);
}
@keyframes modal-overlay-in { from { opacity: 0; } }
@keyframes modal-box-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
}
.modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 1rem 1.25rem .75rem;
  border-bottom: 1px solid var(--border);
}
.modal-header h3 { font-size: 1.1rem; margin: 0; color: var(--text-strong); }
.modal-close {
  background: none; border: none; cursor: pointer;
  font-size: 1.3rem; color: var(--text-dim); line-height: 1;
  padding: .1rem .4rem; border-radius: var(--r-xs);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.modal-close:hover { background: var(--border); color: var(--text-strong); }
/* overflow-x is stated, not left out. Setting overflow-y alone doesn't leave
   the other axis visible — a box that scrolls on one axis computes `visible`
   to `auto` on the other — so the body has been quietly draggable sideways
   all along, and anything a pixel too wide for a phone turned into a dialog
   that slides out from under your finger. Nothing in here is meant to be
   read by panning across it: the fields shrink to their column instead. */
.modal-body { padding: 1.25rem; overflow-y: auto; overflow-x: hidden; overscroll-behavior: contain; touch-action: pan-y; }
.modal-footer {
  display: flex; justify-content: space-between; align-items: center;
  padding: .75rem 1.25rem;
  border-top: 1px solid var(--border);
  gap: .5rem;
}
/* Shared date-range row used across export / share / send-offer modals */
.modal-date-row {
  display: flex; align-items: center; gap: .5rem; flex-wrap: wrap;
  margin-bottom: .85rem;
}
.modal-date-row label { font-size: .85rem; font-weight: 600; color: var(--text); }
/* The two fields split whatever the labels leave, rather than each holding a
   fixed 7.5rem. That width was set before .date-input grew its calendar glyph,
   and the 2.1rem of padding the glyph needs came out of the date: the field
   read "2026-08-0" with the last digit cut off. Sharing the row also keeps the
   pair centred in the modal at any width instead of stopping short of its
   right edge. min-width: 0 because a flex item won't shrink below its own
   content otherwise, which would push the row wider than the modal on a
   narrow screen. */
.modal-date-row input[type="text"] { flex: 1 1 8rem; min-width: 0; }
/* Inside a .form-group the row is the field, so its own bottom margin would
   double the gap the group already leaves under it. */
.form-group > .modal-date-row { margin-bottom: 0; }

/* A picker filling a modal field rather than sharing a header line. Its own
   height matches the inputs above and below it, so the field reads as one
   control stacked on another rather than two sizes of box.

   Written as both classes deliberately: `.plan-picker` sets `display: flex`
   and lives in the SPA bundle, which the browser reads after this file, so a
   plain `.modal-picker` lost the tie and the list below opened as a second
   flex column beside the button instead of under it. */
.plan-picker.modal-picker { width: 100%; display: block; }
.modal-picker .plan-picker-btn { width: 100%; font-size: .9rem; font-weight: 600; }

/* The list opens inline here rather than floating over the form. A modal body
   scrolls, and a scroll container clips what is absolutely positioned inside
   it — the popup would have been cut off at the first fold, which for a list
   as tall as this one is most of it. Opening it pushes the fields below it
   down instead, and the body scrolls if that runs past the bottom. Capped
   shorter than the header's for the same reason: this one has to fit in a
   dialog rather than over a page. */
.modal-picker .plan-picker-pop {
  position: static;
  width: 100%; min-width: 0; max-width: none;
  margin-top: .3rem;
}
.modal-picker .plan-picker-list { max-height: min(11rem, 28vh); }

/* The chips under the picker hold the answer, whichever way a name got onto
   the list, so the row below the control always reads as who this is going to
   rather than as a second question. They wrap: a send can reach twenty people
   and the modal is one column wide. */
.offer-chips {
  display: flex;
  flex-wrap: wrap;
  gap: .3rem;
  margin: .4rem 0 .35rem;
}
.offer-chip {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  padding: .2rem .3rem .2rem .45rem;
  border: 1px solid var(--accent-hi);
  border-radius: var(--r-pill);
  background: var(--accent-wash);
  color: var(--accent-hi);
  font-size: .76rem;
  max-width: 100%;
  overflow-wrap: anywhere;
}
.offer-chip-x {
  border: 0;
  background: transparent;
  color: inherit;
  /* A tap target rather than a glyph: taking the wrong person off a send is
     easier to do than to notice, so the × gets a finger's worth of room. */
  min-width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
  opacity: .7;
}
.offer-chip-x:hover {
  opacity: 1;
  background: rgba(239, 68, 68, .2);
  color: var(--danger-soft);
}
/* The wash is the same pale blue on a light page as on a dark one, so the
   text on it has to come down to meet it — the bright sky the dark theme
   wants is barely there against white. */
:root[data-theme="light"] .offer-chip { border-color: var(--accent); color: var(--accent-deep); }
:root[data-theme="light"] .offer-chip-x:hover { color: var(--danger); }

/* Typing a name and putting it on the list is one line: the field takes what
   is left after the button, which stays its own width. */
.offer-add-row { display: flex; gap: .35rem; align-items: stretch; }
.offer-add-row .offer-username { flex: 1; min-width: 0; }
.offer-add-row .btn-export { flex: 0 0 auto; white-space: nowrap; }

/* The box anyone who is not on the friend list is typed into. The chips above
   are the answer; this is the other way of adding to them. */
.offer-username { font-size: .9rem; }

/* The word beside a label that says the field can be left alone. */
.label-hint { color: var(--text-muted); font-weight: 400; }

/* What the modal says back after you press its button. Green for the one
   message that means it worked, the danger colour for everything else, both
   from the palette rather than written out per modal. */
.modal-status {
  margin: .2rem 0 0;
  font-size: .82rem;
  font-weight: 600;
  color: var(--danger);
}
.modal-status.is-ok { color: var(--ok-deep); }

/* ── Avatar crop modal ──
   A circular viewport the size the picture actually renders at everywhere
   else in the app, so what the user frames here is what they get — not a
   preview at some other aspect ratio that object-fit: cover would then
   re-crop a second time, invisibly, to whatever the browser picks. */
.avatar-crop-viewport {
  position: relative;
  width: min(280px, 70vw);
  height: min(280px, 70vw);
  border-radius: 50%;
  overflow: hidden;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  touch-action: none;
  cursor: grab;
}
.avatar-crop-viewport.is-dragging { cursor: grabbing; }
.avatar-crop-viewport img {
  position: absolute;
  left: 0; top: 0;
  max-width: none;
  user-select: none;
  -webkit-user-drag: none;
  pointer-events: none;
}
.avatar-crop-zoom { width: min(280px, 70vw); }
.avatar-crop-hint {
  font-size: .8rem;
  color: var(--text-muted);
  text-align: center;
  margin: 0;
}

/* ── Import mode option cards ── */
.import-options {
  display: flex; flex-direction: column; gap: .5rem;
  margin-top: .85rem;
}
.import-option {
  display: flex; flex-direction: column; gap: .2rem;
  text-align: left; width: 100%;
  background: var(--bg); border: 1px solid var(--border); border-radius: .5rem;
  padding: .6rem .75rem; cursor: pointer; font-family: inherit;
  transition: background .15s, border-color .15s;
}
.import-option:hover:not(:disabled) { background: rgba(14,165,233,.1); border-color: var(--accent); }
.import-option:disabled { opacity: .55; cursor: not-allowed; }
.import-option-title {
  font-weight: 600; color: var(--text);
  display: flex; align-items: center; gap: .45rem;
}
.import-option-desc { font-size: .78rem; color: var(--text-dim); }
.import-option--danger { border-color: #991b1b; }
.import-option--danger:hover:not(:disabled) { background: #450a0a; border-color: var(--danger); }
.import-option--danger .import-option-title { color: #fca5a5; }

/* ── Offer badge ── */
.offer-badge {
  display: inline-flex; align-items: center; gap: .4rem;
  background: #fef3c7; border: 1px solid #fcd34d; color: #92400e;
  border-radius: 6px; padding: .3rem .7rem; font-size: .82rem;
  font-weight: 600; cursor: pointer; margin-left: .5rem;
}
.offer-badge:hover { background: #fde68a; }
.offer-badge-count {
  background: #d97706; color: #fff; border-radius: 50%;
  width: 1.2rem; height: 1.2rem; display: inline-flex;
  align-items: center; justify-content: center; font-size: .72rem;
}
/* ── Offer item in inbox modal ── */
.offer-item {
  padding: .75rem 1rem; border-bottom: 1px solid var(--border);
}
.offer-item:last-child { border-bottom: none; }
.offer-item-meta {
  display: flex; flex-wrap: wrap; gap: .5rem;
  align-items: center; margin-bottom: .5rem; font-size: .83rem;
}
.offer-item-from   { font-weight: 700; color: #7dd3fc; }
.offer-item-dates  { color: #7dd3fc; font-variant-numeric: tabular-nums; }
.offer-item-label  { font-style: italic; color: var(--text-dim); }
.offer-item-count  { color: var(--text-muted); }
.offer-accept-btn  { margin-bottom: .35rem; }

/* ── Import status banner ── */
.import-modal-status {
  margin-top: .9rem;
  display: flex; align-items: center; gap: .5rem;
  padding: .6rem .75rem;
  border-radius: .5rem;
  font-size: .85rem;
  font-weight: 600;
}
.import-modal-status--busy {
  background: rgba(14,165,233,.12); border: 1px solid var(--accent); color: #7dd3fc;
  animation: import-status-pulse 1.4s ease-in-out infinite;
}
.import-modal-status--error {
  background: #450a0a; border: 1px solid #991b1b; color: #fca5a5;
}
@keyframes import-status-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: .55; }
}
@keyframes my-loc-pulse {
  0%   { box-shadow: 0 0 0 3px rgba(14,165,233,.4); }
  70%  { box-shadow: 0 0 0 10px rgba(14,165,233,0); }
  100% { box-shadow: 0 0 0 3px rgba(14,165,233,0); }
}
/* Chip text sits on a ~100-level wash of its own hue, so the ink is the
   700-level of that hue: dark enough to read at .75rem, still obviously the
   chip's colour rather than a generic grey. The leading bar is currentColor,
   so it darkens with the text and keeps the day's identity at a glance.
   .day-chip-1 takes green rather than the sky --accent-hi it used to: its
   wash is green, and light sky ink on it was the one pairing you couldn't
   read at all. The calendar map's pins wear the 500-level of the same hue
   (DAY_COLORS in frontend/src/components/CalendarGrid.jsx), so keep the two
   in step. A plan day N wears .day-chip-(N-1); a calendar day its weekday's,
   Monday = 0 … Sunday = 6, so a week wears a plan's Day 1 to Day 7. */
.day-chip-0 { background:#e0f2fe; color:#0369a1; }
.day-chip-1 { background:#dcfce7; color:#15803d; }
.day-chip-2 { background:#ede9fe; color:#6d28d9; }
.day-chip-3 { background:#ffedd5; color:#b45309; }
.day-chip-4 { background:#fce7f3; color:#be123c; }
.day-chip-5 { background:#ccfbf1; color:#0f766e; }
.day-chip-6 { background:#fef9c3; color:#a16207; }
.day-chip-7 { background:#e0e7ff; color:#4338ca; }
.day-chip-8 { background:#fee2e2; color:#b91c1c; }
.day-chip-9 { background:#cffafe; color:#0e7490; }

/* active overrides the day colour */
.event-chip.active {
  background: #fde68a !important;
  color: #92400e !important;
  font-weight: 700;
}
.event-chip.chip-hovered {
  background: #bae6fd !important;
  outline: 2px solid var(--accent-hi);
}
.event-chip.chip-focused {
  outline: 2px solid var(--warn);
  outline-offset: 1px;
  box-shadow: 0 0 0 3px rgba(245,158,11,.25);
}
.chip-wrapper.drop-target-before {
  border-top: 2px solid var(--brand);
  margin-top: -2px;
}
.ri-stop.drop-target-before {
  border-top: 2px solid var(--brand);
  margin-top: -2px;
}
/* The end of the day, drawn behind the last event — the one drop position with
   no event in front of it to mark. */
.chip-wrapper.drop-target-after,
.ri-stop.drop-target-after {
  border-bottom: 2px solid var(--brand);
  margin-bottom: -2px;
}
.cal-stop-num {
  font-size: .72em;
  font-weight: 700;
  margin-right: .2em;
  opacity: .75;
}
/* Says a chip has photos behind it. Held back to the chip's own colour at
   part opacity: it is a fact about the event, not a warning about it, so it
   should be findable when you scan for it and quiet when you aren't. */
.chip-photo-icon {
  font-size: .78em;
  margin-right: .3em;
  opacity: .7;
}
/* Who last changed a synced event — see SyncEditorMark.jsx. In the chip's own
   ink on a neutral wash, so it reads on every day colour and in both themes.
   A friend's change is the news here and carries full weight; one's own is
   held back, since it only confirms what the reader already knows. */
.chip-sync-editor,
.modal-sync-editor {
  display: inline-block;
  white-space: nowrap;
  font-size: .68em;
  font-weight: 600;
  line-height: 1.3;
  padding: 0 .35em;
  margin-left: .35em;
  border-radius: 999px;
  background: rgba(127,127,127,.16);
  vertical-align: .08em;
}
.chip-sync-editor i,
.modal-sync-editor i { font-size: .9em; margin-right: .1em; }
.chip-sync-editor.is-me,
.modal-sync-editor.is-me { font-weight: 500; opacity: .7; }
.ri-stop-title .chip-sync-editor { flex: 0 0 auto; align-self: center; }
.modal-header .modal-sync-editor {
  margin: 0 .5rem 0 auto;
  font-size: .8rem;
  color: var(--text-dim);
}

/* ── The plan grid ───────────────────────────────────────────────────────────
   A trip laid out the way the calendar lays out a month: days are cells, stops
   are chips, and everything above already applies. What is left is the ways a
   plan differs from a month — a day wears the colour its pins wear on the map,
   and a stop's note and its map link are written under the chip in whichever
   column is wide enough to hold them.

   Here rather than in spa.css because two pages draw this grid: the SPA's plan
   page (PlanDayGrid) and the public share link (templates/share.html), which
   is a Flask page and only ever loads this file. They are the same itinerary
   seen by its owner and by whoever it was sent to, so a change to one has to
   be a change to both — which is what a second copy of these rules would
   quietly stop being. */

/* .calendar-table paints white behind the whole table and leaves `overflow` to
   cut the corners off, which a table doesn't honour — so a pale frame showed
   around the grid. None of that white is load-bearing here: every cell, the
   empty tail of the last row included, paints its own paper. */
.plan-grid-table { table-layout: fixed; overflow: clip; background: transparent; }

/* The one thing on the page that scrolls: the days. Each cell names itself,
   so there is no header bar above the body to align padding with. */
.plan-body { flex: 1; min-height: 0; overflow-y: auto; padding: 0 .25rem .25rem; }

/* Each day carries the colour of its pins on the map. It arrives as a custom
   property on the cell rather than an inline colour on the label, which would
   outrank the hover state below and leave the one you're pointing at
   unreadable. */
.plan-day-cell .day-number { color: var(--plan-day-color, var(--ink)); }
.plan-day-cell .day-number.day-clickable:hover { background: var(--accent); color: #fff; }
.plan-day-label { cursor: grab; }
.plan-day-label:active { cursor: grabbing; }

/* A chip — a plan's stop or the calendar's event, which are the same thing
   twice. The time rides inside it; the rest takes a line of its own below,
   which is why the wrapper (a flex row of chip and menu button) is allowed to
   wrap. The chip itself is min-width: 0, so it goes on shrinking rather than
   pushing the menu button onto a line of its own. */
.chip-time { font-weight: 700; margin-right: .25rem; opacity: .75; }
.day-cell .chip-wrapper { flex-wrap: wrap; }
/* The chip sits beside the done box, never under it. The wrapper wraps for
   the detail line's sake, and a flex row breaks lines on an item's *unflexed*
   width — which for a chip basing itself on its content is the title written
   out in full. Any title longer than a day column therefore sent the whole
   chip to a line of its own, tick box stranded above it and the title running
   flush under the box rather than beside it. Basing the chip on nothing at
   all and letting it grow into whatever the row has left keeps it on the
   first line, where its own text wraps inside its own column. */
.day-cell .chip-wrapper .event-chip { flex: 1 1 0; min-width: 0; }

/* How far a chip's own left edge sits from the cell's. Everything written
   under a chip — the note, the Google Maps link, and the travel onward from it
   — hangs off this one number, so all three begin where the chip begins.
   They each carried their own indent before, none of which was the chip's:
   .5rem for the note and link, .85rem for the travel, against a chip pushed
   over by the tick box in front of it. Three lines belonging to one stop,
   stepped in at three different depths and none of them lining up with the
   stop itself, which read as loose text in the cell rather than as the rest of
   what the chip says.

   Two values because the two grids are built differently: a calendar event
   carries a tick box ahead of its chip, a plan's stop does not, so on a plan
   the chip starts at the cell's own edge and so does everything under it. */
.day-cell               { --chip-indent: calc(.85rem + .18rem); } /* .done-box width + .chip-wrapper gap */
.day-cell.plan-day-cell { --chip-indent: 0rem; }

/* The rest of a chip: the note, and the way to the place. Only ever shown in a
   column with the room to be read across — the day you have picked, or every
   day once the grid is only a few columns wide. Elsewhere a day is a few
   characters wide and a note printed into it is unreadable rather than brief.

   min-width: 0 is what keeps a note inside its day. A flex item's min-width is
   `auto`, which floors its width at its own min-content — the longest thing in
   it that cannot be broken. One pasted URL, file path or long word in a note
   was therefore wider than the column, and the floor made the box that wide
   too: the note ran out over the day beside it and took the cell's own border
   with it. */
.chip-detail { flex: 1 0 100%; min-width: 0; padding: .1rem 0 .2rem var(--chip-indent, .5rem); }
.chip-desc {
  font-size: .72rem; line-height: 1.35; color: var(--ink-dim);
  white-space: pre-wrap;
  /* `anywhere`, not `break-word`. They break a line the same way, but only
     `anywhere` counts that break when the min-content width is measured —
     which is the number the rule above is trying to keep small. */
  overflow-wrap: anywhere;
}
/* A link written into a note is clickable where the note is read. It has to
   break mid-word: a URL is one unbreakable token and a day column is a few
   characters wide, so without this a pasted link decides the column width. */
.chip-desc a { color: var(--accent-deep); word-break: break-all; }
.chip-desc a:hover { text-decoration: underline; }
.chip-link {
  display: inline-flex; align-items: center; gap: .2rem; margin-top: .1rem;
  font-size: .7rem; color: var(--accent-deep); text-decoration: none;
  /* "Google Maps" is two words and fits, but an inline-flex box won't wrap
     between them on its own — max-width is what stops it reaching past the day
     if the column is ever narrower than the label. */
  max-width: 100%; flex-wrap: wrap;
}
.chip-link:hover { text-decoration: underline; }

/* ── A plan stop's photos, under its title ───────────────────────────────────
   The first photo, large enough to see the place by, between the stop's chip
   and its note in an open day, and between the day line and the note in its
   map popup (components/StopPhotoStrip; templates/share.html draws the same
   markup). In the day grid the rest follow as tiles of their own (--all
   below); any of them opens the stop's photos full screen. The photo's box is sized by its width alone, so a
   popup knows its height before the picture has arrived and doesn't jump as
   it does.
   The picture lies over the box rather than in it. In the box's flow, iOS
   Safari laid it out at its own size — a card-sized thumbnail several times
   the box — and the box's overflow left only its top-left corner showing. */
.stop-photo-strip { display: flex; align-items: flex-start; gap: 4px; margin: .2rem 0 .3rem; }
.stop-photo-thumb {
  position: relative; display: block; flex: 0 1 auto; width: 240px; min-width: 0; aspect-ratio: 4 / 3;
  padding: 0; border: 0; border-radius: 6px; overflow: hidden;
  background: #1e293b; cursor: zoom-in;
}
@supports not (aspect-ratio: 1) { .stop-photo-thumb { height: 180px; } }
.stop-photo-thumb img {
  position: absolute; inset: 0; display: block;
  width: 100%; height: 100%; max-width: none; object-fit: cover;
}
.stop-photo-thumb:hover img { filter: brightness(1.08); }
.stop-photo-play {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  color: #fff; font-size: 1.1rem; text-shadow: 0 1px 4px rgba(0,0,0,.7); pointer-events: none;
}
/* Every photo, in an open day of the grid (StopPhotoStrip's `all`): tiles the
   size of a lone photo, 240px, as many across as the column holds — each
   giving up a little, to no less than 180px, to fit one more in the row. The
   tiles fill their columns and the columns share out the row, so the photos sit
   side by side; the strip is held to the width its own photos need (--n of
   them, set where it is drawn), so two or three photos in a wide column stay
   near 240px together on the left rather than spread across it with bands of
   empty space between them. A column narrower than a tile has one a row, its
   full width. A stop with only one photo is drawn as above, not as a tile. */
.stop-photo-strip--all {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  max-width: calc(var(--n, 4) * 244px - 4px);
}
.stop-photo-strip--all .stop-photo-thumb { width: auto; }
@supports not (aspect-ratio: 1) { .stop-photo-strip--all .stop-photo-thumb { height: 150px; } }
/* On a phone the day is one column the width of the screen, and a 240px photo
   left a band of empty space beside it. Every photo — a lone one and each
   tile, one a row — takes the column's full width instead. Not the popup's,
   which keeps its own width on the map. */
@media (max-width: 768px) {
  .stop-photo-strip:not(.stop-photo-strip--popup) .stop-photo-thumb { flex: 1 1 auto; width: 100%; max-width: none; }
  .stop-photo-strip--all { grid-template-columns: 1fr; max-width: none; }
}
/* Across the popup's 220px. */
.stop-photo-strip--popup { margin: .1rem 0 .4rem; }
.stop-photo-strip--popup .stop-photo-thumb { flex: 1 1 0; width: auto; }

/* ── The travel written between two stops ────────────────────────────────────
   "↓ 25 min · 12 km" answers for itself, and only where it is written.
   The line was a block, so it ran the full width of the cell and most of that
   band was empty space beside the words — and pointing anywhere in it drew a
   road on the map and lit a hop you hadn't asked about. Reading down a day
   meant crossing several of those bands, each one repainting the map on the
   way past. Shrink-wrapped, the words are the target: point at them and the
   map answers, and the space either side is the day's again.

   Shrink-wrapped by width rather than by going inline: an inline-block sits
   on a text line and takes that line's leading with it, which pushed the
   chips either side of it apart and cost a day column height it hasn't got.
   A block box with a fit-content width is the same narrow target with the old
   compact stacking — no line box, nothing between the travel and the stop it
   leads to but its own small margins.

   The indent is a margin now rather than padding. Padding is inside the box
   and would have stayed part of the target, which is the one thing this is
   trying to stop. */
.ri-leg {
  display: block;
  width: -webkit-fit-content;
  width: fit-content;
  max-width: 100%;
  padding-left: 0;
  margin-left: .5rem;
}
/* Indented to the chip's left edge, like the note and the link above it — see
   --chip-indent. Still a margin rather than padding: padding is inside the box
   and would put the indent back inside the target, which is the whole point of
   the shrink-wrap above. */
.chip-leg { padding-left: 0; margin-left: var(--chip-indent, .85rem); }

/* Pointed at or held, the line says so in its own ink — orange, the colour of
   the road it puts on the map. It used to take a dark block behind it as
   well, which in a column a few characters wide read as another chip stacked
   among the stops rather than as the travel between two of them. The text
   alone carries it: there is only ever one lit hop. */
.ri-leg:hover, .ri-leg--active { color: #ea580c; }
.ri-leg--active { background: none; padding: 0; border-radius: 0; }
.chip-leg.ri-leg--active { padding-left: 0; }

/* A stop's note, its map link and the travel onward from it belong to the day
   you have opened: in a column a few characters wide "↓ 25 min · 12 km" wraps
   to three lines of grey between every pair of chips, and a note to a dozen.
   The SPA renders none of it outside the open day and so has nothing to hide;
   this is for the share page, whose grid arrives whole from the server and
   decides what to show where in CSS. The one exception is the stop you have
   picked, whose note the SPA prints in any column (--open). */
.plan-day-cell:not(.day-wide) .chip-wrapper:not(.chip-wrapper--open) .chip-detail,
.plan-day-cell:not(.day-wide) .chip-leg { display: none; }

.plan-cell-none { font-size: .72rem; color: var(--text-muted); font-style: italic; }

.plan-empty { text-align: center; padding: 3rem 1rem; color: var(--text-dim); }
.plan-empty i { font-size: 3rem; margin-bottom: .75rem; display: block; }
/* The rule above dresses the big glyph that heads the empty state. An icon
   riding inside a button in it is a different thing entirely, and at 3rem,
   stacked above its own label, it was unrecognisable as one. */
.plan-empty .btn-export { margin: .9rem auto 0; }
.plan-empty .btn-export i { font-size: inherit; margin: 0; display: inline; }

/* A stop's popup on the map. A fixed width (matching the Popup minWidth/
   maxWidth the SPA passes Leaflet) — more specific than the app-wide mobile
   ".leaflet-popup-content { width: auto !important }" rule, which otherwise
   lets long unbroken CJK text collapse the popup to one character per line. */
.plan-stop-popup .leaflet-popup-content-wrapper { border-radius: .5rem; }
.plan-stop-popup .leaflet-popup-content { width: 220px !important; margin: .7rem .9rem; line-height: 1.45; }
.plan-popup-title { font-weight: 700; color: var(--ink); font-size: .92rem; margin-bottom: .35rem; overflow-wrap: break-word; }
.plan-popup-meta { font-size: .78rem; color: var(--text-faint); margin-bottom: .3rem; }
.plan-popup-desc { font-size: .8rem; color: var(--text-muted); margin-bottom: .45rem; overflow-wrap: break-word; }
/* Same as in the grid: a popup is a fixed 220px, and a link in a note is not
   allowed to be the thing that overflows it. */
.plan-popup-desc a, .cal-popup-desc a { word-break: break-all; }
.plan-popup-link { display: inline-flex; align-items: center; gap: .3rem; font-size: .78rem; color: var(--accent); text-decoration: none; }
.plan-popup-link:hover { text-decoration: underline; }

/* ── Leaflet map surface ──
   Every map in the app is a panel like any other and takes the same corner as
   one. This was previously up to whoever placed the map: the three pickers in
   modals each drew their own wrapper with a 6px or .4rem radius inline, and
   every full-size map — the World Map, My Events, the calendar's side pane,
   the plan map — was left square, so a map was the only square-cornered
   surface on a page of rounded ones.

   The radius goes on .leaflet-container itself rather than on a wrapper so it
   reaches all of them at once, including maps the templates create directly.
   Leaflet already sets overflow:hidden there, so this clips the tile pane with
   nothing else to do — and it fixes the plan stop picker, whose wrapper had a
   radius but no clipping, leaving square tiles painted over a rounded border.

   Controls sit 10px inside the container by Leaflet's own margins and so clear
   the curve; the attribution is flush to the bottom-right and simply follows
   it. */
.leaflet-container { border-radius: var(--r-lg); }

/* A map in a modal is a small inset field — 140-180px tall — not a page
   surface: --r-lg on one of those reads as a lozenge. --r-sm is also what the
   hand-rolled wrappers around them already use, so nothing shifts. */
.modal-overlay .leaflet-container,
.plan-modal-overlay .leaflet-container { border-radius: var(--r-sm); }

/* ── Leaflet popup ── */
.lf-popup { font-family: "Twemoji Country Flags", "Segoe UI", system-ui, sans-serif; font-size: .88rem; line-height: 1.5; }
.lf-popup-actions { display: flex; align-items: center; justify-content: space-between; margin-top: .35rem; gap: .5rem; }
.lf-popup-link { color: var(--accent); font-weight: 600; }
.lf-popup-link:hover { text-decoration: underline; }
.lf-delete-btn {
  background: none; border: none; cursor: pointer;
  color: var(--danger); font-size: .82rem; font-weight: 600;
  padding: 0; font-family: inherit;
}
.lf-delete-btn:hover { text-decoration: underline; }
.lf-edit-btn {
  background: none; border: none; cursor: pointer;
  color: var(--accent); font-size: .82rem; font-weight: 600;
  padding: 0; font-family: inherit;
}
.lf-edit-btn:hover { text-decoration: underline; }
.lf-tooltip { font-family: "Twemoji Country Flags", "Segoe UI", system-ui, sans-serif; font-size: .82rem; line-height: 1.5; }

/* ── Pending new-event pin (blue, pulsing) ── */
.pending-pin {
  color: var(--accent);
  font-size: 1.9rem;
  filter: drop-shadow(0 2px 4px rgba(14,165,233,.6));
  animation: pin-pulse 1.2s ease-in-out infinite;
  line-height: 1;
}
@keyframes pin-pulse {
  0%, 100% { transform: scale(1);   opacity: 1; }
  50%       { transform: scale(1.2); opacity: .8; }
}

/* ── Stop dot ── */
/* Sized inline per state (see makeCalIcon); the ring and shadow are what lift
   it off the tiles, and it keeps its own place in the badge stack below. */
.pin-wrap--dot { display: block; }
/* Square, not round. A circle is the shape every other map draws — the user's
   own location dot, the place markers, the photo pins — so a stop had to be
   told apart from those by colour alone. A square reads as a different kind of
   thing at a glance and at any size, while the white ring around it stays the
   same and keeps it off dark tiles.

   The corners are taken off, though: a hard square is a sharper thing than
   anything else on these tiles, and at 12px the points were the part of it you
   saw first. In percent rather than px because the dot is sized inline from
   12px up to 26px, and a fixed radius that softens the small ones squares off
   the large. This much still reads as a square next to a circle. */
.pin-dot {
  display: block;
  width: 100%; height: 100%;
  box-sizing: border-box;
  border-radius: 30%;
  border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.45);
}
/* The stop in hand — picked or hovered — turns inside out: a red ring around a
   white centre. A shape change as well as a colour one, so it is still the
   obvious one on the map at a glance, and the white halo replaces the outer
   ring the filled dots wear to hold up against dark tiles. */
.pin-dot--ring {
  background: #fff;
  border-width: 7px;
  box-shadow: 0 0 0 1px rgba(255,255,255,.9), 0 1px 3px rgba(0,0,0,.45);
}

/* ── Pin stop-number badge ── */
.pin-wrap {
  position: relative;
  display: inline-block;
  line-height: 1;
  transform-origin: 50% 100%;
  transition: color .2s, font-size .2s, transform .15s ease;
}
/* Hovering the matching event chip in the calendar enlarges its pin and turns it red */
.leaflet-marker-icon.pin-hover .pin-wrap {
  transform: scale(1.35);
  color: #ef4444 !important;
}
.pin-num {
  position: absolute;
  top: -5px; right: -7px;
  background: #fff;
  color: var(--ink);
  border-radius: 50%;
  width: 14px; height: 14px;
  font-size: .6rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 3px rgba(0,0,0,.35);
  pointer-events: none;
}

/* ── Ticked-off stop ── */
/* The pin stays at full strength — a tick is a note on the stop, not a reason
   to make it harder to see. The green badge alone says it is done. */
/* A teardrop had a wide head to hang badges off; a dot is smaller than the
   badges themselves, so a corner offset still buries the colour that says
   which day the stop belongs to. Stack them clear of it instead — number
   above, tick below — which also holds at every dot size. */
.pin-wrap--dot .pin-num,
.pin-wrap--dot .pin-done {
  left: 50%;
  right: auto;
  transform: translateX(-50%);
}
.pin-wrap--dot .pin-num  { top: auto; bottom: calc(100% + 3px); }
.pin-wrap--dot .pin-done { top: calc(100% + 3px); }

.pin-done {
  position: absolute;
  top: -5px; left: -7px;
  background: var(--ok-deep, #16a34a);
  color: #fff;
  border-radius: 50%;
  width: 14px; height: 14px;
  font-size: .55rem;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,.35);
  pointer-events: none;
}

/* ── Route midpoint badge ── */
.route-badge {
  background: rgba(15,23,42,.93);
  border: 1px solid var(--border);
  border-radius: .25rem;
  padding: .1rem .35rem;
  font-size: .68rem;
  font-weight: 600;
  white-space: nowrap;
  box-shadow: 0 1px 3px rgba(0,0,0,.4);
  text-align: center;
  line-height: 1.4;
  color: var(--text);
  pointer-events: none;
}

/* ── Hamburger (hidden on desktop) ── */
.nav-hamburger {
  display: none;
  background: none; border: none; color: var(--text);
  font-size: 1.3rem; cursor: pointer;
  padding: .3rem .5rem; border-radius: .3rem; line-height: 1;
  flex-shrink: 0;
}
.nav-hamburger:hover { background: var(--surface-hi); }

/* Phone-only avatar beside the hamburger — see Navbar.jsx. Off on desktop,
   where the Account row (icon, username and this same picture) is already
   visible in the nav-links row instead. */
.nav-avatar-mobile { display: none; flex-shrink: 0; }
.nav-avatar-mobile img {
  display: block; width: 28px; height: 28px;
  border-radius: 50%; object-fit: cover;
}

/* Dark/light switch — lives among the nav links (see Navbar.jsx / base.html)
   so it collapses into the hamburger menu on a phone the same as any other
   row. Both icons are always in the markup; which one shows is decided
   here, purely by the data-theme attribute set on <html>, rather than by
   any script re-rendering the button — theme.js only ever has to touch the
   attribute and this follows for free, in either shell. */
.theme-toggle-btn {
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  font-size: 1rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
  padding: .3rem .6rem; border-radius: var(--r-sm);
  transition: color var(--base) var(--ease), background var(--base) var(--ease);
}
.theme-toggle-btn:hover { color: var(--text); background: var(--surface-hi); }
/* Scoped under .theme-toggle-btn rather than bare .theme-toggle-icon--light:
   Font Awesome's own stylesheet sets `.fa { display: ... }`, at the same
   single-class specificity as an unscoped rule here, and it loads after
   this sheet in base.html — so on that tie it was winning and undoing the
   display:none below, leaving both the moon and the sun showing at once in
   the default (dark) theme. The extra ancestor class puts these rules a
   notch above that tie for good, regardless of load order. */
.theme-toggle-btn .theme-toggle-icon--light { display: none; }
:root[data-theme="light"] .theme-toggle-btn .theme-toggle-icon--dark { display: none; }
:root[data-theme="light"] .theme-toggle-btn .theme-toggle-icon--light { display: inline; }

/* Language picker — lives right next to the theme toggle among the nav links
   (see LanguageSwitcher.jsx / base.html) for the same reason: a plain
   <select> rather than a custom dropdown, so it collapses into the phone's
   stacked hamburger menu with no extra layout work. */
.lang-switch {
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  font-size: .85rem; line-height: 1; max-width: 6.5rem;
  padding: .3rem .4rem; border-radius: var(--r-sm);
  transition: color var(--base) var(--ease), background var(--base) var(--ease);
}
.lang-switch:hover, .lang-switch:focus { color: var(--text); background: var(--surface-hi); }
.lang-switch option { color: initial; background: initial; }

/* Text size picker — same idea as the language picker right above it: a
   plain <select> next to it in the nav (see FontSizeSwitcher.jsx / base.html)
   rather than a custom control, so it collapses into the phone's stacked
   hamburger menu for free. Scales --font-scale on :root below, which is what
   actually changes text size app-wide. */
.font-size-switch {
  background: none; border: none; color: var(--text-dim); cursor: pointer;
  font-size: .85rem; line-height: 1; max-width: 6.5rem;
  padding: .3rem .4rem; border-radius: var(--r-sm);
  transition: color var(--base) var(--ease), background var(--base) var(--ease);
}
.font-size-switch:hover, .font-size-switch:focus { color: var(--text); background: var(--surface-hi); }
.font-size-switch option { color: initial; background: initial; }

/* Wraps the theme toggle + language + text size pickers (see Navbar.jsx /
   base.html). display:contents here means the wrapper is invisible to
   layout on desktop — .theme-toggle-btn, .lang-switch and .font-size-switch
   stay direct flex items of .nav-links, exactly as if this div weren't
   there. The phone breakpoint below turns it into an actual row, which is
   the only place the grouping matters. */
.nav-utility-row { display: contents; }

/* Desktop only: the theme toggle, language picker and text size picker sit
   between the Account row and Log Out, rather than at the front of the
   nav-links row where their DOM position (ahead of World Map) would
   otherwise put them. .nav-utility-row is display:contents here, so its
   children are already direct flex items of .nav-links (see above) and can
   be reordered with plain flex `order` — everything else keeps its default
   order 0, i.e. its source position, so this only has to number the items
   that must land after it. On the phone breakpoint below, .nav-utility-row
   becomes a real flex row and these child orders no longer move it within
   the stacked menu, so the phone layout is unaffected. */
@media (min-width: 769px) {
  .theme-toggle-btn { order: 2; }
  .nav-utility-row .lang-switch { order: 3; }
  .nav-utility-row .font-size-switch { order: 4; }
  .navbar .nav-links .nav-guest-login { order: 5; }
  /* The SPA's logout button is a direct nav-links child, but base.html's
     server-rendered navbar wraps the same button in a <form> so it posts —
     that form, not the button, is the actual flex item, so the order has to
     land there for the two navbars to match. */
  .navbar .nav-links .btn-nav,
  .navbar .nav-links .nav-logout-form { order: 6; }
}

/* ── Responsive ── */
@media (max-width: 768px) {
  /* Navbar: hamburger + collapsible menu */
  .navbar {
    flex-wrap: wrap;
    padding: .3rem 1rem;
    padding-top: calc(.3rem + var(--safe-top));
    padding-left: calc(1rem + var(--safe-left));
    padding-right: calc(1rem + var(--safe-right));
    gap: 0;
    /* position: sticky + z-index are already set on the base .navbar rule
       above, which now applies at every width, not just this one. */
  }
  /* A slimmer bar on the phone, where every pixel of it is a pixel less of
     the page. The logo's 32px is an inline style in both shells (Navbar.jsx,
     base.html), hence the !important to bring it down here. */
  .navbar .brand { flex: 1; font-size: 1.05rem; }
  .navbar .brand img { height: 24px !important; margin-right: 5px !important; }
  .nav-avatar-mobile { display: flex; margin-right: .5rem; }
  .nav-avatar-mobile img { width: 24px; height: 24px; }
  .nav-hamburger { display: block; font-size: 1.1rem; }
  .nav-links {
    display: flex;
    flex-direction: column;
    width: 100%;
    gap: .55rem;
    /* No margin-top while collapsed: the closed menu is zero height, but a
       margin on it still pushed the bottom of the bar down by that much. */
    margin-top: 0;
    /* collapsed state — no border so nothing bleeds through */
    border-top: none;
    max-height: 0;
    overflow: hidden;
    opacity: 0;
    padding: 0;
    transition: max-height .35s ease, opacity .25s ease, padding .35s ease;
  }
  /* Theme toggle + language picker + text size picker share one row here
     instead of each taking its own full-width stacked row like the links
     below them — they sit centered as one group, rather than each underneath
     the last or pinned to opposite edges. flex-wrap is the fallback for a
     narrow enough phone that all three plus their padding don't fit one row. */
  .nav-utility-row {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .3rem;
    width: 100%;
  }
  .nav-utility-row .lang-switch,
  .nav-utility-row .font-size-switch { max-width: 7rem; }
  .navbar.nav-open .nav-links {
    border-top: 1px solid var(--border);
    margin-top: .3rem;
    max-height: 600px;
    opacity: 1;
    padding: .6rem 0 .25rem;
  }
  /* Close menu when a link inside is tapped */
  .nav-links a,
  .nav-links button.btn-nav-link { width: 100%; }
  .nav-links form { display: block; width: 100%; }
  .nav-links .btn-nav { width: 100%; justify-content: center; }

  /* Stacked, the rows are read down a column, so the words have to start at
     one x — and left-aligning the elements alone does not get that, because
     the glyphs are different widths: a globe against a route against a
     speech bubble is a couple of pixels each time, and every label steps in
     and out by that much. Two columns instead: a fixed-width cell the icon
     is centred in, then the label. One padding across all of them too — the
     Chat row is a <button> whose own rule sets padding to 0, which starts it
     a further .6rem left of every link beside it. Sign Up keeps its own
     geometry; it is a button in the menu, not a row of it. */
  .navbar .nav-links a:not(.btn-primary),
  .navbar .nav-links .nav-username,
  .navbar .nav-links button.btn-nav-link {
    display: flex;
    align-items: center;
    gap: .5rem;
    box-sizing: border-box;
    padding: .3rem .6rem;
    /* .btn-nav-link sets its own smaller font-size (used elsewhere) —
       stacked in this column that left Friends, Chat and Account reading
       noticeably smaller than World Map, My Events, Plan and Calendar
       above them, which threw off the whole column's alignment even with
       the icons themselves lined up. */
    font-size: 1rem;
  }
  .navbar .nav-links a:not(.btn-primary) > i,
  .navbar .nav-links .nav-username > i,
  .navbar .nav-links button.btn-nav-link > i {
    flex: none;
    width: 1.25em;
    text-align: center;
  }

  /* Layout */
  .split-layout { grid-template-columns: 1fr; gap: 1rem; }
  .map-pane { position: static; display: none; }
  #show-cal-map-btn { display: flex !important; }
  .container { padding: 0 .75rem; margin: 1rem auto; }
  /* Matches the smaller container margins above. */
  .report-queue { height: calc(100dvh - 5rem - var(--safe-top)); }

  /* Calendar */
  .calendar-header { gap: .6rem; flex-wrap: wrap; justify-content: center; }
  .cal-weekday-header span { padding: .35rem .15rem; font-size: .72rem; }
  .day-cell { min-height: 56px; padding: .25rem; }
  .day-number { font-size: .78rem; }
  .event-chip { font-size: .68rem; padding: .1rem .3rem; }
  .route-info { font-size: .68rem; }

  /* Toolbar — bars side by side, each folds vertically */
  .toolbar-row { gap: .35rem; flex-direction: row; align-items: flex-start; }
  .import-bar, .export-bar {
    flex: 1; flex-direction: column; align-items: flex-start;
    flex-wrap: nowrap; padding: .3rem .4rem; gap: .3rem;
  }
  .bar-content { flex-wrap: wrap; gap: .3rem; width: 100%; }
  .bar-label { cursor: pointer; user-select: none; width: 100%; flex-shrink: 0; justify-content: space-between; }
  .bar-fold-icon { display: inline-block; }

  /* Tighter on a phone, where the same line carries six or seven controls
     across 390px. Only the numbers change — the layout above is the layout at
     every width. */
  .toolbelt {
    --ctl-t: .82rem;
    --ctl-h: 2rem;
    --ctl-gap: .35rem;
  }
  /* And narrower boxes around the same type. The heights stay where they are —
     these are still thumb targets — but the side padding set for a desktop is
     width the calendar header cannot spare, and a couple of pixels either side
     of five controls is the difference between the arrows, the three pickers
     and the Actions button sharing one line and one of them being pushed onto
     a line of its own. (Today used to be the sixth of them; on this width it
     is a row in the Actions menu.) */
  .toolbelt .cal-select { padding: .25rem .35rem; }
  .toolbelt .btn-nav    { padding: .3rem .45rem; }
  .import-bar.bar-folded .bar-content,
  .export-bar.bar-folded .bar-content { display: none; }
  .import-bar.bar-folded .bar-fold-icon,
  .export-bar.bar-folded .bar-fold-icon { transform: rotate(-90deg); }

  /* Leaflet popups: wider, height-capped, scrollable on small screens */
  .leaflet-popup-content-wrapper { max-width: min(88vw, 300px) !important; }
  .leaflet-popup-content {
    width: auto !important;
    max-height: 42vh;
    overflow-y: auto;
    margin: 10px 12px !important;
  }
  .seg-mini-popup .leaflet-popup-content { min-width: 0; }
  .lf-popup-actions { flex-wrap: wrap; }
  /* A stop's popup keeps a width of its own — see .plan-stop-popup above — so
     it has to be given the phone's rather than inheriting the auto width the
     rule above hands everything else. */
  .plan-stop-popup .leaflet-popup-content { width: min(200px, 68vw) !important; max-height: 40vh; overflow-y: auto; }

  /* Calendar header — Today keeps the nav's row while there is room for it,
     and takes a row of its own when there isn't.

     It is the last control on the line and .cal-pane clips what overflows, so
     a header wider than the phone loses Today off the right edge with no way
     to scroll across to it. Held nowrap that is exactly what happened once the
     span picker joined the month and year: three selects, two arrows and a
     button is more than a 390px screen holds in one row, and a long month name
     is enough to push the button past the edge. Wrapping costs a row of height
     in the cases that used to lose the button altogether — and the tighter
     control padding below buys back the width that keeps those cases rare.

     Mostly a backstop now. The pickers give width back before it comes to this
     — see the shrink floors under "Fitting the toolbelt to the calendar" — so
     the header shrinks where it used to break. */
  .calendar-header { gap: .4rem; flex-wrap: wrap; justify-content: flex-start; }
  .cal-picker-group { gap: .3rem; }
  .cal-select { font-size: .88rem; padding: .25rem .35rem; }
  .btn-today { font-size: .75rem; padding: .25rem .45rem; white-space: nowrap; }

  /* Stretch the grid's rows to fill calendar-scroll-body's full height (which
     itself flexes to the screen bottom — see .cal-page-layout in spa.css) so
     the last row's bottom edge is flush with the screen bottom instead of
     leaving a gap when the month has few weeks. If the rows' own min-height
     needs more room than that, the table just overflows and scrolls as usual. */
  .calendar-table { height: 100%; }

  /* Forms */
  .form-row { grid-template-columns: 1fr; gap: .75rem; }
  /* …except a row whose fields are narrow whatever the screen is. A date and
     a time are both a fixed handful of characters, so stacking them bought
     nothing and pushed everything below them a field further down. minmax(0,…)
     rather than 1fr because a grid item's automatic minimum is its content,
     which lets an input overflow its column instead of shrinking inside it. */
  .form-row--compact { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .6rem; }
  .event-form { padding: 1.1rem; max-width: 100%; }
  .auth-form { margin: .75rem auto; max-width: 100%; }
  .form-actions { flex-wrap: wrap; gap: .5rem; }
  .form-actions > * { flex: 1 1 auto; justify-content: center; text-align: center; }

  /* Page headers */
  .page-header { gap: .5rem; margin-bottom: 1rem; }
  .page-header h2 { font-size: 1.15rem; }
  .detail-header { flex-direction: column; align-items: flex-start; gap: .5rem; }
  .detail-actions { flex-wrap: wrap; gap: .4rem; }

  /* Lightbox: hide prev/next arrows on mobile (swipe instead) */
  .photo-lb-nav { display: none !important; }

  /* Admin settings cards */
  .admin-settings-grid { grid-template-columns: 1fr; gap: .75rem; }

  /* Admin accounts toolbar/filter */
  .admin-accounts-toolbar { flex-direction: column; align-items: stretch; }
  .admin-accounts-actions { flex-direction: column; }
  .admin-accounts-actions > * { width: 100%; }
  .admin-accounts-actions .btn-inline { width: 100%; justify-content: center; }
  .admin-filter-row { flex-direction: column; align-items: stretch; }
  .admin-filter-search { min-width: 0; }

  /* Accounts table -> stacked cards, collapsed to just the username until
     tapped open. The list wrapper (border/radius live on the container, see
     .admin-list-scroll-container above) keeps a fixed height like the
     desktop table, so it scrolls in place instead of growing the whole
     page. */
  .admin-table, .admin-table thead, .admin-table tbody, .admin-table th, .admin-table td, .admin-table tr {
    display: block;
  }
  .admin-table thead { position: static; }
  .admin-table thead tr { position: absolute; top: -9999px; left: -9999px; }
  .admin-table tbody tr {
    background: var(--surface); border-bottom: 1px solid var(--border);
    padding: 0 .75rem;
  }
  .admin-table tbody tr:last-child { border-bottom: none; }
  .admin-table td {
    display: flex; align-items: center; justify-content: space-between; gap: .75rem;
    padding: .45rem 0; font-size: .85rem; border-bottom: 1px dashed var(--border); text-align: right;
  }
  .admin-table td:last-child { border-bottom: none; }
  .admin-table td::before {
    content: attr(data-label); font-weight: 600; color: var(--text-dim);
    text-align: left; margin-right: auto;
  }
  .admin-actions-cell { justify-content: flex-end; }
  .admin-actions-cell::before { display: none; }

  /* Collapsed by default: only the username row shows, everything else
     (role, account type, status, counts, actions) is folded away until
     the username row is tapped. */
  .admin-table td:not(.admin-username-cell) { display: none; }
  .admin-table tr.expanded td:not(.admin-username-cell) { display: flex; }
  .admin-username-cell {
    cursor: pointer; user-select: none;
    font-weight: 700; color: var(--text-strong); font-size: .9rem;
    padding: .65rem 0; border-bottom: none !important;
  }
  .admin-username-cell::before { display: none; content: none; }
  .admin-table tr.expanded .admin-username-cell {
    border-bottom: 1px dashed var(--border) !important;
  }

  /* A phone has no room for a tall plot. Only the height gives: the lines
     span whatever width there is, so nothing has to scroll sideways. */
  .login-plot { --plot-h: 145px; }
  .login-plot-yaxis { width: 1.7rem; }
  /* Half as many date labels: what fits a card fits a phone at half the
     width. Counted from the end so the newest day keeps its label, which is
     the one that says what the range runs up to. */
  .login-plot-xtick:nth-last-child(even) { display: none; }
  .chart-stat { flex: 1 1 5.5rem; }

  /* Custom scroll slider replaces the native scrollbar on mobile, since
     touch browsers auto-hide native scrollbars except during an active
     scroll gesture. */
  .admin-table-wrap { scrollbar-width: none; padding-right: 10px; }
  .admin-table-wrap::-webkit-scrollbar { display: none; width: 0; }
  .admin-scroll-track {
    position: absolute; top: 0; bottom: 0; right: 2px; width: 4px;
    background: rgba(148, 163, 184, .15); border-radius: 2px; z-index: 2;
  }
  .admin-scroll-thumb {
    position: absolute; left: 0; width: 100%; border-radius: 2px;
    background: var(--text-muted);
  }

  /* Admin charts */
  .admin-chart-header { flex-direction: column; align-items: flex-start; gap: .4rem; }

  /* Modal */
  .modal-box { width: 96vw; max-height: 88vh; }
  .modal-footer { flex-wrap: wrap; gap: .4rem; }
  /* Buttons keep their own size here. Stretching them to fill the row made
     the destructive one — widest label, furthest left — the biggest thing in
     the dialog, and left the pair that actually finishes the job floating in
     the middle of its share of the row rather than hard right where the eye
     ends up. They are comfortably tappable at their own size; wrapping still
     catches the case where they genuinely can't share a line. */
  .modal-footer > * { flex: 0 1 auto; min-width: 0; }
  .modal-date-row { flex-direction: column; align-items: flex-start; }
  /* Stacked, the flex basis above would be read as a height and the grow would
     stretch the boxes down the column — so the fields go back to being sized
     by width alone, one per line. */
  .modal-date-row input[type="text"] { flex: 0 0 auto; width: 100%; }

  /* Chat panel */
  .chat-panel {
    top: auto; bottom: 0;
    left: 0; right: 0;
    width: 100%;
    max-height: 55vh;
    border-radius: .5rem .5rem 0 0;
  }
  .chat-panel::before, .chat-panel::after { display: none; }
  .chat-window { width: 100% !important; right: 0 !important; left: 0 !important; }

  /* Undo toast */
  .undo-toast { left: .5rem; right: .5rem; bottom: .5rem; }

  /* Map filter */
  .map-filter-form { gap: .4rem; }
}

/* ── Chat widget ──────────────────────────────────────────────────────── */
.chat-panel {
  position: fixed; top: calc(56px + var(--safe-top)); right: calc(1rem + var(--safe-right));
  width: 260px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: .5rem;
  box-shadow: 0 8px 24px rgba(0,0,0,.45);
  display: flex; flex-direction: column;
  z-index: 8000;
  max-height: 420px;
}
.chat-panel::before {
  content: '';
  position: absolute; top: -8px; right: 1.5rem;
  border-left: 8px solid transparent;
  border-right: 8px solid transparent;
  border-bottom: 8px solid var(--border);
}
.chat-panel::after {
  content: '';
  position: absolute; top: -6px; right: calc(1.5rem + 2px);
  border-left: 6px solid transparent;
  border-right: 6px solid transparent;
  border-bottom: 6px solid var(--border-subtle);
}
.chat-panel-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: .55rem .75rem;
  background: #111827;
  border-radius: .5rem .5rem 0 0;
  font-weight: 700; font-size: .9rem; color: #fff;
}
.chat-close-btn {
  background: none; border: none; color: inherit;
  cursor: pointer; font-size: 1rem; line-height: 1; padding: 0 .2rem;
  opacity: .8;
}
.chat-close-btn:hover { opacity: 1; }
.chat-contacts {
  overflow-y: auto; flex: 1;
  padding: .35rem 0;
}
.chat-contact-item {
  display: flex; align-items: center; justify-content: space-between;
  padding: .45rem .85rem;
  cursor: pointer; font-size: .88rem;
  transition: background .15s;
}
.chat-contact-item:hover { background: var(--border); }
.chat-contact-name { display: flex; align-items: center; gap: .4rem; }
.chat-unread-badge {
  background: var(--danger); color: #fff;
  border-radius: 9px; padding: 0 .45rem;
  font-size: .7rem; font-weight: 700;
  min-width: 1.2rem; text-align: center;
}
.chat-loading, .chat-no-friends {
  padding: .75rem 1rem; font-size: .82rem; color: var(--text-dim); text-align: center;
}
/* Search bar inside chat panel */
.chat-search-wrap {
  position: relative; padding: .4rem .5rem;
  border-bottom: 1px solid var(--border);
}
.chat-search-icon {
  position: absolute; left: .9rem; top: 50%; transform: translateY(-50%);
  color: var(--text-muted); font-size: .78rem; pointer-events: none;
}
.chat-search-input {
  width: 100%; padding: .35rem .6rem .35rem 1.8rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: .35rem;
  color: var(--text); font-size: .82rem; font-family: inherit;
}
.chat-search-input:focus { outline: none; border-color: var(--accent); }
.chat-search-results {
  border-bottom: 1px solid var(--border); overflow-y: auto; max-height: 220px;
}
/* Blocked tag in search results */
.chat-blocked-tag {
  font-size: .68rem; display: flex; align-items: center; gap: .25rem;
  background: #7f1d1d; border: 1px solid #b91c1c; color: #fca5a5;
  border-radius: .25rem; padding: .1rem .35rem; flex-shrink: 0;
}
/* Block button in window header — always spells out the current state */
.chat-block-btn {
  display: flex; align-items: center; gap: .25rem;
  background: none; border: 1px solid var(--border); color: var(--text-dim);
  cursor: pointer; font-size: .7rem; padding: .15rem .4rem;
  line-height: 1.3; border-radius: .25rem; white-space: nowrap;
  transition: color .15s, background .15s, border-color .15s;
}
.chat-block-btn:hover { background: var(--border); color: #f97316; border-color: var(--border-strong); }
/* Blocked state: red pill so it reads as "on", not as an action */
.chat-block-btn.is-blocked {
  background: #7f1d1d; border-color: #b91c1c; color: #fca5a5;
}
.chat-block-btn.is-blocked:hover { background: #991b1b; color: #fecaca; }
/* Banner under the header stating the block status in full */
.chat-block-banner {
  padding: .3rem .65rem;
  background: #451a03; border-bottom: 1px solid #b91c1c;
  color: #fdba74; font-size: .72rem; text-align: center;
}
/* Error/notice row inside chat messages */
.chat-notice {
  font-size: .75rem; color: #f97316; text-align: center;
  padding: .25rem .5rem; font-style: italic;
}

/* Individual chat windows */
#chat-windows-container {
  position: fixed; bottom: var(--safe-bottom);
  display: flex; gap: 8px;
  z-index: 7999;
  pointer-events: none;
}
.chat-window {
  pointer-events: all;
  position: fixed; bottom: var(--safe-bottom);
  width: 280px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-bottom: none;
  border-radius: .5rem .5rem 0 0;
  box-shadow: 0 -2px 16px rgba(0,0,0,.35);
  display: flex; flex-direction: column;
  max-height: 400px;
  z-index: 7999;
}
.chat-window-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: .5rem .75rem;
  background: #111827;
  border-radius: .5rem .5rem 0 0;
  font-weight: 600; font-size: .85rem; color: var(--text-dim);
  cursor: pointer;
}
.chat-window-title {
  flex: 1; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.chat-messages {
  flex: 1; overflow-y: auto;
  padding: .5rem .65rem;
  display: flex; flex-direction: column; gap: .4rem;
  min-height: 120px;
}
.chat-bubble {
  max-width: 85%;
  display: flex; flex-direction: column; gap: .15rem;
}
.chat-bubble-mine    { align-self: flex-end; align-items: flex-end; }
.chat-bubble-theirs  { align-self: flex-start; align-items: flex-start; }
.chat-bubble-text {
  padding: .35rem .65rem;
  border-radius: 1rem;
  font-size: .84rem;
  word-break: break-word;
  white-space: pre-wrap;
}
.chat-bubble-mine .chat-bubble-text   { background: var(--accent); color: #fff; border-bottom-right-radius: .25rem; }
.chat-bubble-theirs .chat-bubble-text { background: var(--border); color: var(--text); border-bottom-left-radius: .25rem; }
.chat-bubble-text a { color: inherit; text-decoration: underline; word-break: break-all; }
.chat-bubble-time { font-size: .68rem; color: var(--text-muted); }
.chat-no-msgs { font-size: .8rem; color: var(--text-dim); text-align: center; padding: .5rem; }
/* A photo bubble carries no background of its own — the picture is the
   bubble — so only the wrapping link (which clips it to the same rounded
   corners as a text bubble) gets styled. */
.chat-bubble-image-link {
  display: block; line-height: 0;
  padding: 0; border: none; background: none;
}
.chat-bubble-image {
  max-width: 100%; max-height: 220px;
  border-radius: .75rem;
  display: block;
  cursor: zoom-in;
}
.chat-bubble-photo.chat-bubble-mine   .chat-bubble-image { border-bottom-right-radius: .25rem; }
.chat-bubble-photo.chat-bubble-theirs .chat-bubble-image { border-bottom-left-radius: .25rem; }
.chat-input-row {
  display: flex; gap: .4rem; align-items: center;
  padding: .4rem .55rem;
  border-top: 1px solid var(--border);
}
.chat-input {
  flex: 1; min-width: 0;
  padding: .35rem .6rem;
  border: 1px solid var(--border);
  border-radius: 1rem;
  font-size: .84rem; font-family: inherit;
  background: var(--bg); color: var(--text);
}
.chat-input:focus { outline: none; border-color: var(--accent); }
.chat-send-btn {
  padding: .3rem .6rem; border-radius: 1rem; font-size: .8rem;
}
.chat-attach-btn {
  flex-shrink: 0;
  width: 1.9rem; height: 1.9rem;
  display: flex; align-items: center; justify-content: center;
  border: 1px solid var(--border); border-radius: 50%;
  background: none; color: var(--text-muted);
  cursor: pointer; font-size: .85rem;
  transition: background .15s, color .15s;
}
.chat-attach-btn:hover   { background: var(--border); color: var(--text); }
.chat-attach-btn:disabled { opacity: .5; cursor: default; }

/* ── Group conversations ──────────────────────────────────────────────────
   A group is the same window with more than one person on the other side of
   it, so everything below is an addition to the .chat-* rules above rather
   than a second set of them. */

/* "New group" sits beside Close in the panel header, which was a single
   button until now. */
.chat-panel-actions { display: flex; align-items: center; gap: .35rem; flex-shrink: 0; }
.chat-newgroup-btn {
  display: flex; align-items: center; gap: .15rem;
  background: none; border: 1px solid rgba(255,255,255,.25); color: inherit;
  cursor: pointer; font-size: .7rem; line-height: 1.3;
  padding: .12rem .35rem; border-radius: .25rem;
  transition: background .15s, border-color .15s;
}
.chat-newgroup-btn:hover { background: rgba(255,255,255,.15); border-color: rgba(255,255,255,.5); }
.chat-newgroup-btn .fa-plus { font-size: .6rem; }

/* Groups and people are one scrolling list with a heading over each half —
   two lists would have meant two scrollbars in a 260px panel. */
.chat-section-label {
  padding: .3rem .85rem .15rem;
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
}
/* How many people are in it, next to the name. */
.chat-group-count {
  font-size: .68rem; color: var(--text-muted);
  background: var(--border); border-radius: 8px;
  padding: 0 .35rem; margin-left: .1rem;
}

/* Naming a new group, and renaming one. */
.chat-group-name-input {
  width: 100%; padding: .35rem .6rem; margin-bottom: .6rem;
  background: var(--bg); border: 1px solid var(--border); border-radius: .35rem;
  color: var(--text); font-size: .85rem; font-family: inherit;
}
.chat-group-name-input:focus { outline: none; border-color: var(--accent); }
.chat-group-pick-label {
  font-size: .78rem; font-weight: 600; color: var(--text-dim); margin-bottom: .3rem;
}
/* Capped and scrollable: a long friends list must not push the buttons off
   the bottom of a phone screen. */
.chat-group-picker {
  max-height: 190px; overflow-y: auto;
  border: 1px solid var(--border); border-radius: .35rem;
  padding: .3rem; margin-bottom: .5rem;
}
.chat-group-pick {
  display: flex; align-items: center; gap: .45rem;
  padding: .22rem .3rem; font-size: .84rem; cursor: pointer; border-radius: .25rem;
}
.chat-group-pick:hover { background: var(--border); }

/* The members drawer, opened from the window header. */
.chat-group-info {
  border-bottom: 1px solid var(--border);
  background: var(--bg);
  padding: .45rem .65rem;
  max-height: 210px; overflow-y: auto;
  font-size: .8rem;
}
.chat-group-info-btn {
  display: flex; align-items: center; gap: .25rem;
  background: none; border: 1px solid var(--border); color: var(--text-dim);
  cursor: pointer; font-size: .7rem; padding: .15rem .4rem;
  line-height: 1.3; border-radius: .25rem; white-space: nowrap;
  transition: color .15s, background .15s, border-color .15s;
}
.chat-group-info-btn:hover,
.chat-group-info-btn.is-open { background: var(--border); color: var(--text); border-color: var(--border-strong); }
.chat-group-info-btn .chat-group-count { background: none; padding: 0; margin: 0; }
.chat-group-section {
  font-size: .68rem; font-weight: 700; letter-spacing: .04em;
  text-transform: uppercase; color: var(--text-muted);
  margin: .35rem 0 .2rem;
}
.chat-group-members { list-style: none; margin: 0; padding: 0; }
.chat-group-member {
  display: flex; align-items: center; justify-content: space-between;
  gap: .4rem; padding: .18rem 0;
}
.chat-group-owner-tag {
  font-size: .64rem; text-transform: uppercase; letter-spacing: .03em;
  color: var(--text-muted); border: 1px solid var(--border);
  border-radius: .25rem; padding: 0 .25rem; margin-left: .35rem;
}
.chat-group-remove-btn,
.chat-group-add-btn,
.chat-group-leave-btn,
.chat-group-link {
  background: none; border: 1px solid var(--border); color: var(--text-dim);
  cursor: pointer; font-size: .72rem; line-height: 1.3;
  padding: .12rem .4rem; border-radius: .25rem;
  transition: color .15s, background .15s, border-color .15s;
}
.chat-group-remove-btn:hover { background: #7f1d1d; border-color: #b91c1c; color: #fca5a5; }
.chat-group-add { display: flex; flex-wrap: wrap; gap: .25rem; }
.chat-group-add-btn:hover,
.chat-group-link:hover { background: var(--border); color: var(--text); border-color: var(--border-strong); }
.chat-group-link { display: inline-flex; align-items: center; gap: .3rem; margin-bottom: .2rem; }
.chat-group-rename { display: flex; gap: .3rem; align-items: flex-start; }
.chat-group-rename .chat-group-name-input { margin-bottom: 0; }
.chat-group-mini-btn { padding: .2rem .5rem; font-size: .72rem; border-radius: .25rem; }
/* Leave and Delete, fenced off under a rule so neither is a stray click away
   from the member list above them. */
.chat-group-danger {
  display: flex; flex-wrap: wrap; gap: .3rem;
  margin-top: .5rem; padding-top: .45rem;
  border-top: 1px solid var(--border);
}
.chat-group-leave-btn:hover { background: #7f1d1d; border-color: #b91c1c; color: #fca5a5; }

/* Who said it, over an incoming group bubble. Not on our own — we know. */
.chat-bubble-author {
  font-size: .68rem; font-weight: 700; color: var(--text-muted);
  padding-left: .2rem;
}




/* Landing page */

/* Reset body for landing */
body.landing-body {
  background: #0a0f1e;
  color: var(--text);

  /* The marketing page keeps its own fixed dark design regardless of the
     app's theme toggle — its background above is a literal, not var(--bg),
     and was never meant to repaint. Everything under it, though, was built
     on the shared --text/--surface/--border tokens the light theme now
     re-points to light values, so without this override switching to light
     theme on any other page would leave the landing page's own text and
     cards drawn in near-white on near-white the next time someone opened
     it. Re-pinning them here to their dark-theme values keeps the landing
     page exactly as designed no matter what the toggle is set to. */
  --bg:             #0f172a;
  --surface:        #1e293b;
  --surface-hi:     #273549;
  --surface-lift:   #33455f;
  --surface-sunken: #0b1220;
  --border:         #334155;
  --border-strong:  #475569;
  --border-subtle:  #1e293b;
  --text:           #e2e8f0;
  --text-strong:    #f1f5f9;
  --text-dim:       #94a3b8;
  --text-muted:     #64748b;
  --text-faint:     #475569;
  --text-prose:     #cbd5e1;
}

/* �w�w Top nav �w�w */
.landing-nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 1rem 2rem;
  padding-top: calc(1rem + var(--safe-top));
  padding-left: calc(2rem + var(--safe-left));
  padding-right: calc(2rem + var(--safe-right));
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(10, 15, 30, 0.85);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid rgba(255,255,255,.06);
}
.landing-nav__brand {
  font-size: 1.3rem;
  font-weight: 800;
  display: flex;
  align-items: center;
  gap: .5rem;
  letter-spacing: -.02em;
}
.landing-nav__brand .brand-text {
  background: linear-gradient(135deg, #38d7f5 0%, #2f9de8 50%, #3b6ef5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.landing-nav__brand:hover { opacity: .85; }
.landing-nav__links {
  display: flex;
  align-items: center;
  gap: 1.5rem;
}
.landing-nav__links a { color: var(--text-dim); font-size: .95rem; transition: color .2s; }
.landing-nav__links a:hover { color: var(--text); }
.landing-nav__signin { font-weight: 500; }

/* �w�w Shared buttons �w�w */
.landing-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .5rem;
  padding: .5rem 1.1rem;
  border-radius: .5rem;
  /* A <button> takes neither the page font nor its line-height from an
     ancestor, so without these two the guest button sits a pixel shorter
     than the <a> beside it and in a different typeface. */
  font-family: inherit;
  line-height: 1.25;
  font-size: .9rem;
  font-weight: 600;
  text-decoration: none;
  /* Transparent rather than none, so the bordered variants below (Apple,
     Google, ghost) come out exactly as tall as the borderless primary. */
  border: 1px solid transparent;
  cursor: pointer;
  transition: background .2s, box-shadow .2s, transform .15s;
  white-space: nowrap;
}
.landing-btn:hover { transform: translateY(-1px); }
.landing-btn--primary {
  background: linear-gradient(135deg, var(--accent-hi), var(--accent));
  color: #fff;
  box-shadow: 0 4px 18px rgba(14,165,233,.3);
}
.landing-btn--primary:hover {
  background: linear-gradient(135deg, var(--accent), var(--accent-deep));
  box-shadow: 0 6px 24px rgba(14,165,233,.4);
}
.landing-btn--apple {
  background: #000;
  color: #fff;
  border: 1px solid #000;
  box-shadow: 0 2px 8px rgba(0,0,0,.25);
}
.landing-btn--apple:hover { background: #1c1c1e; box-shadow: 0 4px 14px rgba(0,0,0,.35); }
.landing-btn--google {
  background: #fff;
  color: #3c4043;
  border: 1px solid #dadce0;
  box-shadow: 0 2px 8px rgba(0,0,0,.15);
}
.landing-btn--google:hover { background: #f8f9fa; box-shadow: 0 4px 14px rgba(0,0,0,.2); }
.landing-btn--ghost {
  background: transparent;
  color: var(--text-dim);
  border: 1px solid var(--border);
}
.landing-btn--ghost:hover { background: var(--surface); color: var(--text); border-color: var(--border-strong); }
.landing-btn--lg { padding: .75rem 1.6rem; font-size: 1rem; border-radius: .6rem; }

/* �w�w Badge �w�w */
.landing-badge {
  display: inline-block;
  background: rgba(14,165,233,.12);
  color: #7dd3fc;
  border: 1px solid rgba(14,165,233,.3);
  border-radius: 999px;
  padding: .25rem .85rem;
  font-size: .78rem;
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  margin-bottom: 1.25rem;
}

/* �w�w Hero �w�w */
.landing-hero {
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 5rem 1.5rem 4rem;
  background: radial-gradient(ellipse 80% 60% at 50% 0%, rgba(14,165,233,.15) 0%, transparent 70%);
}
.landing-hero__inner { max-width: 720px; }
.landing-hero__title {
  font-size: clamp(2.4rem, 5vw, 3.8rem);
  font-weight: 800;
  line-height: 1.15;
  letter-spacing: -.03em;
  color: var(--text-strong);
  margin-bottom: 1.25rem;
}
.landing-hero__desc {
  font-size: 1.15rem;
  color: var(--text-dim);
  line-height: 1.7;
  max-width: 540px;
  margin: 0 auto 2.25rem;
}
.landing-hero__cta {
  /* Four ways in wrap to 3 + 1 in a row, leaving the last button floating
     under the gaps of the three above it. Two per row instead: the columns
     size to their widest button and every button stretches to fill one, so
     the block lines up on both edges and down the middle. */
  display: inline-grid;
  grid-template-columns: repeat(2, 1fr);
  gap: .85rem;
  justify-content: center;
}
/* The closing CTA carries three buttons, not four — a second column would
   strand the last one — so that copy stays a single centered row. */
.landing-cta-section .landing-hero__cta {
  display: flex;
  flex-wrap: wrap;
}

/* �w�w Shared section wrapper �w�w */
.landing-section-inner {
  max-width: 1100px;
  margin: 0 auto;
  padding: 0 1.5rem;
}
.landing-section-title {
  font-size: clamp(1.7rem, 3vw, 2.4rem);
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--text-strong);
  text-align: center;
  margin-bottom: .75rem;
}
.landing-section-sub {
  text-align: center;
  color: var(--text-dim);
  font-size: 1.05rem;
  margin-bottom: 3rem;
}

/* �w�w Product showcase / mockup �w�w */
.landing-showcase {
  padding: 2rem 1.5rem 4rem;
}
@keyframes mockup-pulse-ring {
  0%   { transform: translate(-50%, 0) scale(1);   opacity: .65; }
  100% { transform: translate(-50%, 0) scale(2.8); opacity: 0;   }
}
.landing-mockup {
  position: relative;
  border-radius: 1.25rem;
  overflow: hidden;
  border: 1px solid rgba(14,165,233,.25);
  box-shadow:
    0 0 0 1px rgba(255,255,255,.07),
    0 40px 100px rgba(0,0,0,.7),
    0 0 120px rgba(14,165,233,.14),
    0 0 50px  rgba(99,102,241,.08);
  background: #111827;
  min-height: 380px;
}

/* Faux map background */
.mockup-map {
  position: relative;
  height: 280px;
  background: linear-gradient(160deg, #0a1020 0%, #0e1a30 50%, #0a1020 100%);
  overflow: hidden;
}
.mockup-map::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 55% 70% at 58% 38%, rgba(14,165,233,.12) 0%, transparent 65%),
    radial-gradient(ellipse 32% 38% at 22% 68%, rgba(168,85,247,.09) 0%, transparent 55%),
    linear-gradient(to bottom, transparent 60%, rgba(10,16,32,.85) 100%);
  pointer-events: none;
  z-index: 1;
}
.mockup-map__grid {
  position: absolute;
  inset: 0;
  background-image:
    linear-gradient(rgba(99,102,241,.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(99,102,241,.1) 1px, transparent 1px);
  background-size: 40px 40px;
}

/* Route SVG */
.mockup-routes {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  filter: drop-shadow(0 0 6px rgba(14,165,233,.5));
  z-index: 2;
}

/* Location pins */
.mockup-pin {
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .15rem;
  font-size: .72rem;
  font-weight: 600;
  color: var(--text);
  filter: drop-shadow(0 2px 8px rgba(0,0,0,.6));
  z-index: 3;
}
.mockup-pin i { font-size: 1.6rem; line-height: 1; }
.pin-ring {
  position: absolute;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 2px solid currentColor;
  top: 0;
  left: 50%;
  transform: translate(-50%, 0);
  animation: mockup-pulse-ring 2.2s ease-out infinite;
  opacity: 0;
}
.mockup-pin--4 .pin-ring { animation-delay: 0s; }
.mockup-pin--1 .pin-ring { animation-delay: .75s; }
.mockup-pin--2 .pin-ring { animation-delay: 1.5s; }
.mockup-pin--3 .pin-ring { animation-delay: .35s; }
.mockup-pin span {
  background: rgba(10,16,32,.85);
  border: 1px solid rgba(255,255,255,.15);
  border-radius: .3rem;
  padding: .1rem .4rem;
  white-space: nowrap;
  backdrop-filter: blur(4px);
}
.mockup-pin--1 { top: 38%; left: 25%; color: #7dd3fc; }
.mockup-pin--2 { top: 22%; left: 62%; color: #60a5fa; }
.mockup-pin--3 { top: 45%; left: 84%; color: #60a5fa; }
.mockup-pin--4 { top: 62%; left: 17%; color: #f472b6; }

/* Timeline strip */
.mockup-timeline {
  background: var(--bg);
  border-top: 1px solid rgba(14,165,233,.2);
  padding: 1rem 1.5rem;
}
.mockup-timeline__label {
  font-size: .75rem;
  font-weight: 600;
  color: #7dd3fc;
  margin-bottom: .75rem;
  display: flex;
  align-items: center;
  gap: .4rem;
  letter-spacing: .04em;
  text-transform: uppercase;
}
.mockup-timeline__track {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scrollbar-width: none;
}
.mockup-timeline__track::-webkit-scrollbar { display: none; }
.mockup-tl-item {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4rem;
  flex: 0 0 auto;
}
.mockup-tl-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--border);
  border: 2px solid var(--border-strong);
  flex-shrink: 0;
}
.mockup-tl-item--active .mockup-tl-dot {
  background: var(--accent);
  border-color: var(--accent-hi);
  box-shadow: 0 0 10px rgba(14,165,233,.7), 0 0 20px rgba(14,165,233,.3);
}
.mockup-tl-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: .5rem;
  padding: .5rem .75rem;
  font-size: .78rem;
  color: #cbd5e1;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .2rem;
  min-width: 90px;
  text-align: center;
}
.mockup-tl-card i { color: #7dd3fc; font-size: .9rem; }
.mockup-tl-card span { font-weight: 600; color: var(--text); }
.mockup-tl-card small { color: var(--text-muted); }
.mockup-tl-item--active .mockup-tl-card {
  background: linear-gradient(135deg, #0d2d4a 0%, #0c1a2e 100%);
  border-color: var(--accent);
  box-shadow: 0 0 16px rgba(14,165,233,.22), inset 0 1px 0 rgba(56,189,248,.18);
}

/* �w�w Features �w�w */
.landing-features {
  padding: 5rem 1.5rem;
  background: linear-gradient(180deg, transparent 0%, rgba(5,150,105,.12) 50%, transparent 100%);
}
.landing-features__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 1.5rem;
}
.landing-feature-card {
  background: #111827;
  border: 1px solid rgba(255,255,255,.07);
  border-radius: 1rem;
  padding: 2rem 1.5rem;
  transition: border-color .2s, box-shadow .2s, transform .2s;
}
.landing-feature-card:hover {
  border-color: rgba(14,165,233,.35);
  box-shadow: 0 8px 32px rgba(14,165,233,.1);
  transform: translateY(-3px);
}
.landing-feature-card h3 {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--text-strong);
  margin-bottom: .6rem;
}
.landing-feature-card p {
  font-size: .9rem;
  color: var(--text-dim);
  line-height: 1.65;
}
.landing-feature-card__icon {
  width: 48px;
  height: 48px;
  border-radius: .75rem;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.3rem;
  margin-bottom: 1.1rem;
}
.landing-feature-card__icon--purple { background: rgba(14,165,233,.18); color: #7dd3fc; }
.landing-feature-card__icon--blue   { background: rgba(37,99,235,.18);  color: #60a5fa; }
.landing-feature-card__icon--cyan   { background: rgba(8,145,178,.18);  color: #22d3ee; }
.landing-feature-card__icon--green  { background: rgba(5,150,105,.18);  color: #34d399; }

/* �w�w Final CTA section �w�w */
.landing-cta-section {
  padding: 5rem 1.5rem;
  text-align: center;
  background: radial-gradient(ellipse 70% 60% at 50% 50%, rgba(14,165,233,.1) 0%, transparent 70%);
}
.landing-cta-section h2 {
  font-size: clamp(1.8rem, 3.5vw, 2.8rem);
  font-weight: 800;
  letter-spacing: -.02em;
  color: var(--text-strong);
  margin-bottom: .75rem;
}
.landing-cta-section p {
  color: var(--text-dim);
  font-size: 1.05rem;
  margin-bottom: 2rem;
}

/* �w�w Footer �w�w */
.landing-footer {
  border-top: 1px solid rgba(255,255,255,.07);
  padding: 3rem 2rem 1.5rem;
  color: var(--text-muted);
  font-size: .88rem;
}
.landing-footer__inner {
  max-width: 1100px;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 2rem;
  align-items: flex-start;
  justify-content: space-between;
  margin-bottom: 2rem;
}
.landing-footer__brand a {
  display: inline-flex;
  align-items: center;
  gap: .4rem;
  font-size: 1.1rem;
  font-weight: 700;
  margin-bottom: .5rem;
}
.landing-footer__brand a .brand-text {
  background: linear-gradient(135deg, #38d7f5 0%, #2f9de8 50%, #3b6ef5 100%);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
.landing-footer__brand p { color: var(--text-muted); font-size: .85rem; max-width: 220px; }
.landing-footer__links,
.landing-footer__legal {
  display: flex;
  flex-direction: column;
  gap: .5rem;
}
.landing-footer__links a,
.landing-footer__legal a { color: var(--text-muted); transition: color .2s; font-size: .88rem; }
.landing-footer__links a:hover,
.landing-footer__legal a:hover { color: var(--text); }
.landing-footer__copy {
  max-width: 1100px;
  margin: 0 auto;
  text-align: center;
  color: var(--text-faint);
  font-size: .8rem;
  padding-top: 1.5rem;
  border-top: 1px solid rgba(255,255,255,.05);
}

/* �w�w Responsive �w�w */
@media (max-width: 640px) {
  .landing-nav {
    padding: .75rem 1rem;
    padding-top: calc(.75rem + var(--safe-top));
    padding-left: calc(1rem + var(--safe-left));
    padding-right: calc(1rem + var(--safe-right));
  }
  .landing-nav__links { gap: .75rem; }
  .landing-hero { padding: 3.5rem 1rem 2.5rem; }
  body.landing-body { overflow-x: hidden; }
  .landing-hero__desc { white-space: normal !important; max-width: 100% !important; }
  .landing-hero__cta { grid-template-columns: minmax(0, 1fr); width: 100%; }
  .landing-cta-section .landing-hero__cta { flex-direction: column; align-items: stretch; }
  .landing-features__grid { grid-template-columns: 1fr; }
  .mockup-map { height: 200px; }
  .mockup-pin span { display: none; }
  .landing-footer__inner { flex-direction: column; gap: 1.5rem; }
}

/* ── Friends nav badge ──
   Carries both unanswered friend requests and unanswered location invites.
   It is always in the DOM (hidden at zero) so a socket push can update it
   in place rather than having to insert an element into the navbar. */
.friends-badge {
  background: var(--danger); color: #fff;
  border-radius: 9px; padding: 0 5px;
  font-size: .7rem; margin-left: .2rem;
}
/* A badge that does something of its own when pressed, rather than riding
   along on the link it sits in — the Photo Map's comment count, which gathers
   the cards those comments are on at the top of the panel (see Navbar's
   showNewCommentsFirst). It has to read as pressable from a glance at the
   navbar, so it gets the pointer and lifts under one; and it is reachable by
   keyboard, so it needs a focus ring of its own — the link's own outline is
   drawn around the whole row and says nothing about the number inside it.
   The hit area is padded out a little, because the number is four or five
   pixels wide and a thumb is not. */
.friends-badge--action {
  cursor: pointer;
  padding: .1rem .35rem;
  transition: transform var(--fast) var(--ease), background var(--fast) var(--ease);
}
.friends-badge--action:hover { background: #ef4444; transform: scale(1.12); }
.friends-badge--action:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 1px;
}
@media (prefers-reduced-motion: reduce) {
  .friends-badge--action { transition: none; }
  .friends-badge--action:hover { transform: none; }
}
/* Applied for one beat when the count goes up, so something arriving while
   you are reading a different page actually catches the eye. */
.friends-badge--new { animation: friends-badge-pop 1.1s ease-out 2; }
@keyframes friends-badge-pop {
  0%   { transform: scale(1);    box-shadow: 0 0 0 0 rgba(220,38,38,.7); }
  35%  { transform: scale(1.28); box-shadow: 0 0 0 .35rem rgba(220,38,38,0); }
  100% { transform: scale(1);    box-shadow: 0 0 0 0 rgba(220,38,38,0); }
}
@media (prefers-reduced-motion: reduce) {
  .friends-badge--new { animation: none; }
}

/* ── Date picker (flatpickr) ───────────────────────────────────────────────
   The small calendar behind every date field — event dates, photo dates, the
   map date range, plan start/end, the export and admin range filters.

   flatpickr ships it white, and lays the header out absolutely: the month
   dropdown and the year sit in one centred 75%-wide box while the prev/next
   arrows are pinned on top of it, so the year's hairline spinner ends up
   crowded against the month name and the whole row drifts off-centre as the
   month name changes length. Dropped into a dark modal it read as a white
   card with a jumbled header.

   Below the header is rebuilt as a single flex row —
       [‹]   [ Month ▾ | Year ▾ ]   [›]
   with the two selectors sharing one sunken pill so they read as the two
   halves of the same control, and the arrows as real buttons on the ends.
   The rest repaints the calendar in the app's own surface/border/select
   tokens instead of flatpickr's greys.

   Every selector is prefixed with `body` deliberately: flatpickr's own
   stylesheet loads *after* this file in both places it is used (bundled by
   Vite in the SPA, a CDN <link> in the admin page head), so an equally
   specific rule would lose the tie. The calendar is always appended to
   <body>, so the prefix costs nothing and always matches. */
body .flatpickr-calendar {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  box-shadow: var(--sh-lg);
  color: var(--text);
  font-family: inherit;
  font-size: var(--t-md);
  padding-bottom: .35rem;
}
/* The little arrow that points back at the input it opened from. */
body .flatpickr-calendar.arrowTop:before { border-bottom-color: var(--border); }
body .flatpickr-calendar.arrowTop:after  { border-bottom-color: var(--surface); }
body .flatpickr-calendar.arrowBottom:before { border-top-color: var(--border); }
body .flatpickr-calendar.arrowBottom:after  { border-top-color: var(--surface); }

/* ── Header row ── */
body .flatpickr-calendar .flatpickr-months {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding: .5rem .5rem .35rem;
}
body .flatpickr-calendar .flatpickr-months .flatpickr-month {
  flex: 1 1 auto;
  height: auto;
  overflow: visible;      /* was hidden — it clipped the pill's border */
  background: transparent;
  color: var(--text);
}
body .flatpickr-calendar .flatpickr-months .flatpickr-prev-month,
body .flatpickr-calendar .flatpickr-months .flatpickr-next-month {
  position: static;       /* out of the absolute overlay, into the flex row */
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  color: var(--text-dim);
  fill: currentColor;
  transition: background var(--fast) var(--ease),
              border-color var(--fast) var(--ease),
              color var(--fast) var(--ease);
}
body .flatpickr-calendar .flatpickr-months .flatpickr-prev-month:hover,
body .flatpickr-calendar .flatpickr-months .flatpickr-next-month:hover {
  background: var(--surface-hi);
  border-color: var(--border);
  color: var(--text-strong);
}
body .flatpickr-calendar .flatpickr-months .flatpickr-prev-month svg,
body .flatpickr-calendar .flatpickr-months .flatpickr-next-month svg,
body .flatpickr-calendar .flatpickr-months .flatpickr-prev-month:hover svg,
body .flatpickr-calendar .flatpickr-months .flatpickr-next-month:hover svg {
  width: 13px;
  height: 13px;
  fill: currentColor;     /* flatpickr turns these red on hover */
}
body .flatpickr-calendar .flatpickr-months .flatpickr-prev-month svg path,
body .flatpickr-calendar .flatpickr-months .flatpickr-next-month svg path {
  fill: inherit;
}

/* ── Month + year: one pill, two halves ── */
body .flatpickr-calendar .flatpickr-current-month {
  position: static;       /* was absolute at left:12.5%; width:75% */
  width: auto;
  height: auto;
  padding: 0;
  display: flex;
  align-items: stretch;
  justify-content: center;
  font-size: var(--t-md);
  font-weight: 600;
  transform: none;
}
body .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months,
body .flatpickr-calendar .flatpickr-current-month .flatpickr-yearDropdown-years {
  /* The native menulist arrow is drawn by the OS and stays black on our dark
     fill, so the control draws its own chevron instead. The slate matches
     --text-dim; a data URI cannot read a custom property. */
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml;charset=utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none' stroke='%2394a3b8' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 4.5 6 7.5 9 4.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right .4rem center;
  background-size: 11px 11px;
  background-color: var(--surface-sunken);
  border: 1px solid var(--border);
  color: var(--text-strong);
  cursor: pointer;
  font-family: inherit;
  font-size: var(--t-md);
  font-weight: 600;
  line-height: 1;
  height: 30px;
  margin: 0;
  padding: 0 1.45rem 0 .6rem;
  text-align: left;
  transition: background-color var(--fast) var(--ease),
              border-color var(--fast) var(--ease);
}
/* Joined into one control: the month keeps the left cap, the year the right,
   and they share the hairline between them. */
body .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months {
  border-radius: var(--r-sm) 0 0 var(--r-sm);
  border-right-width: 0;
  min-width: 7.5rem;
}
body .flatpickr-calendar .flatpickr-current-month .flatpickr-yearDropdown-years {
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  font-variant-numeric: tabular-nums;
}
body .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months:hover,
body .flatpickr-calendar .flatpickr-current-month .flatpickr-yearDropdown-years:hover {
  background-color: var(--surface-hi);
  border-color: var(--border-strong);
}
body .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months:focus-visible,
body .flatpickr-calendar .flatpickr-current-month .flatpickr-yearDropdown-years:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: var(--ring);
  position: relative;     /* so the ring is not covered by its neighbour */
}
/* The open list is drawn by the browser, which only inherits colour if the
   options carry it themselves. */
body .flatpickr-calendar .flatpickr-current-month .flatpickr-monthDropdown-months option,
body .flatpickr-calendar .flatpickr-current-month .flatpickr-yearDropdown-years option {
  background: var(--surface);
  color: var(--text);
  font-weight: 500;
}

/* The SPA replaces flatpickr's year spinner with a dropdown (see DateInput.jsx)
   and marks the calendar so the now-redundant number input can step aside. */
body .flatpickr-calendar.has-year-dropdown .flatpickr-current-month .numInputWrapper {
  display: none;
}
/* Pages still on the stock spinner (the admin range filter) get it dressed as
   the right-hand half of the same pill, with arrows big enough to hit. */
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper {
  display: flex;
  align-items: center;
  width: auto;
  padding-right: 15px;
  background: var(--surface-sunken);
  border: 1px solid var(--border);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  height: 30px;
}
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper:hover {
  background: var(--surface-hi);
}
body .flatpickr-calendar .flatpickr-current-month input.cur-year {
  /* border-box everywhere, so the width has to carry the padding too or the
     fourth digit is cut off */
  width: 6ch;
  padding: 0 .15rem 0 .55rem;
  color: var(--text-strong);
  font-size: var(--t-md);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  background: transparent;
}
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper span {
  width: 15px;
  border: none;
  background: transparent;
  opacity: 1;             /* was only visible on hover */
}
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper span:hover {
  background: var(--border);
}
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper span.arrowUp:after {
  border-bottom-color: var(--text-dim);
}
body .flatpickr-calendar .flatpickr-current-month .numInputWrapper span.arrowDown:after {
  border-top-color: var(--text-dim);
}

/* ── Weekday strip ── */
body .flatpickr-calendar .flatpickr-weekdays {
  background: transparent;
  height: 26px;
}
body .flatpickr-calendar span.flatpickr-weekday {
  background: transparent;
  color: var(--text-muted);
  font-size: var(--t-xs);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .05em;
}

/* ── Days ── */
body .flatpickr-calendar .flatpickr-days,
body .flatpickr-calendar .flatpickr-innerContainer,
body .flatpickr-calendar .flatpickr-rContainer {
  background: transparent;
}
/* Height only — the width stays flatpickr's, because the row is a wrapping
   flexbox with no column count: narrow the cells and an eighth day fits on
   the line, which silently shifts every date in the month. */
body .flatpickr-calendar .flatpickr-day {
  height: 36px;
  line-height: 36px;
  border-radius: var(--r-sm);
  border-color: transparent;
  color: var(--text);
  font-weight: 500;
  transition: background var(--fast) var(--ease),
              color var(--fast) var(--ease);
}
body .flatpickr-calendar .flatpickr-day:hover,
body .flatpickr-calendar .flatpickr-day:focus {
  background: var(--surface-hi);
  border-color: var(--border);
  color: var(--text-strong);
}
body .flatpickr-calendar .flatpickr-day.prevMonthDay,
body .flatpickr-calendar .flatpickr-day.nextMonthDay {
  color: var(--text-faint);
}
body .flatpickr-calendar .flatpickr-day.today {
  border-color: var(--accent);
  color: var(--accent-hi);
  font-weight: 700;
}
body .flatpickr-calendar .flatpickr-day.today:hover,
body .flatpickr-calendar .flatpickr-day.today:focus {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--accent-hi);
}
body .flatpickr-calendar .flatpickr-day.inRange {
  background: var(--select-wash);
  border-color: transparent;
  box-shadow: none;
  color: var(--text-strong);
}
body .flatpickr-calendar .flatpickr-day.selected,
body .flatpickr-calendar .flatpickr-day.selected:hover,
body .flatpickr-calendar .flatpickr-day.selected:focus,
body .flatpickr-calendar .flatpickr-day.startRange,
body .flatpickr-calendar .flatpickr-day.endRange {
  background: var(--select);
  border-color: var(--select);
  color: #fff;
  font-weight: 700;
  box-shadow: none;
}
body .flatpickr-calendar .flatpickr-day.flatpickr-disabled,
body .flatpickr-calendar .flatpickr-day.flatpickr-disabled:hover {
  background: transparent;
  border-color: transparent;
  color: var(--text-faint);
  opacity: .45;
}

/* ── Moderation queue (/admin/reports) ─────────────────────────────────────
   Cards rather than a table: a report's body is free text of unpredictable
   length, which a table column either truncates or lets stretch the row. */
/* Title line: the heading on the left, the search pushed to the right end of
   it. The search used to have a row to itself between the heading and the
   tabs, which on a 1400px page meant one short control alone on a line and
   three-quarters of that line empty — and on this page every row above the
   list is a row the list doesn't get. */
.report-header { flex-wrap: wrap; margin-bottom: .6rem; }
.report-header h2 { margin-right: auto; }

.report-search { display: flex; align-items: center; gap: .4rem; }

/* Input and button as one object rather than two with a gap between: the
   button does nothing except submit what is in the box beside it, and joined
   they read as the single control they are. The border lives on the group,
   so the input inside carries none of its own. */
.report-search-group {
  display: flex;
  align-items: center;
  flex: 0 1 24rem;
  border: 1px solid var(--border);
  border-radius: 7px;
  background: var(--surface-sunken);
  overflow: hidden;
}
.report-search-group:focus-within { border-color: var(--accent); }
.report-search-icon { color: var(--text-dim); font-size: .78rem; padding: 0 .1rem 0 .6rem; }
.report-search-input {
  flex: 1 1 auto;
  min-width: 7rem;
  padding: .38rem .55rem;
  font-size: .82rem;
  border: 0;
  background: transparent;
  color: var(--text);
}
.report-search-input:focus { outline: none; }
/* The browser's own clear cross, which would sit next to ours. */
.report-search-input::-webkit-search-cancel-button { display: none; }
.report-search-go {
  border: 0;
  border-left: 1px solid var(--border);
  background: var(--surface-hi);
  color: var(--text-dim);
  font-size: .8rem;
  padding: .38rem .8rem;
  cursor: pointer;
}
.report-search-go:hover { background: var(--accent); color: #fff; }

/* A cross, not the word "Clear": it sits directly beside the box it empties
   and only appears when there is something in it. */
.report-search-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--border);
  border-radius: 7px;
  color: var(--text-dim);
  font-size: .8rem;
  /* A button now that the queue is React (pages/AdminReportsPage.jsx) and
     clearing the search rewrites the route rather than following a link.
     Buttons arrive with the browser's own grey and its own font, so both are
     spelled out here — the rest of the rule was already button-shaped. */
  background: none;
  font-family: inherit;
  cursor: pointer;
}
.report-search-clear:hover { color: var(--text); border-color: var(--text-dim); }

/* Both filters on one line. They were two stacked rows, which cost a row and
   read as two unrelated controls; the rule between them says they are one
   control with two halves. Wraps rather than squeezes on a narrow window. */
.report-filters {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: .5rem .7rem;
  margin-bottom: .8rem;
}
.report-filter-sep {
  width: 1px;
  align-self: stretch;
  min-height: 1.3rem;
  background: var(--border);
}
.report-tabs { display: flex; gap: .35rem; flex-wrap: wrap; }
.report-tab {
  padding: .3rem .7rem;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: .8rem;
  color: var(--text-dim);
  text-decoration: none;
  white-space: nowrap;
  /* Buttons rather than links since the queue became a React route: picking a
     tab is a change of filter, not a page to fetch. See .report-search-clear
     above for why a button needs these three said out loud. */
  background: none;
  font-family: inherit;
  cursor: pointer;
}
.report-tab:hover { color: var(--text); border-color: var(--text-dim); }
.report-tab.is-active { background: var(--accent); border-color: var(--accent); color: #fff; }
.report-tab-count { opacity: .7; font-size: .74rem; }

/* Lighter than the queue tabs beside them, so the eye still lands first on
   which queue it is reading. */
.report-tabs-type .report-tab { padding: .26rem .55rem; font-size: .77rem; }

/* Takes the empty end of the filter row rather than a line of its own. */
.report-search-note {
  margin-left: auto;
  font-size: .78rem;
  color: var(--text-dim);
}

/* The queue is a column the height of the window: everything above the list
   at its natural height, the list taking whatever is left and scrolling on
   its own. The subtraction is the chrome above it — the navbar, the safe
   area it sits under, and the container's margins — and nothing else, so a
   wrapped row of tabs shortens the list instead of overflowing the page. */
.report-queue {
  display: flex;
  flex-direction: column;
  height: calc(100dvh - 7rem - var(--safe-top));
  /* Below this the list is too short to be worth scrolling inside; let the
     page scroll instead. */
  min-height: 22rem;
}

.report-list {
  display: flex;
  flex-direction: column;
  gap: .7rem;
  flex: 1 1 auto;
  /* Without this a flex child refuses to shrink past its content, and the
     list grows the page instead of scrolling. */
  min-height: 0;
  overflow-y: auto;
  /* Reaching the end of the queue shouldn't start scrolling the page. */
  overscroll-behavior: contain;
  /* Room for the scrollbar, and for the accent edge on grouped cards, so
     neither sits on top of a card border. */
  padding: 0 .5rem 0 .15rem;
}

/* Repeat reports about one thing are laid out as a block. The second card on
   is pulled up against the one above and marked down its left edge, so the
   block reads as one subject seen several times rather than as separate
   rows that happen to look alike. */
.report-repeat-head {
  font-size: .78rem;
  color: var(--text-dim);
  margin: -.15rem 0 .5rem;
}
.report-repeat-head i { margin-right: .3rem; opacity: .8; }
.report-card.is-repeat {
  margin-top: -.45rem;
  border-left: 3px solid var(--accent);
}
.report-card {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: .8rem .9rem;
  background: var(--surface);
}
.report-card.is-resolved { opacity: .55; }

.report-card-head { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .4rem; }
.report-reason { font-weight: 600; font-size: .9rem; }
.report-type {
  font-size: .7rem; text-transform: uppercase; letter-spacing: .04em;
  padding: .1rem .4rem; border-radius: 4px;
  background: var(--surface-alt, rgba(148,163,184,.15)); color: var(--text-dim);
}
.report-date { margin-left: auto; font-size: .74rem; color: var(--text-faint); }

.report-meta { display: flex; gap: 1.2rem; flex-wrap: wrap; font-size: .8rem; color: var(--text-dim); margin-bottom: .5rem; }

/* pre, not a div: reported text keeps its own line breaks, and wrapping is
   forced so a pasted URL can't push the card wider than the page. */
.report-snapshot {
  margin: 0 0 .5rem;
  padding: .5rem .6rem;
  border-left: 3px solid var(--border);
  background: rgba(148,163,184,.08);
  font-size: .8rem;
  font-family: inherit;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  max-height: 12rem;
  overflow-y: auto;
}
.report-details { font-size: .82rem; margin-bottom: .5rem; overflow-wrap: anywhere; }

/* The reported media, sized to judge by rather than to browse: big enough to
   see what a report means, small enough that a queue of them stays scannable.
   Scrolls sideways instead of wrapping, so one event with six photos can't
   push the buttons off the screen. */
.report-media {
  display: flex;
  gap: .4rem;
  margin-bottom: .5rem;
  padding-bottom: .25rem;
  overflow-x: auto;
}
.report-media-item {
  height: 7rem;
  width: auto;
  max-width: 12rem;
  object-fit: cover;
  border-radius: var(--r-sm, 6px);
  border: 1px solid var(--border);
  background: rgba(148,163,184,.08);
  flex-shrink: 0;
}
.report-media-more {
  align-self: center;
  font-size: .78rem;
  color: var(--text-dim);
  white-space: nowrap;
  flex-shrink: 0;
}
.report-media-item { cursor: zoom-in; }

/* A suspended account reads as suspended from across the queue, without
   having to look at the button's wording. Amber rather than red: the account
   is held, not deleted, and this button is the way back. */
/* Said once, after the page has rebuilt itself: the card that was clicked is
   gone or changed by then, so the confirmation can't live on it. */
.report-flash {
  position: fixed;
  left: 50%;
  bottom: 1.5rem;
  transform: translateX(-50%);
  z-index: 9999;
  display: flex;
  align-items: center;
  gap: .45rem;
  max-width: min(90vw, 34rem);
  padding: .55rem .9rem;
  border-radius: 8px;
  border: 1px solid rgba(34, 197, 94, .4);
  background: rgba(20, 46, 31, .96);
  color: #86efac;
  font-size: .84rem;
  box-shadow: 0 8px 24px rgba(0, 0, 0, .45);
  opacity: 1;
  transition: opacity .5s var(--ease, ease);
}
.report-flash.is-going { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  .report-flash { transition: none; }
}

/* Sits where a takedown button would be, when there is nothing left to take
   down. Deliberately not a button and not amber: nothing here is held, and
   nothing can be undone. */
.report-gone {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: .78rem;
  color: var(--text-dim);
  font-style: italic;
}

/* Amber means a moderation hold is in force right now, and this is the button
   that lifts it — a suspended account, an event off the world map, a plan
   unpublished. One colour for one meaning, so a queue says what has already
   been done to it without every card having to be read. */
.report-suspend-btn.is-suspended,
.report-restore-btn {
  background: var(--warn);
  border-color: var(--warn);
  color: #1f1300;
}
.report-suspend-btn.is-suspended:hover,
.report-restore-btn:hover {
  background: #d97706;
  border-color: #d97706;
  box-shadow: 0 4px 14px rgba(245, 158, 11, .3);
}
.report-suspend-btn.is-suspended:focus-visible,
.report-restore-btn:focus-visible {
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--warn);
}

/* ── Full-screen photo viewer (static/js/photo-viewer.js) ──────────────────
   Mirrors the React <PhotoViewer> the world map uses, so a reported photo is
   examined the same way it was seen. */
.tm-viewer {
  position: fixed;
  inset: 0;
  z-index: 99999;
  background: #000;
  display: flex;
  align-items: center;
  justify-content: center;
  touch-action: none;
  overflow: hidden;
  user-select: none;
}
.tm-viewer-stage {
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tm-viewer-media {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  transform-origin: center center;
  /* No transition: a pinch must track the fingers, not chase them. */
}
.tm-viewer-close,
.tm-viewer-nav {
  position: absolute;
  z-index: 2;
  background: rgba(0, 0, 0, .55);
  border: 1px solid rgba(255, 255, 255, .2);
  color: #fff;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.tm-viewer-close {
  top: 1rem;
  right: 1rem;
  width: 2.5rem;
  height: 2.5rem;
  border-radius: 50%;
  font-size: 1rem;
}
.tm-viewer-nav {
  top: 50%;
  transform: translateY(-50%);
  width: 2.75rem;
  height: 2.75rem;
  border-radius: 50%;
  font-size: 1.1rem;
}
.tm-viewer-prev { left: 1rem; }
.tm-viewer-next { right: 1rem; }
.tm-viewer-counter {
  position: absolute;
  top: 1rem;
  left: 50%;
  transform: translateX(-50%);
  background: rgba(0, 0, 0, .5);
  border-radius: 12px;
  padding: .2rem .75rem;
  color: #e2e8f0;
  font-size: .82rem;
  z-index: 2;
  pointer-events: none;
}
/* Touch devices have the swipes; the arrows just cover the media there. */
@media (hover: none) and (pointer: coarse) {
  .tm-viewer-nav { display: none; }
}

.report-actions { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
.report-note { flex: 1 1 12rem; font-size: .82rem; padding: .3rem .5rem; }
.report-block-toggle { display: flex; align-items: center; gap: .3rem; font-size: .78rem; color: var(--text-dim); }
.report-status { font-size: .78rem; margin-top: .4rem; }
.report-resolved { font-size: .8rem; color: var(--text-dim); }
.report-note-shown { font-style: italic; margin-top: .2rem; overflow-wrap: anywhere; }

/* ── Report a conversation (chat) ──────────────────────────────────────────
   Sits beside Block in the chat window header. Icon-only: the header is
   already tight, and the flag reads clearly enough with a tooltip. */
.chat-report-btn {
  display: flex; align-items: center;
  background: none; border: 1px solid var(--border); color: var(--text-dim);
  cursor: pointer; font-size: .7rem; padding: .15rem .4rem;
  line-height: 1.3; border-radius: .25rem;
  transition: color .15s, background .15s, border-color .15s;
}
.chat-report-btn:hover { background: var(--border); color: #f87171; border-color: var(--border-strong); }

.chat-report-overlay {
  position: fixed; inset: 0; z-index: 100000;
  background: rgba(0,0,0,.6);
  display: flex; align-items: center; justify-content: center;
  padding: 1rem;
}
.chat-report-box {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 10px; padding: 1rem 1.1rem;
  width: min(420px, 100%); max-height: 90vh; overflow-y: auto;
  box-shadow: 0 10px 40px rgba(0,0,0,.5);
}
.chat-report-box h3 { margin: 0 0 .75rem; font-size: 1rem; }
.chat-report-box p  { font-size: .86rem; margin: 0 0 .9rem; }
.chat-report-reasons { display: flex; flex-direction: column; gap: .3rem; margin-bottom: .7rem; }
.chat-report-reason  { display: flex; align-items: center; gap: .45rem; font-size: .85rem; cursor: pointer; }
.chat-report-details {
  width: 100%; box-sizing: border-box; font-size: .85rem;
  resize: vertical; margin-bottom: .6rem;
}
.chat-report-blockopt { display: flex; align-items: center; gap: .45rem; font-size: .82rem; margin-bottom: .6rem; }
.chat-report-error    { font-size: .8rem; color: #dc2626; min-height: 1em; margin-bottom: .4rem; }
.chat-report-actions  { display: flex; gap: .5rem; justify-content: flex-end; }

/* ── Guest banner ──────────────────────────────────────────────────────────
   The same banner the SPA pages carry (see .guest-banner in spa.css), so a
   guest crossing from the map into Friends or their own account page meets
   one shape rather than two. It used to be written as inline styles on
   base.html with no phone rule at all: beside a button that cannot wrap, the
   sentence was squeezed into a column a few words wide and stood five or six
   lines tall. */
.guest-banner {
  display: flex;
  align-items: center;
  gap: .75rem;
  background: #422006;
  border: 1px solid #f59e0b;
  border-radius: 6px;
  padding: .5rem 1rem;
  margin-bottom: .75rem;
  font-size: .85rem;
  color: #fde68a;
}
.guest-banner__icon { font-size: 1rem; }
.guest-banner__text { flex: 1; }
.guest-banner__cta {
  margin-left: auto;
  font-size: .78rem;
  padding: .25rem .65rem;
  white-space: nowrap;
}

/* On a phone the sentence gets the full width on its own line and the button
   sits under it. */
@media (max-width: 640px) {
  .guest-banner {
    flex-direction: column;
    align-items: stretch;
    gap: .5rem;
  }
  .guest-banner__text { flex: none; }
  /* The icon reads as a marker for the sentence, not a column of its own. */
  .guest-banner__icon { display: none; }
  .guest-banner__cta {
    margin-left: 0;
    justify-content: center;
    text-align: center;
  }
}
