/* ===== Theme variables ===== */
/* LIGHT — "Studio": a clean, modern dashboard light theme. A soft cool-slate canvas is the work
   area; the sidebar, header and cards all sit on one near-white surface above it, separated by thin
   hairlines rather than heavy fills. Colour is rationed on purpose — a single vivid indigo carries
   every interactive state, and the semantic set (trooper blue, ICP teal, K9 purple, danger red)
   supplies the rest. The surface is #fbfcfd rather than #fff, and the canvas behind it is a real
   grey, so the page reads crisp instead of glaring. */
:root {
  /* two planes: a muted surface (cards, rail, header) floating on a cool-slate canvas. The whole
     scale is pulled down from near-white so the page sits calmer — nothing here is close to #fff. */
  --bg: #d6dbe4;            /* canvas — cool slate, the work area behind everything */
  --surface: #eaedf3;       /* cards / sidebar / header */
  --surface-2: #dfe4ed;     /* inputs / secondary fills */
  --border: #bfc8d6;
  /* ink is a soft slate GREY, not black — deliberately faded so the page reads gentle. It's still
     ~8:1 on the card surface, so "faded" here costs nothing in legibility. */
  --text: #3f4a5c;
  --text-soft: #525f77;     /* secondary ink — sits between the grey ink and the surfaces. Can't be
                               faded much further: it also lands directly on the canvas (history
                               dates, header counts), the darkest ground any text sits on here. */
  --accent: #4a6699;        /* the dark theme's navy, shared across both themes now */
  --accent-soft: #e2e8f2;
  /* a deeper rung of the accent for small text/hairlines on light surfaces — the navy itself only
     reaches ~4.2:1 once it sits on its own pale tint (buttons, history pills) */
  --accent-deep: #3a5280;
  /* text placed ON TOP OF a solid --accent fill. The navy measures ≈5.2:1 against this surface, so
     white ink is the right call here — same as the dark theme. */
  --accent-ink: #ffffff;
  /* The semantic set runs one rung darker than the usual "600" values across the board: every one
     of these doubles as TEXT on its own pale tint (outcome tags, call-sign pills, filter buttons,
     history type pills), where the 600s measured between 2.1 and 4.1:1. As fills they're unchanged
     in character — just deep enough to read as small type too. */
  --tc-blue: #1d4ed8;
  --danger: #a01212;
  --success: #047857;
  --warn: #a04a07;
  --icp-teal: #0e6c65;      /* ICP pills */
  --unit-orange: #a85708;   /* unit-only call-sign pills (deepened for light) */
  --k9-purple: #7e22ce;     /* K9 dog name pill */
  --radius: 18px;
  /* flat solid surfaces (no glass) */
  --glass-strong: #dce2ec;
  --glass-brd: rgba(15, 23, 42, .10);
  --glass-hi: rgba(255, 255, 255, .75);
  /* flat, like the dark theme. The view header paints its own copy of the page background behind
     its card (that's what masks scrolling content in the gaps); with a gradient those two copies
     could only ever *approximate* each other, so a flat colour makes the header gaps exactly the
     background colour, everywhere, at every scroll position. */
  --app-grad: none;
  /* CHROME — sidebar, view header card and the Troopers filter bar all share the card surface, so
     they read as one continuous plane above the canvas instead of a separate coloured frame. */
  --side-bg: var(--surface);
  --side-brd: rgba(15, 23, 42, .09);
  --side-div: rgba(15, 23, 42, .08);
  --side-fg: #475569;
  --side-fg-soft: #59677d;   /* the "TRACKER" / "MENU" micro-labels — tiny uppercase type, so it
                                needs more weight than a soft grey would suggest */
  --side-strong: var(--text);   /* the rail's brand/heading ink follows the page's grey ink */
  --side-hover: rgba(74, 102, 153, .09);  /* accent-tinted hover — subtle, and it ties the rail in */
  --side-active-fg: var(--accent-deep);
  --side-accent: var(--accent);           /* navy on the rail: ≈5.2:1, reads cleanly */
  --chrome-bg: var(--surface);
  --chrome-brd: var(--glass-brd);
  /* the "Add …" buttons and the Notes FAB both run on the accent — one interactive colour across
     the app is the point of this palette, so they stay deliberately identical */
  --btn-brand: var(--accent);
  --btn-brand-fg: var(--accent-deep);
  --fab-bg: var(--accent);
  --fab-fg: var(--accent-deep);
  /* right-edge panels (Change / Notes / Codes) — one step off the card surface so the panel reads
     as its own plane while the cards inside it stay the brightest thing */
  --panel-bg: #e1e6ef;
  --panel-brd: rgba(15, 23, 42, .10);
  --panel-div: rgba(15, 23, 42, .08);
  /* frosted glass for map controls + popups (over imagery) — light */
  --map-glass: rgba(255, 255, 255, .66);
  --map-glass-brd: rgba(255, 255, 255, .75);
}

html[data-theme="dark"] {
  /* modern dark: softened near-black canvas, elevated rounded cards, blue accent kept */
  --bg: #101117;
  --surface: #191a21;
  --surface-2: #23242c;
  --border: #2e2f38;
  /* not a pure white — a faded, slightly cool off-white so long stretches of text don't glare
     against the near-black canvas (user directive) */
  --text: #dfe3ea;
  --text-soft: #8f919b;
  --accent: #4a6699;
  --accent-soft: #171f30;
  --accent-ink: #fff;   /* dark's navy accent stays dark enough for white text, as before */
  --accent-deep: var(--accent);   /* dark's accent already reads as text — no split needed there */
  --tc-blue: #4f93ff;
  --danger: #ff6b61;
  --success: #4cc479;
  --warn: #e0b341;
  --icp-teal: #14b8a6;      /* ICP pick-cards / overview pills */
  --unit-orange: #f59e0b;   /* unit-only call-sign pills */
  --k9-purple: #b69bf0;     /* K9 dog name pill */
  /* flat card surfaces (no frosted glass) + near-flat page background */
  --glass-strong: #1c1d21;
  --glass-brd: rgba(255,255,255,.07);
  --glass-hi: rgba(255,255,255,.035);
  --app-grad: none;
  /* sidebar tokens (dark) — dark floating rail */
  --side-bg: #1a1b22;
  --side-brd: rgba(255, 255, 255, .08);
  --side-div: rgba(255, 255, 255, .06);
  --side-fg: rgba(232, 238, 246, .72);
  --side-fg-soft: var(--text-soft);
  --side-strong: #e2e7f0;          /* brand wordmark — same faded-white step as --text */
  --side-hover: rgba(255, 255, 255, .05);
  --side-active-fg: #eaeef5;
  --side-accent: var(--accent);   /* dark rail already sits on a dark page — the page accent works as-is */
  /* dark keeps the header card + filter bar on the plain card surface (the rail is already almost
     the same value here, so there's nothing to unify) */
  --chrome-bg: var(--surface);
  --chrome-brd: var(--glass-brd);
  --btn-brand: #4a6699;
  --btn-brand-fg: #6f8dc0;
  --fab-bg: #4a6699;              /* dark keeps the FAB on the app blue, as before */
  --fab-fg: #6f8dc0;
  --panel-bg: var(--side-bg);     /* dark theme: the panels match the rail, exactly as before */
  --panel-brd: var(--side-brd);
  --panel-div: var(--side-div);
  /* frosted glass for map controls + popups (over imagery) — dark */
  --map-glass: rgba(22, 24, 30, .55);
  --map-glass-brd: rgba(255, 255, 255, .14);
}

/* The breathing room around the floating rail, in one place: the sidebar's own
   top/bottom gap AND the strip that fills that gap across the page (.under-rail).
   The phone block below re-declares it — the rail sits 18px off the edge there. */
:root { --rail-gap: 14px; }

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* scrollbar-gutter: always reserve scrollbar space → fixed elements (the Notes
   button) don't shift between tabs.
   text-size-adjust: mobile browsers "inflate" the type of any block they consider
   a wide column of text — which is why a phone turned sideways rendered the
   pursuit card's "Troopers" label and the Notes form's "Note" heading a size or
   two larger than the identical labels beside them, and why rotating back did not
   always undo it. 100% pins every size to what the stylesheet says. */
html {
  scrollbar-gutter: stable;
  -webkit-text-size-adjust: 100%; text-size-adjust: 100%;
}
body {
  font-family: "Figtree", ui-rounded, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  background: var(--bg);
  background-image: var(--app-grad);
  background-attachment: fixed;
  color: var(--text);
  font-size: 14px;
  line-height: 1.5;
  display: flex;
  align-items: stretch;
  /* the height is set with the rest of the layout further down (`html, body`),
     in svh — see the note there for why not vh */
  /* no double-tap-to-zoom; the viewport meta blocks the pinch, this blocks the
     other way in (and removes the 300ms tap delay that came with it) */
  touch-action: manipulation;
}
/* …except on the maps, which do their own gestures: Mapbox needs the raw touches
   for pinch-zoom and two-finger rotate, on the main map and in Pick Location. */
.mapboxgl-map, .mapboxgl-canvas-container, .mapboxgl-canvas { touch-action: none; }
.hidden { display: none !important; }
/* the rail's tabs, until App.init knows which ones this role gets (see the inline
   script in index.html). visibility, not display: the rail keeps its size, so
   nothing around it moves when they appear. */
.side-nav.nav-booting { visibility: hidden; }
/* subtle fade/slide-in whenever a view becomes visible (tab switch) */
@keyframes viewIn { from { opacity: 0; transform: translateY(7px); } to { opacity: 1; transform: none; } }
.view:not(.hidden) { animation: viewIn .22s ease-out; }
@media (prefers-reduced-motion: reduce) { .view:not(.hidden) { animation: none; } }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ===== Sidebar — collapsible navy liquid-glass rail ===== */
.sidebar {
  /* above the sticky view header (60): the sidebar is its own stacking context, so the toggle's
     hover tooltip pops out to the right and would otherwise be painted under the header card */
  position: sticky; top: var(--rail-gap); align-self: flex-start; z-index: 70;
  height: calc(100vh - var(--rail-gap) * 2); flex-shrink: 0; width: 288px;
  margin: var(--rail-gap) 4px var(--rail-gap) 14px;
  display: flex; flex-direction: column; gap: 4px;
  padding: 17px 15px;
  background: var(--side-bg);
  border: 1px solid var(--side-brd);
  border-radius: 24px;
  box-shadow: none;
  transition: width .22s cubic-bezier(.4,0,.2,1), padding .22s cubic-bezier(.4,0,.2,1);
  /* The rail is exactly as tall as the window and the page behind it no longer
     scrolls, so anything that does not fit has nowhere to go — on a short window
     the footer buttons (theme, settings, admin, sign out) simply ran off the
     bottom edge and could not be reached. The TABS scroll instead; the footer,
     the one part that must always be reachable, stays put. */
  overflow: hidden;
}
/* flex-shrink 0: the emblem stays whole while the tabs beside it give up height */
.side-head { display: flex; align-items: center; gap: 10px; padding: 4px 6px 15px; margin-bottom: 8px; border-bottom: 1px solid var(--side-div); flex-shrink: 0; }
.side-brand { display: flex; align-items: center; gap: 10px; min-width: 0; flex: 1; }
.brand-emblem {
  width: 72px; height: 72px; object-fit: contain; display: block; flex-shrink: 0;
}
.brand-text { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
.brand-text strong { font-size: 18.5px; font-weight: 800; color: var(--side-strong); letter-spacing: .2px; white-space: nowrap; line-height: 1.12; }
.brand-text span { font-size: 10.5px; color: var(--side-fg-soft); letter-spacing: 1.6px; text-transform: uppercase; font-weight: 700; white-space: nowrap; margin-top: 5px; }

.side-toggle {
  position: relative; flex-shrink: 0; margin-top: 3px;
  border: 1px solid var(--side-brd); background: var(--side-hover); color: var(--side-fg);
  width: 36px; height: 36px; border-radius: 12px; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .16s, color .16s, border-color .16s;
}
.side-toggle:hover {
  background: color-mix(in srgb, var(--side-accent) 18%, transparent);
  color: var(--side-strong);
  border-color: color-mix(in srgb, var(--side-accent) 42%, transparent);
}
/* 🗑 The toggle's tooltip used to be a ::after right here, and it could not work:
   the rail is `overflow: hidden` (see .sidebar — that is what makes the TABS
   scroll on a short window), so it was cut off at the rail's own edge and "Open
   sidebar" read as "Op". No z-index fixes that — clipping is not painting order.
   It is the floating .tip-pop now, fixed to the body, wired in js/app.js.
   Do not reintroduce a tooltip that lives inside the rail. */

.side-nav {
  display: flex; flex-direction: column; gap: 4px; margin-top: 2px;
  /* and the part that scrolls when the rail is taller than the window */
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  scrollbar-width: none;                    /* the rail is chrome, not content */
}
.side-nav::-webkit-scrollbar { display: none; }
.side-nav::before {
  content: "Menu"; padding: 2px 13px 10px; font-size: 10.5px; font-weight: 700;
  letter-spacing: 1.7px; text-transform: uppercase; color: var(--side-fg-soft); opacity: .8;
}
.sidebar.collapsed .side-nav::before { display: none; }
.nav-btn {
  position: relative;
  display: flex; align-items: center; gap: 13px;
  border: none; background: transparent; color: var(--side-fg);
  padding: 12px 14px; border-radius: 14px; cursor: pointer; font-size: 16px; font-weight: 600;
  width: 100%; text-align: left; white-space: nowrap; overflow: hidden;
  transition: background .16s ease, color .16s ease;
}
.nav-btn .nav-label { font-size: 16px; }
/* sub-category (Former Troopers) — indented, smaller, muted */
.nav-btn.nav-sub { padding-left: 43px; padding-top: 8px; padding-bottom: 8px; }
.nav-btn.nav-sub .nav-label { font-size: 14.5px; opacity: .82; }
.sidebar.collapsed .nav-btn.nav-sub { padding-left: 0; }
.nav-btn.nav-collapsed { display: none; }
/* rail mode: the tab name is the floating .tip-pop beside the icon (see the
   mouseover handler in app.js). It used to be a ::before living inside the
   rail, and the rail is overflow:hidden — the long names lost their edges and
   the topmost tab's was pushed clean out of the top, so it never appeared. */
.sidebar.collapsed .nav-btn { overflow: visible; }
.nav-ico { display: inline-flex; line-height: 0; flex-shrink: 0; opacity: .82; }
.nav-btn:hover .nav-ico, .nav-btn.active .nav-ico { opacity: 1; }
.nav-btn:hover { background: var(--side-hover); color: var(--side-strong); }
.nav-btn.active {
  background: color-mix(in srgb, var(--side-accent) 16%, transparent); color: var(--side-active-fg); font-weight: 700;
}
.nav-btn.active::after {
  content: ""; position: absolute; left: 3px; top: 50%; transform: translateY(-50%);
  width: 3px; height: 18px; border-radius: 3px; background: var(--side-accent);
}
.nav-btn.active .nav-ico { color: var(--side-accent); }

.side-foot { margin-top: auto; display: flex; gap: 9px; padding: 13px 2px 2px; border-top: 1px solid var(--side-div); }

/* collapsed state — icons only (a touch wider so the above-icon tooltip fits inside the rail) */
.sidebar.collapsed { width: 104px; padding: 16px 14px; }
.sidebar.collapsed .side-head { flex-direction: column-reverse; align-items: center; gap: 12px; padding: 0 0 8px; }
.sidebar.collapsed .side-brand { justify-content: center; flex: initial; }
.sidebar.collapsed .brand-text,
.sidebar.collapsed .nav-btn > .nav-label { display: none; }
.sidebar.collapsed .nav-btn { justify-content: center; gap: 0; padding: 11px 0; }
.sidebar.collapsed .nav-btn.active::after { display: none; }

/* generic icon button (used across the app body + sidebar foot) */
.icon-btn {
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
  width: 38px; height: 38px; border-radius: 999px; cursor: pointer; font-size: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .18s ease, color .18s ease, border-color .18s ease, transform .12s ease;
}
.icon-btn:hover {
  background: var(--accent-soft); color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--border));
}
.icon-btn:active { transform: translateY(1px); }
/* sidebar foot buttons — full-width, follow the rail palette */
.side-foot .icon-btn {
  flex: 1; width: auto; height: 44px; border-radius: 14px;
  border-color: var(--side-brd); background: var(--side-hover); color: var(--side-fg);
}
.side-foot .icon-btn:hover {
  background: color-mix(in srgb, var(--side-accent) 20%, transparent); color: var(--side-strong);
  border-color: color-mix(in srgb, var(--side-accent) 42%, transparent);
}
/* Administration reaches everything shared — roles, deletions, the whole
   dataset. Orange under the cursor, one step short of Sign out's red: a warning
   that this is not an ordinary panel, not that you are about to lose anything.
   Its own token rather than --warn: that one is the app's amber and reads
   yellow in dark. This is the same orange the "changed" badge wears. */
:root { --admin-glow: #c2691a; }
html[data-theme="dark"] { --admin-glow: #e0913f; }
#adminBtn { transition: background .18s ease, color .18s ease, border-color .18s ease, box-shadow .18s ease, transform .12s ease; }
#adminBtn:hover {
  background: color-mix(in srgb, var(--admin-glow) 18%, transparent); color: var(--admin-glow);
  border-color: color-mix(in srgb, var(--admin-glow) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--admin-glow) 35%, transparent),
              0 0 18px -2px color-mix(in srgb, var(--admin-glow) 60%, transparent);
}
#adminBtn:hover svg { color: var(--admin-glow); }

/* Settings is the neutral one of the three, so it glows plain white rather than
   claiming a colour — the same shape of glow Administration and Sign out use,
   which is what makes those two read as warnings beside it. In the light theme
   white on a pale rail would be invisible, so it borrows the ink instead. */
:root { --set-glow: #3f4a5c; }
html[data-theme="dark"] { --set-glow: #ffffff; }
#settingsBtn { transition: background .18s ease, color .18s ease, border-color .18s ease, box-shadow .18s ease, transform .12s ease; }
#settingsBtn:hover {
  background: color-mix(in srgb, var(--set-glow) 14%, transparent); color: var(--set-glow);
  border-color: color-mix(in srgb, var(--set-glow) 45%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--set-glow) 28%, transparent),
              0 0 18px -2px color-mix(in srgb, var(--set-glow) 50%, transparent);
}
#settingsBtn:hover svg { color: var(--set-glow); }

/* Sign out is the one button in the rail you cannot undo — it turns red under the
   cursor instead of following the accent, so a stray hover on the way to Settings
   reads as a warning rather than as one more blue button. */
#signOutBtn { transition: background .18s ease, color .18s ease, border-color .18s ease, box-shadow .18s ease, transform .12s ease; }
#signOutBtn:hover {
  background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--danger) 35%, transparent),
              0 0 18px -2px color-mix(in srgb, var(--danger) 60%, transparent);
}
#signOutBtn:hover svg { color: var(--danger); }
.sidebar.collapsed .side-foot { flex-direction: column; align-items: stretch; }
.sidebar.collapsed .side-foot .icon-btn { flex: initial; width: 100%; }

/* The band under the rail, edge to edge across the page. Same height as the gap
   the rail leaves below itself (--rail-gap), and painted with the same recipe the
   sticky header card uses — background-color + app-grad at a FIXED attachment —
   so the fill is pixel-identical to the true page background wherever it sits
   rather than seaming against the gradient in light mode. In dark, --app-grad is
   none and --bg is #101117, so this paints exactly that flat colour.
   z-index sits above the sticky header (60) and below the rail (70): the rail's
   rounded bottom corner keeps its own edge, the page content scrolls under. */
.under-rail {
  position: fixed; left: 0; right: 0; bottom: 0;
  /* safe area: on a home-screen app the band also has to cover the strip the
     home indicator sits in, or the page shows through under it */
  height: calc(var(--rail-gap) + env(safe-area-inset-bottom, 0px));
  z-index: 65; pointer-events: none;
  background-color: var(--bg);
  background-image: var(--app-grad);
  background-attachment: fixed;
}
/* safe area: the same band at the top, for the strip under the status bar. Zero
   tall in a browser tab, where there is no inset — see index.html. */
.over-header {
  position: fixed; left: 0; right: 0; top: 0;
  height: env(safe-area-inset-top, 0px);
  z-index: 66; pointer-events: none;
  background-color: var(--bg);
  background-image: var(--app-grad);
  background-attachment: fixed;
}
/* (This used to be followed by `.view-head { top: env(safe-area-inset-top) }` to
   park the sticky header below that strip. It never took effect — the header's
   own block further down restates `top: 0` and, at equal specificity, wins. And
   by a 300px ::after that covered the strip above the header, which never worked
   on the device either. Both are gone: no page scrolls any more, so nothing can
   get above the header to begin with.) */

/* ===== Layout ===== */
/* The page does not scroll here either — the view does. Same model as the phone
   and for the same reason, one link shorter: the height goes screen → main →
   view, and the view is the scroller. No wrapper box is needed on a wide screen
   (the phone blocks add one; see them for why it has to exist there), because
   the header can simply stay STICKY inside the view — there is no status-bar
   strip above it to keep clear of.
   Two things this deliberately does not disturb: the scrollbar sits on .view,
   whose outer edge is main's edge is the window's edge, so it stays exactly
   where the page scrollbar used to be; and the header keeps sticking at the top
   of the view, which starts at the top of the window — so it does not move
   either. What changes is only that the document itself now has nowhere to go. */
html, body { height: 100vh; height: 100svh; overflow: hidden; }
main { flex: 1; min-width: 0; padding: 0; display: flex; flex-direction: column; min-height: 0; }
/* full-width content column, and the box that scrolls */
.view { max-width: none; margin: 0; padding: 0 40px 48px; flex: 1; min-height: 0; overflow-y: auto; }
/* The gap the eye reads starts at the SIDEBAR's edge, and the sidebar carries
   its own 4px right margin — so 26px of padding is what measures 30px on screen.
   These five views share it; Pursuits and Scanner keep their own wide gutter. */
#view-troopers, #view-former { padding-left: 26px; }
/* Statistics and the Map share the Pursuit dashboard's frame, like History and
   Scanner already do — see the header rules further down */
#view-stats { padding-left: 150px; padding-right: 154px; }
/* History uses the Pursuit dashboard's gutter instead — see the header rules below */
#view-history { padding-left: 150px; padding-right: 154px; }
/* The map page is exactly one screen tall and does not scroll: the header takes
   what it needs and the panel gets the rest, so the map's lower edge lands on the
   same line as the rail's. The phone block already worked this way — above 700px
   the map was still sized by a fixed `calc(100vh - 210px)` guess and stopped a
   good stretch short of the rail's bottom corner. */
/* 26px both sides. The gutter you SEE on the left starts at the rail's edge, and
   the rail carries its own 4px right margin — so 26 of padding measures 30 on
   screen there. The right had a literal 30 and came out four pixels wider than
   the left, which is visible on a full-width map. */
#view-map { display: flex; flex-direction: column; height: 100vh; height: 100svh; padding: 0 154px var(--rail-gap) 150px; }
#view-map .map-panel { flex: 1; min-height: 0; }
#view-map #mapContainer { height: 100%; min-height: 0; }
/* Both headers recess into that frame by exactly as much as Pursuits' does, so
   the card lands on the same two lines on every page. */
#view-stats .view-head, #view-map .view-head { margin: 0 -154px 0 -150px; padding: 30px 172px 30px 168px; }
#view-stats .view-head::before, #view-map .view-head::before { inset: 14px 154px 14px 150px; }
/* The gap under the Statistics header, set on the KPI row rather than on the
   header. Taking it out of the header's padding worked, but it made the header
   itself two pixels shorter than every other view's — one card in a row of
   identical cards, off by just enough to see. The header is left exactly as it
   is everywhere else and the counters carry the difference. */
#view-stats .stat-kpis { margin-top: 8px; }
.view-head {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  /* z-index above a hovered .pcard (55): when zoomed out so a card overlaps the sticky header, the
     card's hover-lift must stay UNDER the header, not poke over it */
  /* The 14px breathing room above/below the card is baked INTO the header box as opaque bg
     padding strips (top/bottom 14px), with the ::before card inset 14px vertically. So both gaps
     stick WITH the header (top:0 → card floats at 14px, matching the sidebar) and stay visible
     while scrolling — the box's bg fill masks content right up to the card edges, top and bottom.
     Horizontally this is the plain/default look (card fills its box edge-to-edge, no side gap,
     positioned by each view's own padding) — Pursuits alone opts into a wider 150/40 side gap,
     see the #view-pursuits override below. */
  position: sticky; top: 0; z-index: 60;
  margin: 0; padding: 30px 18px;
  /* One height for every header, whether or not the view has buttons in it —
     30 + a 38px button + 30. History and the Map have none and were coming out
     4px shorter than the rest, which put their headings 2px higher. (The phone
     block has carried the same rule at its own 64px for a while.) */
  min-height: 98px; align-content: center;
  /* same recipe as body (bg + app-grad, fixed attachment) so this fill is pixel-identical to the
     true page background wherever it sits on screen — in light mode a flat --bg here visibly
     seamed against body's gradient; fixed-attachment makes both paint from the viewport origin.
     (dark: --app-grad is none, so this is equivalent to a flat --bg, unchanged.) */
  background-color: var(--bg);
  background-image: var(--app-grad);
  background-attachment: fixed;
}
.view-head::before {
  content: ""; position: absolute; inset: 14px 0; z-index: -1;
  background: var(--chrome-bg);
  border: 1px solid var(--chrome-brd);
  border-radius: 14px;
}
/* Troopers: a box of its own at the right end of the header row, holding the
   one button that opens the side panel. The header card stops short of it (the
   rule below), so the two read as two boxes with a gap, not one long bar. */
#view-troopers .view-head::before { right: 52px; }
/* …and the row's own content stops there too, or Add Card slides under the box */
#view-troopers .view-head { padding-right: 70px; }
/* 38px wide, like the eye in the filter row directly below it — both end at the
   same right edge, so they read as one column */
.head-side {
  position: absolute; top: 14px; right: 0; bottom: 14px; width: 38px;
  display: flex; align-items: center; justify-content: center;
  /* the box itself is the red mark now, not only the glyph inside it: this is the one control on
     the page that rewrites a trooper, and it should read as that from across the room. Same
     tinted-red recipe as .tag.red, so it sits in the palette rather than shouting over it. */
  background: color-mix(in srgb, var(--danger) 16%, var(--chrome-bg));
  border: 1px solid color-mix(in srgb, var(--danger) 45%, var(--chrome-brd));
  border-radius: 14px;
}
/* 38px square — the same button the eye in the filter row below it is, so the
   two line up down the right edge. Hovering does not recolour it: the icon is
   already the red it should be, and it glows instead. */
/* The button FILLS its box rather than measuring 38px of its own: on a phone the header is shorter,
   the box comes out 36px tall, and a 38px button inside it bulged past the rounded corners at the
   top and bottom. Filling it keeps the two the same shape at every header height. */
.head-side-btn { color: var(--danger); width: 100%; height: 100%; border-radius: 13px; transition: filter .14s; }
.head-side-btn:hover:not(:disabled) {
  background: transparent; color: var(--danger);
  filter: drop-shadow(0 0 7px color-mix(in srgb, var(--danger) 75%, transparent));
}
.head-side-btn:disabled { opacity: .45; cursor: default; }
/* Pursuits only: recess the card 150px/154px within a full-bleed box (same mechanism as the 14px
   top/bottom gap) instead of the plain edge-to-edge look every other view keeps. Negative margins
   break the box out of #view-pursuits's own 150/154 padding to the true edge, then this padding
   places the card back at exactly 150px left / 154px right — flush with the pursuit cards below —
   with the card's own 18px internal text padding folded in (168 = 150+18, 172 = 154+18) so its
   internal proportions stay identical to the plain look.
   Why 154 on the right and 150 on the left: the gap the eye reads on the left starts at the
   SIDEBAR's edge, and the sidebar carries its own 4px right margin on top of the 150px gutter.
   154 makes the right-hand whitespace measure identical to that (user directive).
   Reaching the true left edge is what hides the hover outcome label: it floats into that gutter
   (widest is "FLED ON FOOT", 112px + a 12px offset), so the box's opaque background must cover the
   whole gutter or the label peeks out past the header while scrolling. Safe to go full-bleed now
   that the app has no shadows — this used to clip the sidebar's box-shadow. */
#view-pursuits .view-head { margin: 0 -154px 0 -150px; padding: 30px 172px 30px 168px; }
#view-pursuits .view-head::before { inset: 14px 154px 14px 150px; }
/* Stop the header short at the widest thing each of these tabs actually renders, instead of
   running to the content edge: the right edge then lines up with the content below it on a wide
   window. Each cap equals that view's own content max-width, so on a narrow window (content
   already narrower than the cap) header and content still both fall back to the available width.
   The filter row under the header gets the same cap, so a right-aligned control in it (Troopers'
   placeholder toggle) lands under the header's right edge instead of way out at the content edge. */
#view-troopers .view-head, #view-troopers .filters,
#view-former .view-head, #view-former .filters { max-width: 2094px; }   /* .trooper-grid: 10*195 + 9*16 gap */
/* History sits in the same frame as the Pursuit dashboard — the wide 150/154
   gutter and the header card recessed into it by exactly as much. It used to
   have its own, much narrower 26px version of the same idea, which read as a
   different page rather than the same one showing something else. The header and
   the filter row lost their 1170px cap with it: in this frame the card is meant
   to reach both gutters, the way Pursuits' does. The entries themselves keep
   their own width (.hist-row, 1150px) — that is line length, not page margin. */
#view-history .view-head { margin: 0 -154px 0 -150px; padding: 30px 172px 30px 168px; }
#view-history .view-head::before { inset: 14px 154px 14px 150px; }
/* Scanner shares the Pursuit dashboard's frame — see #view-scanner's padding
   below. No width cap on the header or the master row: in this frame they reach
   both gutters, the way Pursuits' header does. */
.view-head h1 {
  font-size: 33px; font-weight: 800; margin: 0; letter-spacing: -.9px; line-height: 1.02;
  position: relative; padding-left: 17px;
}
.view-head h1::before {
  content: ""; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
  width: 5px; height: .74em; border-radius: 4px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent), #fff 20%), var(--accent));
}
.view-head .spacer { flex: 1; }
/* heading count — plain inline count, no badge/label look */
.head-count {
  align-self: flex-end; color: var(--text-soft); font-size: 15px; font-weight: 600;
  letter-spacing: .1px; margin-bottom: 5px; padding: 0; background: none; border: none;
}
.muted { color: var(--text-soft); }

/* ===== Buttons ===== */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border: 1px solid var(--glass-brd); background: var(--surface-2); color: var(--text);
  padding: 10px 15px; border-radius: 12px; cursor: pointer; font-size: 14px; font-weight: 600;
  transition: background .16s ease, border-color .16s ease, transform .1s ease, filter .16s ease;
}
.btn:hover { background: color-mix(in srgb, var(--surface-2) 82%, #fff); border-color: color-mix(in srgb, var(--accent) 32%, var(--glass-brd)); }
.btn:active { transform: translateY(1px); }
/* blocked by an open right-edge panel (Change while Notes is open — they'd stack on each other).
   pointer-events:none rather than overriding the hovers: .btn.danger:hover et al are later in the
   file with equal specificity, so they'd win — with no pointer events the hover never matches. */
.btn:disabled { opacity: .38; pointer-events: none; }
.btn .icon { opacity: .85; }
.btn.primary .icon, .btn.primary:hover .icon { opacity: 1; }
/* Add buttons: same soft-outline look as Save/Change, but in the app's own brand colour */
.btn.primary { color: var(--btn-brand-fg); border-color: color-mix(in srgb, var(--btn-brand) 55%, transparent); background: color-mix(in srgb, var(--btn-brand) 16%, transparent); padding: 10px 20px; letter-spacing: .2px; }
.btn.primary:hover { background: var(--btn-brand); color: #fff; border-color: var(--btn-brand); box-shadow: 0 0 0 1px var(--btn-brand), 0 6px 20px -4px color-mix(in srgb, var(--btn-brand) 55%, transparent); }
.btn.primary:active { transform: translateY(1px); }
.btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.btn.danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); }
/* Save / Change buttons: same soft-outline look as the danger buttons, but green */
.btn.success { color: var(--success); border-color: color-mix(in srgb, var(--success) 42%, transparent); background: color-mix(in srgb, var(--success) 12%, transparent); }
.btn.success:hover { background: var(--success); color: #fff; border-color: var(--success); box-shadow: 0 0 0 1px var(--success), 0 6px 20px -4px color-mix(in srgb, var(--success) 55%, transparent); }
/* The Admin rung's own orange, for the one button that hands those rights out.
   Same token the Admin badge and the role switch use, so the confirmation is
   the colour of the thing it leads to. */
.btn.amber-btn { color: var(--admin-glow); border-color: color-mix(in srgb, var(--admin-glow) 42%, transparent); background: color-mix(in srgb, var(--admin-glow) 12%, transparent); }
.btn.amber-btn:hover { background: var(--admin-glow); color: #fff; border-color: var(--admin-glow); box-shadow: 0 0 0 1px var(--admin-glow), 0 6px 20px -4px color-mix(in srgb, var(--admin-glow) 55%, transparent); }
.btn.sm { padding: 6px 11px; font-size: 12px; border-radius: 9px; gap: 5px; }

/* consistent icons + icon-with-text buttons/links */
.icon { display: inline-block; vertical-align: middle; flex-shrink: 0; }
.btn-icon { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.icon-btn .icon { display: block; }

/* ===== Cards / panels (liquid glass) ===== */
.panel {
  background: var(--surface);
  border: 1px solid var(--glass-brd);
  border-radius: var(--radius);
  box-shadow: none;
}

/* ===== Filters ===== */
.filters {
  display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 14px;
}
/* consistent search field across all pages — distinct monospace look */
.search-field {
  min-width: 280px; height: 42px; padding: 0 15px 0 40px; border: 1px solid var(--glass-brd); border-radius: 13px;
  background: var(--surface-2); color: var(--text); font-size: 16px; font-weight: 600;
  font-family: inherit; letter-spacing: .2px;
  transition: border-color .16s ease, box-shadow .16s ease, background .16s ease;
}
.search-field::placeholder { font-weight: 700; color: var(--text-soft); }
.search-field:hover { border-color: color-mix(in srgb, var(--accent) 30%, var(--glass-brd)); }
.search-field:focus {
  outline: none; background: var(--surface);
  border-color: color-mix(in srgb, var(--accent) 65%, var(--glass-brd));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent);
}
.search-wrap .search-ico { transition: color .16s ease; }
.search-wrap:focus-within .search-ico { color: var(--accent); }
/* magnifier icon inside the pill */
.search-ico {
  position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
  display: inline-flex; color: var(--text-soft); pointer-events: none;
}
.search-field::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }

/* search field with a clear (✕) button */
.search-wrap { position: relative; display: inline-flex; align-items: center; }
.search-wrap .search-field { padding-right: 30px; }
.search-clear {
  position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
  border: none; background: transparent; color: var(--text-soft); cursor: pointer;
  width: 20px; height: 20px; border-radius: 50%; font-size: 12px; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
.search-clear:hover { background: var(--surface-2); color: var(--text); }

/* searchable dropdown (combobox) */
.combo { position: relative; }
.combo-input { width: 100%; padding-right: 28px !important; }
/* rich selected-value overlay (e.g. bold dog name + thin handler) shown over the input */
.combo-display {
  position: absolute; left: 0; top: 0; bottom: 0; right: 28px;
  display: none; align-items: center; padding: 0 9px;
  pointer-events: none; white-space: nowrap; overflow: hidden;
  font-size: 13px;
}
.combo[data-clearable] .combo-display { right: 48px; }
.combo.has-display .combo-display { display: flex; }
.combo.has-display .combo-input { color: transparent !important; }
.combo-display b { font-weight: 700; }
.combo-caret { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); color: var(--text-soft); cursor: pointer; display: inline-flex; padding: 4px; }
.combo-caret:hover { color: var(--text); }
/* clear (deselect) button — sits left of the caret when the field has a value */
.combo[data-clearable] .combo-input { padding-right: 48px !important; }
.combo-clear { position: absolute; right: 26px; top: 50%; transform: translateY(-50%); color: var(--text-soft); cursor: pointer; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 5px; font-size: 12px; }
.combo-clear:hover { color: var(--danger); background: rgba(217,52,43,.14); }
.combo-clear.hidden { display: none; }
.combo-list {
  position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 130;
  background: var(--surface); border: 1px solid var(--border); border-radius: 13px;
  box-shadow: none; max-height: 240px; overflow-y: auto; padding: 5px;
  scrollbar-width: thin; scrollbar-color: var(--border) transparent;
}
.combo-list::-webkit-scrollbar { width: 8px; }
.combo-list::-webkit-scrollbar-track { background: transparent; }
.combo-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; border: 2px solid var(--surface); }
.combo-list::-webkit-scrollbar-thumb:hover { background: var(--text-soft); }
.combo-opt { padding: 7px 11px; cursor: pointer; font-size: 13px; border-radius: 6px; }
.combo-opt .tag { font-size: 12.5px; padding: 3px 11px; }
.combo-opt:hover, .combo-opt.active { background: var(--accent-soft); color: var(--accent); }
.combo-opt.has-del { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.combo-opt-lbl { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.combo-acts { display: inline-flex; align-items: center; gap: 2px; flex-shrink: 0; }
.combo-del { opacity: 0; flex-shrink: 0; color: var(--text-soft); font-size: 12px; line-height: 1; padding: 3px 5px; border-radius: 5px; }
.combo-opt:hover .combo-del { opacity: .6; }
.combo-del:hover { opacity: 1 !important; color: #fff; background: var(--danger); }
/* rename (pencil) — mirrors the ✕ delete affordance, accent instead of danger on hover */
.combo-edit { opacity: 0; flex-shrink: 0; color: var(--text-soft); line-height: 0; padding: 3px 4px; border-radius: 5px; display: inline-flex; }
.combo-opt:hover .combo-edit { opacity: .55; }
.combo-edit:hover { opacity: 1 !important; color: var(--accent-ink); background: var(--accent); }
.combo-opt.editing { padding: 3px 5px; background: transparent !important; color: inherit !important; }
.combo-edit-input { width: 100%; padding: 5px 8px !important; font-size: 13px !important; }
.combo-empty { padding: 7px 11px; color: var(--text-soft); font-size: 13px; }

/* Pills / tags */
.tag {
  display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px;
  font-weight: 600; background: var(--surface-2); color: var(--text-soft); white-space: nowrap;
}
/* outcome labels carry a border in their own text colour */
.tag.outcome { border: 1px solid currentColor; }
.tag.green { background: rgba(31,157,85,.14); color: var(--success); }
.tag.red { background: rgba(217,52,43,.14); color: var(--danger); }
.tag.amber { background: rgba(184,134,11,.16); color: var(--warn); }
.tag.blue { background: var(--accent-soft); color: var(--accent); }
/* FLED ON FOOT outcome — a lighter sky-blue (distinct from the navy Troop chips) */
/* both of these are LIGHT-mode values — dark overrides each on the next line */
.tag.fled { background: color-mix(in srgb, #38bdf8 16%, transparent); color: #0369a1; }
html[data-theme="dark"] .tag.fled { color: #7dd3fc; }
.tag.gray { background: var(--surface-2); color: #52607a; }
html[data-theme="dark"] .tag.gray { color: #c5ccd6; }
.tag.fade { animation: tag-fade 2.2s ease-in-out infinite; }
@keyframes tag-fade { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }

/* ===== Pursuit list (card / list hybrid) ===== */
.pursuit-list { display: flex; flex-direction: column; gap: 12px; }
.pcard {
  position: relative;   /* overflow stays visible so the hover outcome (.pcard-oc) can float outside */
  background: var(--surface);
  border: 1px solid var(--glass-brd); border-radius: 18px;
  padding: 15px 18px 15px 22px; cursor: pointer;
  transition: border-color .14s, transform .14s, box-shadow .14s;
}
/* outcome-coloured accent rail on the left edge */
.pcard::before {
  /* inset pill rail — never pokes past the card's rounded corners */
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 4px;
  background: var(--oc, var(--text-soft)); opacity: .9;
  border-radius: 999px;
}
/* Deceased → the rail blinks in sync with the outcome label (same keyframes + timing) */
.pcard-deceased::before { animation: tag-fade 2.2s ease-in-out infinite; }
.pcard:hover {
  z-index: 55;   /* above the neighbouring cards, so the floating outcome label isn't cut off */
  border-color: color-mix(in srgb, var(--oc, var(--accent)) 60%, var(--glass-brd));
  transform: translateY(-2px);
  box-shadow: 0 12px 40px -12px color-mix(in srgb, var(--oc, var(--accent)) 55%, transparent);
}
/* the whole card is ONE band of labelled columns — same treatment for every value */
/* room on the left so the hover outcome label fits BETWEEN sidebar and card
   (the whole view shifts, so head/search/cards stay aligned with each other).
   150px clears the widest label ("FLED ON FOOT" 112px + its 12px offset = 124px) with room to
   spare, so it never pokes out of the gutter. */
/* the 150px left gutter holds the hover outcome label; 154px on the right mirrors the gap the eye
   reads on the left (150px gutter + the sidebar's own 4px margin) */
#view-pursuits { padding-left: 150px; padding-right: 154px; }
/* slightly larger text throughout the pursuit card */
.pcard .case-pill { font-size: 14px; }
.pcard .tag { font-size: 12.5px; padding: 4px 11px; }
.pcard .outcome { font-size: 12.5px; }
/* trooper / K9 labels on the card are clickable → their detail window */
.pcard-trp { cursor: pointer; transition: filter .12s; }
.pcard-trp:hover { filter: brightness(1.25); text-decoration: underline; }
/* the outcome label floats OUTSIDE the card (left of the coloured rail) while hovering it */
.pcard-oc {
  position: absolute; right: calc(100% + 12px); top: 50%; z-index: 6;
  transform: translateY(-50%) translateX(8px);
  opacity: 0; pointer-events: none;
  transition: opacity .16s ease, transform .16s ease;
}
.pcard:hover .pcard-oc { opacity: 1; transform: translateY(-50%) translateX(0); }
.pcard-oc .tag { background: var(--surface); }   /* solid backdrop, it floats over whatever is behind */
.pcard-body { display: flex; align-items: stretch; gap: 18px; }
.pcard-row2 { margin-top: 13px; padding-top: 13px; border-top: 1px solid var(--glass-brd); }
/* when the Troopers column wraps to several lines, the headings stay on top —
   only the VALUES centre vertically in the space below */
.pcard-row2 .pcard-sec:not(.grow) .pcard-chips { flex: 1; align-items: center; align-content: center; }
/* the location pin sits at the far right, centred on the band */
.pcard-locwrap { display: flex; align-items: center; margin-left: 2px; border-left: 1px solid var(--glass-brd); padding-left: 18px; }
/* labeled sections: micro heading + chips, separated by vertical hairlines */
.pcard-sec { display: flex; flex-direction: column; gap: 7px; min-width: 0; flex-shrink: 0; }
/* The Troopers column may be squeezed by a long Event Tags list beside it — but never past the
   width of its own chips: min-content here is a WHOLE name chip (they stay nowrap), so a name can
   never be clipped or run under Troop / Vehicle. What gives way instead is the Event Tags list,
   which wraps onto a second row of chips — .pcard-chips already wraps, it just needed permission
   to be narrower than its one-line width. Troopers shrinks a fifth as fast as the rest, so it is
   the last section to lose width, not the first. Holds on a narrowed window too. */
.pcard-sec.grow { flex: 1; min-width: min-content; }
.pcard-row2 .pcard-sec { flex-shrink: 1; min-width: min-content; }
.pcard-row2 .pcard-sec.grow { flex-shrink: .2; }
.pcard-sec + .pcard-sec { border-left: 1px solid var(--glass-brd); padding-left: 18px; }
.pcard-lbl {
  font-size: 11px; font-weight: 800; letter-spacing: 1.3px; text-transform: uppercase;
  color: var(--text-soft); opacity: .85;
}
.pcard-chips { display: flex; flex-wrap: wrap; gap: 5px; align-items: flex-start; }

/* ===== the pursuit card's fold =====
   An arrow at the foot of the card opens one more band: why the pursuit started on the left, and
   whatever was written about it on the right. Both are sentences rather than chips, so they read
   as paragraphs — and both are worth having on the list itself, where the Watch window is a click
   further away. The card's bottom padding grows with the arrow so the two never touch. */
.pcard { padding-bottom: 26px; }
/* Wide, not tall: it is a mouse target at the foot of a full-width card, and 34px of it was a lot
   to aim at. The height stays small so the band it sits in does not grow. */
.pcard-fold {
  position: absolute; left: 50%; bottom: 3px; transform: translateX(-50%);
  width: 140px; height: 20px; padding: 0; border: 0; background: transparent;
  color: var(--text-soft); cursor: pointer; border-radius: 8px;
  display: flex; align-items: center; justify-content: center;
  transition: color .14s, background .14s;
  /* No grey box under the finger on a phone: the arrow's whole job is to open the band under it,
     and the browser's own press flash read as the button staying switched on. (Rick, 2026-09-06) */
  -webkit-tap-highlight-color: transparent;
}
/* …and the hover fill only where there IS a pointer. A touch screen never un-hovers, so the fill
   stayed on the arrow that was last pressed — the same "still lit" look, from the other side. */
@media (hover: hover) {
  .pcard-fold:hover { color: var(--text); background: var(--surface-2); }
}
.pcard-fold .icon { transition: transform .16s ease; }
.pcard.folded-open .pcard-fold .icon { transform: rotate(180deg); }
/* shut by default; the class on the card is the only thing that opens it */
.pcard-fold-body { display: none; }
.pcard.folded-open .pcard-fold-body { display: flex; }
/* the two halves share the width evenly — a long reason must not squeeze the note to nothing */
.pcard-fold-body > .pcard-sec { flex: 1 1 0; min-width: 0; }
.pcard-para {
  font-size: 13.5px; line-height: 1.5; color: var(--text);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.pcard .loc-link, .pcard .loc-none {
  display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: 11px;
  /* pin + outline both carry the accent (--accent-deep = the same navy in dark, a reading-weight
     version of it in light, so the icon stays legible on the pale card) */
  border: 1px solid var(--accent-deep);
  background: var(--surface-2); color: var(--accent-deep); cursor: pointer;
  transition: color .14s, border-color .14s, box-shadow .14s;
}
/* hover only lights the button up — no fill, so the pin keeps reading as an outline control */
.pcard .loc-link:hover {
  box-shadow: 0 0 14px 2px color-mix(in srgb, var(--accent) 75%, transparent);
}
/* no end location → the crossed-out pin reads as a gap in the data (red) */
.pcard .loc-none { cursor: default; opacity: 1; color: var(--danger); border-color: color-mix(in srgb, var(--danger) 35%, var(--glass-brd)); }

/* compact pursuit form */
.pursuit-form { display: flex; flex-direction: column; gap: 12px; }
.pursuit-form .field { gap: 4px; }
.inline-row { display: flex; gap: 8px; align-items: center; }
.row-date { display: flex; gap: 12px; align-items: flex-start; }
.rating-field { margin-left: 16px; }
.row-vl { display: flex; gap: 12px; align-items: flex-start; }
/* Reason runs out to the same right edge as the Notes box under it (its 235px is
   an inline style, so this has to win on specificity) */
.row-vl > .reason-field { flex: 1; width: auto !important; }
.troopers-field { max-width: 400px; }
.troopers-field.troop-field { max-width: 150px; }
/* let inputs shrink inside fixed-width fields (otherwise calendar/# overflow) */
.dp-text, .input-prefix input, .combo-input, .field input.sm-input { min-width: 0; }
.dp { min-width: 0; }
/* the rating has no box of its own, so it borrows the one every other empty
   field gets — same red line, same ring, so the reminder reads as one thing */
.field-invalid.stars {
  border: 1px solid var(--danger) !important;
  box-shadow: 0 0 0 2px rgba(217, 52, 43, .18) !important;
  border-radius: 8px; padding: 2px 4px;
}

/* star rating (form) */
.stars { display: flex; gap: 2px; }
.stars .star {
  border: none; background: none; padding: 2px; cursor: pointer;
  color: var(--text-soft); display: inline-flex; line-height: 0;
}
.stars .star.on { color: #f5b301; }
.stars .star.on svg { fill: #f5b301; }
.stars .star:hover { color: #f5b301; }

/* star rating (table, read-only) */
.stars-mini { display: inline-flex; gap: 1px; }
.stars-mini .star { color: var(--text-soft); line-height: 0; display: inline-flex; }
.stars-mini .star svg { width: 13px; height: 13px; }
.stars-mini .star.on { color: #f5b301; }
.stars-mini .star.on svg { fill: #f5b301; }

/* ===== Forms ===== */
.field { display: flex; flex-direction: column; gap: 5px; }
.field label { font-size: 12px; font-weight: 600; color: var(--text-soft); }
.field input, .field select, .field textarea {
  padding: 11px 13px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text); font-size: 14px; font-family: inherit;
  transition: border-color .16s ease;
}
.field input:focus, .field select:focus, .field textarea:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.field textarea { resize: vertical; min-height: 60px; }
.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }
/* the pursuit form's last row: picture slot on the left, Delete + Save beside it
   on the same line, aligned to the bottom of the drop zone */
.pf-foot { display: flex; align-items: flex-end; gap: 14px; }
.pf-foot > .field { flex: 1; min-width: 0; }
.pf-foot .form-actions { flex-shrink: 0; margin-top: 0; margin-bottom: 2px; }
/* more than one row of pictures → the buttons move under them, and the grid gets
   the full width of the form to split five ways (js/pursuits.js sets the class) */
.pf-foot.pf-stacked { flex-direction: column; align-items: stretch; gap: 10px; }
.pf-foot.pf-stacked .form-actions { justify-content: flex-end; margin-bottom: 0; }
/* holds the room without being there — visibility, not display, so the row
   measures the same on Add as on Edit and the picture slots come out one size.
   Also out of the tab order, which `display: none` and this both manage but
   `opacity: 0` would not. */
.pf-ghost { visibility: hidden; pointer-events: none; }
/* …and when the buttons drop under the pictures there is nothing left to line up
   with, so the empty space goes rather than sitting beside Save */
.pf-foot.pf-stacked .pf-ghost { display: none; }
.hint { font-size: 11px; color: var(--text-soft); }

/* compact fields */
.compact-row { display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: 10px; margin-bottom: 6px; }
.sm-input { padding: 6px 9px !important; font-size: 13px !important; }
.field input:disabled, .field select:disabled, .sm-input:disabled {
  opacity: .5; cursor: not-allowed; background: var(--surface-2);
}
.icon-btn.sm { width: 30px; height: 30px; font-size: 14px; }
.grow { flex: 1; }
.danger-icon { color: var(--danger); }
/* close (X) + delete buttons → red fill + red glow on hover.
   NOTE: standard close-button convention = class "icon-btn close-x" (no title/tooltip). */
.danger-icon:hover, #modalClose:hover, #sw-close:hover, .close-x:hover {
  background: var(--danger) !important; color: #fff !important; border-color: var(--danger) !important;
  box-shadow: 0 0 15px 2px color-mix(in srgb, var(--danger) 72%, transparent) !important;
}
/* small text ✕ removers → red + subtle glow */
.search-clear:hover, .combo-clear:hover, .token button:hover, .photo-rm:hover,
.ts-rm:hover, .anno-rm:hover {
  color: var(--danger) !important;
  text-shadow: 0 0 9px color-mix(in srgb, var(--danger) 75%, transparent);
}

/* date picker */
.dp { position: relative; display: flex; gap: 6px; align-items: stretch; }
.dp-text { flex: 1; }
.dp-btn {
  border: 1px solid var(--border); background: var(--surface); color: var(--text-soft);
  border-radius: 8px; width: 38px; cursor: pointer; flex-shrink: 0;
  display: flex; align-items: center; justify-content: center;
}
.dp-btn:hover { background: var(--surface-2); }
.dp-cal {
  position: absolute; top: calc(100% + 6px); left: 0; z-index: 120;
  background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
  box-shadow: none; padding: 10px; width: 252px;
}
/* …opening upwards instead, when there is no room below it inside the window
   it lives in. That is the alternative to making the window taller, which is
   what it used to do (DatePicker.reveal decides which). */
.dp-cal.dp-up { top: auto; bottom: calc(100% + 6px); }
/* …and the same the other way for a narrow column: the calendar's right edge on the field's, so
   it opens to the LEFT of the button instead of hanging out past the panel (DatePicker.reveal
   measures which — a field with room to its right keeps the default). */
.dp-cal.dp-left { left: auto; right: 0; }
.dp-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
.dp-title { font-weight: 600; font-size: 13px; }
.dp-nav {
  border: 1px solid var(--border); background: var(--surface); color: var(--text);
  width: 28px; height: 28px; border-radius: 7px; cursor: pointer; font-size: 16px; line-height: 1;
}
.dp-nav:hover { background: var(--surface-2); }
.dp-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; }
.dp-wd { text-align: center; font-size: 11px; color: var(--text-soft); padding: 4px 0; font-weight: 600; }
.dp-cell {
  border: none; background: transparent; color: var(--text); border-radius: 7px;
  height: 30px; cursor: pointer; font-size: 12px;
}
.dp-cell:hover { background: var(--surface-2); }
.dp-empty { visibility: hidden; }
.dp-today { box-shadow: inset 0 0 0 1px var(--accent); }
.dp-sel { background: var(--accent); color: var(--accent-ink); }
.dp-sel:hover { background: var(--accent); }
.dp-foot { display: flex; justify-content: space-between; margin-top: 8px; }
.dp-link { border: none; background: none; color: var(--accent); cursor: pointer; font-size: 12px; padding: 4px; }
.dp-link:hover { text-decoration: underline; }

/* jurisdiction filter buttons */
.jfilter {
  display: inline-flex; gap: 3px; flex-wrap: wrap; align-items: center;
  padding: 4px; border-radius: 14px; background: var(--chrome-bg); border: 1px solid var(--chrome-brd);
  box-shadow: none;
}
.jbtn {
  border: none; background: transparent; color: var(--text-soft);
  min-width: 32px; height: 32px; padding: 0 11px; border-radius: 9px; cursor: pointer;
  font-size: 13px; font-weight: 700;
  transition: background .14s ease, color .14s ease;
}
.jbtn:hover { background: var(--surface-2); color: var(--text); }
.jbtn.active {
  background: var(--accent); color: var(--accent-ink);
}
.jbtn-icp { color: #14b8a6; }
.jbtn-icp.active { background: #14b8a6; border-color: #14b8a6; color: #fff; }
/* Former Troopers filters by employment status, and everything on that page
   that says "this one has left" is red — the card outlines, the status pills.
   The chosen filter says the same thing, so it wears the same colour. */
#formerStatus .jbtn.active { background: var(--danger); color: #fff; }

/* trooper card grid — fixed-size cards (195 x 250), as many per row as fit */
.trooper-grid {
  display: grid;
  grid-template-columns: repeat(10, 195px);
  justify-content: start;
  gap: 16px; margin-top: 6px;
  /* always exactly 10 cards per row; Troopers.fitGrid() zoom-scales the grid down when 10 don't fit */
}
/* Notes panel open on the Troopers page → keep the cards clear of the right-edge panel (like Change).
   Former Troopers is the same grid on the same page shell, so it gets the same rule — without it the
   panel simply covered the cards there. */
body.notes-active #view-troopers, body.notes-active #view-former { padding-right: 480px; }
/* Same for Pursuits — plain reflow (no zoom/scale): the list, header and buttons just get less
   width to work with, same text/icon sizes, same as Troopers' padding-right rule above. The header
   box normally goes full-bleed to the true right edge (margin-right -154px, see #view-pursuits
   .view-head below) to hide the hover-outcome label right up to the sidebar's old shadow gutter —
   with Notes open that's no longer the true edge, so margin-right resets to 0 and the header sits
   flush inside the new 480px-narrower content box instead, same right edge as the pursuit cards.
   The visible rounded card (::before) normally keeps a 154px right inset too, matching the 150px
   one on the left for a symmetric "recessed card" look — that's purely cosmetic on the right (the
   150px on the left is the one doing real work, clearing the hover-outcome label), so it's dropped
   here: right:0 lets the card itself reach the same edge as the box, flush with the pursuit cards
   and the Add Pursuit button. padding-right drops from 172 (154 gutter + 18 internal) to just the
   18px internal part, so the button keeps the same normal gap to the card's edge. */
body.notes-active #view-pursuits { padding-right: 480px; }
body.notes-active #view-pursuits .view-head { margin-right: 0; padding-right: 18px; }
body.notes-active #view-pursuits .view-head::before { right: 0; }
.tcard {
  position: relative;
  background: var(--surface);
  border: 3px solid var(--glass-brd); border-radius: 20px;
  width: 195px; height: 270px; overflow: hidden;
  padding: 16px 14px 14px; text-align: center; cursor: pointer;
  display: flex; flex-direction: column; align-items: center;
  box-shadow: none;
  transition: border-color .14s, transform .14s, box-shadow .14s;
}
/* active troopers get a subtle blue outline; fired keep their red one (below) */
.tcard:not(.fired) { border-color: color-mix(in srgb, var(--tc-blue) 48%, var(--glass-brd)); }
.tcard:hover {
  border-color: var(--tc-blue); transform: translateY(-3px);
  box-shadow: 0 14px 44px -12px color-mix(in srgb, var(--tc-blue) 60%, transparent);
}
/* the .tcard.fired:hover red outline lives with the other "former card" rules further down */
.tcard.k9card:not(.fired):hover {
  border-color: #8957e5;
  box-shadow: 0 12px 32px -14px rgba(137,87,229,.32);
}
/* drag-hand cursor only while the Switch panel is open */
body.switch-active .tcard { cursor: grab; }
body.switch-active .tcard:active, .tcard.dragging { cursor: grabbing; }
.tcard.dragging { opacity: .5; }
.t-av {
  width: 120px; height: 120px; border-radius: 50%; object-fit: cover; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 34px; font-weight: 700; background: color-mix(in srgb, var(--tc-blue) 16%, transparent);
  color: var(--tc-blue); border: 2px solid var(--glass-brd); margin-bottom: 10px;
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--tc-blue) 16%, transparent);
  transition: box-shadow .14s;
  -webkit-user-drag: none;   /* belt-and-suspenders alongside draggable="false" so dragging the card doesn't grab the photo instead */
}
/* the photo on a card opens full size when pressed (the rest of the card opens
   the trooper's window), so the cursor says which of the two you are on */
img.t-av.clickable { cursor: pointer; }
.tcard:hover .t-av { box-shadow: 0 6px 22px -6px color-mix(in srgb, var(--tc-blue) 50%, transparent), 0 0 0 6px color-mix(in srgb, var(--tc-blue) 28%, transparent); }
.tcard.fired .t-av {
  color: var(--danger); background: rgba(217,52,43,.12);
  box-shadow: 0 0 0 5px rgba(217,52,43,.18);
}
.tcard.fired:hover .t-av { box-shadow: 0 0 0 6px rgba(217,52,43,.28); }
/* ICP trooper → turquoise card + avatar ring */
.tcard.icp:not(.fired) { border-color: color-mix(in srgb, #14b8a6 45%, var(--glass-brd)); }
.tcard.icp:hover {
  border-color: #14b8a6;
  box-shadow: 0 12px 32px -14px rgba(20,184,166,.32);
}
.tcard.icp .t-av { color: #14b8a6; background: rgba(20,184,166,.14); box-shadow: 0 6px 22px -6px rgba(20,184,166,.6), 0 0 0 5px rgba(20,184,166,.16); }
.tcard.icp:hover .t-av { box-shadow: 0 9px 28px -6px rgba(20,184,166,.85), 0 0 0 6px rgba(20,184,166,.24); }
/* Unit-only trooper (a bare number for a call sign) → orange card + avatar ring.
   Same three-part treatment ICP gets, so the kind of a card is readable from
   across the grid: blue = troop, turquoise = ICP, orange = unit, red = former. */
.tcard.unit:not(.fired) { border-color: color-mix(in srgb, #f59e0b 45%, var(--glass-brd)); }
.tcard.unit:hover {
  border-color: #f59e0b;
  box-shadow: 0 12px 32px -14px rgba(245,158,11,.32);
}
.tcard.unit .t-av { color: #f59e0b; background: rgba(245,158,11,.14); box-shadow: 0 6px 22px -6px rgba(245,158,11,.6), 0 0 0 5px rgba(245,158,11,.16); }
.tcard.unit:hover .t-av { box-shadow: 0 9px 28px -6px rgba(245,158,11,.85), 0 0 0 6px rgba(245,158,11,.24); }
.t-rank, .t-name, .t-cs { flex-shrink: 0; }
/* 🔴 EVERY LINE ON A CARD KEEPS ITS PLACE, whatever that particular card happens to hold
   (Rick, 2026-09-06). The three lines between the photo and the call-sign label used to be sized
   by their own content, so a card missing one of them pulled the other two DOWN by its height —
   a trooper with no sighting yet had their rank and name 19px lower than the card beside them,
   and reading down a column the names stepped up and down. Each line now stands at a fixed
   height whether or not anything is in it, so the auto margins always have the same leftover to
   share out and every card lays out identically.
   The numbers are the natural line heights being nailed down, not new ones — 13.5px and 17px at
   the browser's own 1.5, measured — so nothing moved on a card that already had all three.
   ⚠ `line-height` as well as `min-height` on the name: Troopers.fitCardNames shrinks the FONT of
   a long name, and with a content-driven line height that shortened the row and moved everything
   under it again (eight cards on the current roster). A fixed line height is immune to that —
   and it is why these are px and not em, which would shrink right along with the font. */
.t-rank {
  font-size: 13.5px; font-weight: 400; color: var(--text-soft); margin-bottom: 3px; margin-top: auto;
  line-height: 20.25px; min-height: 20.25px;
}
.t-name {
  font-weight: 700; font-size: 17px; white-space: nowrap; overflow: hidden; width: 100%;
  line-height: 25.5px; min-height: 25.5px;
}
/* push the call-sign label to the bottom so they all align. No fixed padding on top of that: the
   auto margins above and below the seen date are what centre it, and 11px of padding here made
   the gap under the date bigger than the one over it. */
.t-cs { margin-top: 0; }
/* (a former trooper's card carries its rank like any other; only the dog cards
   below have no rank line to space against) */
/* K9 cards have no rank line → center the dog name between the photo and the call sign */
.tcard.k9card .t-name { margin-top: auto; }
/* When the trooper was last seen, right under the name: small, thin and grey
   behind an eye, so it reads as a footnote to the name rather than a third
   value competing with rank and call sign. */
/* `margin-top: auto`, not a fixed 2px: with the same auto under it on `.t-cs`, the leftover height
   splits evenly and the date lands MIDWAY between the name and the call sign instead of tucked
   under the name (Rick, 2026-09-05). A card without a date keeps working — the two autos that are
   left still share the space. */
/* …and this row is drawn EMPTY when there is no sighting yet, which is what min-height is for:
   an empty flex box is 0 high, and the card would be back to moving its own contents about. */
.t-seen {
  display: flex; align-items: center; justify-content: center; gap: 4px;
  font-size: 11px; font-weight: 400; color: var(--text-soft);
  width: 100%; margin-top: auto; margin-bottom: auto; line-height: 14px; min-height: 14px; flex-shrink: 0;
  white-space: nowrap; overflow: hidden;
}
.t-seen svg { flex-shrink: 0; opacity: .8; }
/* Trooper Details: the note grows with its text and stops at 130px (the height
   itself is set as it is typed, see fitNotes) — past that it scrolls rather than
   pushing the History tabs under it off the window. */
#d-notes, .d-notes-view { max-height: 130px; overflow-y: auto; }
#d-notes { resize: none; }
/* …and it can be pulled TALLER when there is more to read than 130px holds (Rick, 2026-09-06).
   The grip is on the twin, because the twin is what anyone sees and types in — the textarea
   behind it is only the store. `max-height` has to give way or the drag would have nothing to
   move, so it becomes the STARTING height instead: `resize` sets an inline height, which wins.
   ⚠ `max-height` has to GO, not just grow: a capped box cannot be dragged past its cap, so the
   130 becomes a starting height that renderDetail sets inline instead (see the note there).
   Nothing is remembered on purpose — the window is rebuilt every time it opens, so the note is
   back to its own height the next time it is opened, which is what Rick asked for. */
.d-notes-view { resize: vertical; max-height: none; min-height: 62px; }
/* The K9 mark in a card's top-left corner: a paw on the handler's card, a person
   on the dog's (turquoise when that handler is ICP). It stands outside the
   card's column of text, which is the point — the line of text it replaced was
   pushing the call-sign label off the bottom of a card that also had a date. */
.t-mark {
  position: absolute; top: 9px; left: 9px; z-index: 1;
  display: inline-flex; align-items: center; justify-content: center;
  width: 25px; height: 25px;
}
/* The box is one size; what fills it is not. A paw is drawn wide and low in its
   own square, a person tall and narrow — at the same box size the person read
   as the bigger mark. This is what makes the two weigh the same on a card. */
.t-mark svg { width: 100%; height: 100%; }
.t-mark.trp svg, .t-mark.icp svg { width: 76%; height: 76%; }
.t-mark.k9 { color: #8957e5; }
.t-mark.trp { color: var(--tc-blue); }
.t-mark.icp { color: #14b8a6; }
.t-mark.former { color: var(--danger); }
html[data-theme="dark"] .t-mark.k9 { color: #b69bf0; }
.t-cs-label {
  display: inline-flex; align-items: center; padding: 4px 14px; border-radius: 999px;
  font-size: 14.5px; font-weight: 700; letter-spacing: .4px;
  background: color-mix(in srgb, var(--tc-blue) 15%, transparent); color: var(--tc-blue);
  border: 2px solid color-mix(in srgb, currentColor 55%, transparent);
}
.t-cs-label.fired { background: rgba(217,52,43,.16); color: var(--danger); }
.t-cs-label.k9 { background: rgba(137,87,229,.18); color: #8957e5; }
.t-cs-label.icp { background: rgba(20,184,166,.18); color: #14b8a6; }
.t-cs-label.unit { background: rgba(245,158,11,.18); color: #f59e0b; }

/* ---- vacant call-sign placeholder cards (A–L filters): dashed "slot" look, draggable ---- */
/* declared after .tcard:not(.fired)/.tcard:hover so they win on equal specificity; opacity kept
   high (not a low-contrast grey-out) so they read clearly as an available slot, not a disabled card */
.tcard.tcard-ph {
  border-style: dashed; border-width: 2px;
  border-color: color-mix(in srgb, var(--text-soft) 45%, transparent);
  background: color-mix(in srgb, var(--text-soft) 6%, var(--surface));
  box-shadow: none;
  opacity: .68;
  cursor: grab;
}
.tcard.tcard-ph:hover {
  opacity: .92;
  border-color: var(--text-soft);
  transform: translateY(-3px);
  box-shadow: none;
}
.tcard.tcard-ph:active { cursor: grabbing; }
.t-av.ph {
  color: var(--text-soft);
  background: color-mix(in srgb, var(--text-soft) 14%, transparent);
  border: 2px dashed color-mix(in srgb, var(--text-soft) 60%, transparent);
  box-shadow: none;
}
.tcard.tcard-ph:hover .t-av.ph { box-shadow: none; }
.t-cs-label.ph {
  background: color-mix(in srgb, var(--text-soft) 16%, transparent);
  color: var(--text);
  border-style: dashed;
}
/* A placeholder card carries the photo and the call sign and nothing in between, so without this
   the label sat straight under the photo — half a card higher than the label on every real card
   beside it. The auto margin is what the rank line does on a real card: it pushes everything under
   it to the floor, which is where a call-sign label belongs on both kinds. (Rick, 2026-09-06) */
.tcard.tcard-ph .t-cs { margin-top: auto; }

/* filter-bar toggle: show / hide the vacant-call-sign placeholders — icon-only */
.filters .spacer { flex: 1; }
.ph-toggle {
  position: relative; flex-shrink: 0; width: 38px; height: 38px; border-radius: 999px; cursor: pointer; font-size: 16px;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-soft);
  transition: background .18s ease, color .18s ease, border-color .18s ease, opacity .14s ease;
}
.ph-toggle:hover { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, var(--border)); }
.ph-toggle.off { opacity: .5; }
/* two stacked icons — the "off" (crossed) eye shows on hover, or always once placeholders are hidden */
.ph-toggle .ph-eye-on, .ph-toggle .ph-eye-off { display: inline-flex; }
.ph-toggle .ph-eye-off { display: none; position: absolute; }
.ph-toggle:hover .ph-eye-on, .ph-toggle.off .ph-eye-on { display: none; }
.ph-toggle:hover .ph-eye-off, .ph-toggle.off .ph-eye-off { display: inline-flex; position: static; }

/* Settings → Troops: one 0–100 slider per troop, listed vertically. Twelve of
   them in a row, so the spacing is tighter than a normal field's — the label
   sits ON the same line as the slider rather than above it. */
/* `.field` stacks its label above the control (flex column); this one puts them
   side by side instead, which is what makes twelve of them fit. */
.troopmax-field { margin: 0; display: flex; flex-direction: row; align-items: center; gap: 12px; padding: 2px 0; }
.troopmax-field > label { min-width: 58px; margin: 0; flex-shrink: 0; }
.troopmax-field .inline-row { flex: 1; margin: 0; }
.set-panel[data-panel="placeholders"] .hint { margin-bottom: 10px; display: block; }

/* "Add Card" chooser (Trooper / K9 Dog) */
.add-chooser { display: flex; gap: 14px; padding: 6px 0; }
.add-choice {
  flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
  padding: 28px 14px 24px; border-radius: 20px; cursor: pointer;
  background: linear-gradient(180deg, color-mix(in srgb, var(--surface-2), #fff 4%), var(--surface-2));
  border: 1px solid var(--border); color: var(--text);
  transition: border-color .14s, transform .14s, box-shadow .14s;
}
.add-choice:hover {
  border-color: var(--accent); transform: translateY(-3px);
  box-shadow: 0 14px 38px -14px color-mix(in srgb, var(--accent) 45%, transparent);
}
.add-choice .ac-ico {
  width: 58px; height: 58px; border-radius: 50%; margin-bottom: 8px;
  display: inline-flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 15%, transparent); color: var(--accent);
}
.add-choice .ac-name { font-weight: 800; font-size: 16px; letter-spacing: -.2px; }
.add-choice .ac-sub { font-size: 12px; color: var(--text-soft); }
/* K9 tile — purple identity */
.add-choice.ac-k9:hover {
  border-color: #8957e5;
  box-shadow: 0 14px 38px -14px rgba(137,87,229,.45);
}
.add-choice.ac-k9 .ac-ico { background: rgba(137,87,229,.16); color: #8957e5; }
html[data-theme="dark"] .t-cs-label.k9 { color: #b69bf0; }
/* K9 filter button + dog cards */
.jbtn-k9 { display: inline-flex; align-items: center; gap: 5px; color: #8957e5; }
.jbtn-k9.active { background: #8957e5; border-color: #8957e5; color: #fff; }
.tcard.k9card:not(.fired) { border-color: color-mix(in srgb, #8957e5 48%, var(--glass-brd)); }
.tcard.k9card:hover { border-color: #8957e5; box-shadow: 0 16px 34px -12px rgba(137,87,229,.5), inset 0 1px 0 var(--glass-hi); }
.t-av.k9 { color: #8957e5; background: rgba(137,87,229,.16); box-shadow: 0 6px 22px -6px rgba(137,87,229,.6), 0 0 0 5px rgba(137,87,229,.16); }
.tcard.k9card:hover .t-av.k9 { box-shadow: 0 9px 28px -6px rgba(137,87,229,.75), 0 0 0 6px rgba(137,87,229,.22); }
/* FIRED K9 dog → red card + red paw/ring (paw kept, just recoloured) */
.tcard.k9card.fired .t-av { color: var(--danger); background: rgba(217,52,43,.12); box-shadow: 0 6px 22px -6px rgba(217,52,43,.65), 0 0 0 5px rgba(217,52,43,.16); }
.tcard.k9card.fired:hover .t-av { box-shadow: 0 9px 28px -6px rgba(217,52,43,.8), 0 0 0 6px rgba(217,52,43,.26); }
.trooper-grid .empty { grid-column: 1 / -1; }
.trooper-form { display: flex; flex-direction: column; gap: 12px; }
.trooper-form .field { gap: 4px; }

/* badge input with leading # */
.input-prefix {
  display: flex; align-items: stretch; border: 1px solid var(--border);
  border-radius: 8px; background: var(--surface); overflow: hidden;
}
.input-prefix .prefix {
  display: flex; align-items: center; padding: 0 9px;
  background: var(--surface-2); color: var(--text-soft); font-weight: 700;
}
/* the field inside the prefix box carries no box of its own — and no height
   either, or it would sit off-centre in the one around it */
.input-prefix input {
  border: none !important; border-radius: 0 !important; background: transparent; flex: 1;
  height: 100% !important; min-height: 0 !important; padding-top: 0; padding-bottom: 0;
}
.input-prefix input:focus { outline: none; }

/* styled file picker button */
.file-btn {
  display: inline-flex; align-items: center; gap: 7px; width: max-content;
  padding: 7px 13px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-2); color: var(--text); cursor: pointer;
  font-size: 13px; font-weight: 500;
}
.file-btn:hover { background: var(--border); }

/* photo: dropzone + thumbnail with corner remove button */
.dropzone {
  display: flex; align-items: center; gap: 5px; flex-wrap: wrap;
  padding: 14px; border: 1.5px dashed var(--border); border-radius: 10px;
}
.dropzone.drag { border-color: var(--accent); background: var(--accent-soft); }
.photo-box { position: relative; width: max-content; }
.photo-thumb {
  width: 82px; height: 82px; object-fit: cover; border-radius: 10px;
  border: 1px solid var(--border); display: block;
}
.photo-rm {
  position: absolute; top: -8px; right: -8px; width: 24px; height: 24px;
  border-radius: 50%; border: 1px solid var(--border); background: var(--surface);
  color: var(--danger); cursor: pointer; font-size: 12px; line-height: 1;
  display: flex; align-items: center; justify-content: center; box-shadow: none;
}
.photo-rm:hover { background: var(--danger); color: #fff; border-color: var(--danger); }

/* multi-picture grid — cap at 5 thumbs (82px) per row (fits the 500px Change/Notes panels too) */
.photo-grid { display: flex; flex-wrap: wrap; gap: 10px; max-width: calc(5 * 82px + 4 * 10px); }
.photo-grid .photo-thumb { cursor: zoom-in; }

/* Add/Edit Pursuit: always FIVE across, on any screen. Five fixed 82px boxes
   need 450px and a phone has about 330 to give, so they wrapped onto a second
   and third row there. Equal fractions instead — the thumbs shrink to whatever
   the form is wide and the row count stays put, upright and sideways alike.
   Scoped to .pursuit-form, NOT to #f-photo-zone alone: Add/Edit Trooper uses the
   very same id for its own single-picture slot, and a fifth of the form is not
   what that one wants to be. */
/* width: 100% on both, or the row measures itself against what is IN it: with
   one empty drop zone the five columns came out 91px and with a picture beside
   it 107, so the slot changed size depending on whether the pursuit had photos.
   Full width means five fifths of the form, always. */
.pursuit-form #f-photo-zone { width: 100%; }
.pursuit-form #f-photo-zone .photo-grid {
  display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 8px; max-width: none; width: 100%;
}
.pursuit-form #f-photo-zone .photo-box { width: auto; }
.pursuit-form #f-photo-zone .photo-thumb,
.pursuit-form #f-photo-zone .dropzone.photo-add { width: 100%; height: auto; aspect-ratio: 1; }
/* The drop zone is one of the five here, so "Drop or" has to fit a fifth of a
   phone — it used to be dropped entirely, which left this the one picture slot
   in the app that read differently from all the others. It shrinks and wraps
   instead of disappearing. */
.pursuit-form #f-photo-zone .dropzone.photo-add .muted { font-size: 9.5px; line-height: 1.15; }
.pursuit-form #f-photo-zone .dropzone.photo-add .file-btn { font-size: 11px; padding: 4px 6px; }
.dropzone.photo-add { width: 82px; height: 82px; flex-direction: column; gap: 4px; padding: 6px; text-align: center; justify-content: center; }
/* the 96px box is tight for longer labels (e.g. German "Ablegen oder Auswählen") → shrink to fit, no overflow/wrap */
.dropzone.photo-add .muted { font-size: 11px; line-height: 1.2; }
.dropzone.photo-add .file-btn { font-size: 11.5px; padding: 5px 9px; max-width: 100%; }
.d-photos { display: flex; flex-wrap: wrap; gap: 8px; margin: 4px 0 14px; }
/* former-trooper "Details" tab content */
.detail-emp { margin-top: 12px; }
.detail-emp.hidden { display: none; }
.detail-emp .d-photos { margin: 10px 0 0; }
/* A former card’s pictures are the record of why somebody left, and they are
   looked AT rather than counted — bigger than the 82px a picture strip gets
   elsewhere. Five still fit one row in the 750px Details window. */
.detail-emp .photo-thumb { width: 104px; height: 104px; }
.detail-emp .dropzone.photo-add { width: 104px; height: 104px; }
.detail-emp .photo-grid { max-width: calc(5 * 104px + 4 * 10px); }
.d-photos .photo-thumb { width: 64px; height: 64px; cursor: zoom-in; }

/* map popup: the View-Media button (the Watch chips moved into the Watch window) */
.mp-chips { display: flex; flex-wrap: wrap; gap: 5px; }
/* same soft-outline look as the Save/Change buttons (.btn.success), kept in the app's accent colour */
.mp-details {
  margin-top: 12px; width: 100%; display: flex; align-items: center; justify-content: center;
  border: 1px solid color-mix(in srgb, var(--accent) 55%, transparent); background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent); padding: 10px; border-radius: 12px; cursor: pointer; font-size: 13px; font-weight: 700;
  transition: background .16s ease, border-color .16s ease, color .16s ease, box-shadow .16s ease;
}
.mp-details:hover {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 6px 20px -4px color-mix(in srgb, var(--accent) 55%, transparent);
}
.mapboxgl-popup a:focus, .mapboxgl-popup button:focus { outline: none; }

/* location picker (map overlay) */
.picker-overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,.55); z-index: 250;
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.picker-panel {
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  width: min(1360px, 96vw); max-width: 100%; box-shadow: none; display: flex; flex-direction: column; overflow: hidden;
  /* The window keeps exactly the size it had when the map carried it: the map
     used to be min(82vh, 840px), with a head and a foot of 63px each around it
     (12px padding either side of a 38px control, plus their divider). That total
     is now the PANEL's height instead, so slimming the two bars hands their
     saved height to the map and changes nothing at all on the outside. */
  height: calc(min(82vh, 840px) + 126px);
  max-height: calc(100vh - 48px);
}
/* Head and foot are as short as their contents allow — every pixel they give up
   goes to the map, and the panel itself does not change size. The foot is a
   single line: the coordinates on the left, Save on the right, level with them,
   rather than a hint block with the button below it. */
.picker-head { display: flex; align-items: center; gap: 10px; padding: 7px 12px; border-bottom: 1px solid var(--border); }
.picker-head strong { font-size: 15px; }
.picker-head .icon-btn.sm { width: 30px; height: 30px; }
.picker-head .seg { margin-left: auto; }
.picker-map-wrap { position: relative; flex: 1; min-height: 0; }

/* Full screen is a class on the panel, not the Fullscreen API — iOS only ever
   grants that to a <video>. Same arrangement the Pursuit Map's own button uses. */
.picker-overlay.picker-full { padding: 0; }
.picker-overlay.picker-full .picker-panel {
  width: 100%; max-width: 100%;
  height: 100dvh; max-height: 100dvh; border-radius: 0; border: 0;
}
/* full screen means the MAP — the head and the foot step out of the way and the
   way back is the button at the top of the annotation tools (#pk-full-map).
   Both come straight back on the way out, with the pin and Save as they were. */
.picker-overlay.picker-full .picker-head,
.picker-overlay.picker-full .picker-foot { display: none; }
/* the exit sits above the tools with a hair of daylight, so it reads as its own
   control rather than a fifth drawing tool */
.pk-tool-full { margin-bottom: 6px; }
/* Pick-Location annotation tools (top-left of the picker map) */
.pk-tools { position: absolute; top: 14px; right: 14px; z-index: 6; display: flex; flex-direction: column; gap: 8px; }
/* Same button as the Pursuit Map's own controls — round, the map's frosted glass,
   the same border and blur — because they sit on the same imagery and doing two
   different things there was the only reason they looked unrelated. What they say
   is carried by their colour instead: the four drawing tools are red, like the
   marks they make; the way out of full screen is white, because it is not a tool. */
.pk-tool {
  width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--map-glass); border: 1px solid var(--map-glass-brd); color: #e01b1b;
  -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px);
  box-shadow: inset 0 1px 0 var(--glass-hi);
  transition: border-color .15s, background .15s, color .15s;
}
.pk-tool:hover { border-color: #e01b1b; }
.pk-tool-full { color: #fff; }
.pk-tool-full:hover { border-color: #fff; }
.pk-tool.active { background: #e01b1b; border-color: #e01b1b; color: #fff; }
/* annotation map markers (suspect / vehicle) — just the object's shape, no disc, fixed size like the main pin */
.anno-marker {
  width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center; color: #e01b1b;
  cursor: grab;
  /* no position:relative — Mapbox needs its own position:absolute so the marker stays anchored on zoom.
     This filter is deliberately invisible: any non-none filter still makes this the containing block
     for the absolute .anno-rm button (it used to be a drop-shadow, dropped with the app's shadows).
     Don't swap it for a transform — Mapbox writes its own transform on this element. */
  filter: drop-shadow(0 0 0 transparent);
}
.anno-marker svg { display: block; }
.anno-marker .anno-rm {
  position: absolute; top: -7px; right: -7px; width: 17px; height: 17px; border-radius: 50%;
  border: 1.5px solid #fff; background: #222; color: #fff; font-size: 10px; line-height: 1; cursor: pointer;
  display: none; align-items: center; justify-content: center;
}
.anno-marker:hover .anno-rm { display: flex; }
/* zone resize handles */
.zone-handle { border-radius: 50%; box-shadow: none; }
.zone-handle.zone-center {
  width: 16px; height: 16px; background: #e01b1b; border: 2px solid #fff; cursor: grab;
  box-shadow: 0 0 0 4px rgba(224,27,27,.35), 0 0 13px 3px rgba(224,27,27,.85);
}
.zone-handle.zone-edge { width: 16px; height: 16px; background: #e01b1b; border: 2px solid #fff; cursor: ew-resize !important; }

/* pin pop-ups: auto-width, chips wrap within a sensible max */
.mapboxgl-popup-content { width: auto !important; max-width: 340px; }
.map-popup { white-space: normal; }
#mapContainer.popups-hidden .mapboxgl-popup { display: none !important; }
/* the map takes whatever the two short bars leave; the panel's own height is
   what fixes the window size, so shrinking them grows the map and nothing else */
.picker-map { height: 100%; min-height: 380px; width: 100%; }
.picker-overlay.picker-full .picker-map { min-height: 0; }
.picker-map .mapboxgl-canvas-container { cursor: default !important; }
.picker-map .mapboxgl-marker { cursor: grab !important; }
.picker-map .mapboxgl-marker:active { cursor: grabbing !important; }
/* one line: the coordinates on the left, Save level with them on the right */
.picker-foot { display: flex; align-items: center; gap: 12px; padding: 7px 12px; border-top: 1px solid var(--border); }
.picker-foot #pk-coords { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* fired trooper card */
.tcard.fired { border-color: var(--danger); }
.tcard.fired:hover {
  border-color: var(--danger);
  box-shadow: 0 10px 26px -12px rgba(217,52,43,.5), inset 0 1px 0 var(--glass-hi);
}
html[data-theme="dark"] .tcard.fired { background: rgba(217,52,43,.06); }

/* in-app confirmation dialog (solid, stacks above modals) */
.confirm-overlay {
  position: fixed; inset: 0; z-index: 400; background: rgba(8,10,16,.62);
  display: flex; align-items: center; justify-content: center; padding: 24px;
}
.confirm-box {
  background: var(--surface); border: 1px solid var(--border); border-radius: 24px;
  box-shadow: none;
  width: 380px; max-width: 100%; padding: 26px 24px 22px;
  text-align: center;
}
.confirm-box.danger { border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); }
/* A size smaller, for a question with two words in it — the Trooper Sighting panel's empty-field
   question wears the Add Pursuit window's shape but has a fraction of its content (Rick). */
.confirm-compact { width: 320px; padding: 20px 20px 18px; border-radius: 20px; }
.confirm-compact .confirm-icon { width: 50px; height: 50px; margin: 0 auto 12px; }
.confirm-compact .confirm-title { font-size: 17px; }
.confirm-compact .confirm-msg { font-size: 13.5px; margin-bottom: 16px; }
.confirm-compact .confirm-actions .btn { padding: 11px 14px; }
.confirm-icon {
  width: 62px; height: 62px; margin: 2px auto 16px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent);
}
.confirm-box.danger .confirm-icon { background: color-mix(in srgb, var(--danger) 16%, transparent); color: var(--danger); }
.confirm-title { font-size: 19px; font-weight: 800; color: var(--text); margin-bottom: 6px; letter-spacing: -.2px; }
.confirm-msg { font-size: 14.5px; line-height: 1.5; color: var(--text-soft); margin-bottom: 20px; white-space: pre-line; }
.confirm-actions { display: flex; gap: 10px; }
.confirm-actions .btn { flex: 1; justify-content: center; padding: 13px 16px; }
/* press-and-hold confirm button (e.g. hold 10s to delete all) — a fill bar sweeps across while held */
.hold-btn { position: relative; overflow: hidden; touch-action: none; flex-direction: column; gap: 3px; }
.hold-btn .hold-fill { position: absolute; left: 0; top: 0; bottom: 0; width: 0; background: rgba(255,255,255,.34); pointer-events: none; transition: width .18s ease; }
.hold-btn.holding .hold-fill { width: 100%; transition: width var(--hold, 10s) linear; }
.hold-btn .hold-label { position: relative; z-index: 1; white-space: nowrap; }
.hold-btn .hold-count { position: relative; z-index: 1; padding: 0 8px; border-radius: 999px; background: rgba(255,255,255,.24); font-size: 11px; font-weight: 800; font-variant-numeric: tabular-nums; }
/* extra glow — only for the button that opts in (opts.glow), e.g. Reset "Delete all" */
.hold-glow { box-shadow: 0 0 22px 1px color-mix(in srgb, var(--danger) 62%, transparent); }
.hold-glow.holding { box-shadow: 0 0 32px 4px color-mix(in srgb, var(--danger) 82%, transparent); }
/* subtle springy entrance for dialogs (own idea — no glow) */
@keyframes popIn { from { opacity: 0; transform: translateY(7px) scale(.97); } to { opacity: 1; transform: none; } }
.confirm-box { animation: popIn .17s cubic-bezier(.34,1.4,.64,1); }

/* image lightbox */
.lightbox {
  position: fixed; inset: 0; background: rgba(0,0,0,.82); z-index: 300;
  display: flex; align-items: center; justify-content: center; cursor: zoom-out;
}
.lightbox img { max-width: 94vw; max-height: 94vh; border-radius: 12px; box-shadow: none; }
/* gallery viewer: prev/next arrows + a position counter */
.lightbox-gallery { cursor: default; }
.lightbox-gallery img { cursor: default; }
.lb-nav {
  position: fixed; top: 50%; transform: translateY(-50%); z-index: 301;
  width: 52px; height: 52px; border-radius: 50%; cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  background: rgba(20,20,24,.6); border: 1px solid rgba(255,255,255,.18); color: #fff;
  transition: background .14s, transform .14s;
}
.lb-nav:hover { background: rgba(40,40,48,.9); }
.lb-prev { left: 22px; }
.lb-next { right: 22px; }
.lb-count {
  position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%); z-index: 301;
  color: #fff; font-size: 14px; font-weight: 600; letter-spacing: .3px;
  background: rgba(20,20,24,.6); border: 1px solid rgba(255,255,255,.16);
  padding: 5px 13px; border-radius: 999px;
}
/* an <img> is natively draggable; the detail photo is a click target, so the
   browser's own image drag only gets in the way (same reason as .t-av) */
.detail-photo { -webkit-user-drag: none; user-select: none; }
.detail-photo.clickable { cursor: pointer; transition: box-shadow .14s; }
.detail-photo.clickable:hover { box-shadow: 0 9px 28px -6px rgba(31,111,235,.8), 0 0 0 6px color-mix(in srgb, var(--accent) 26%, transparent); }
/* fired trooper → red ring; K9 dog → purple ring */
.detail-photo.fired { box-shadow: 0 6px 22px -6px rgba(217,52,43,.6), 0 0 0 5px rgba(217,52,43,.16); }
.detail-photo.fired.clickable:hover { box-shadow: 0 9px 28px -6px rgba(217,52,43,.8), 0 0 0 6px rgba(217,52,43,.26); }
.detail-photo.k9 { box-shadow: 0 6px 22px -6px rgba(137,87,229,.6), 0 0 0 5px rgba(137,87,229,.16); }
.detail-photo.k9.placeholder { background: rgba(137,87,229,.16); color: #8957e5; }   /* match the card: purple paw on soft purple */
.detail-photo.k9.clickable:hover { box-shadow: 0 9px 28px -6px rgba(137,87,229,.8), 0 0 0 6px rgba(137,87,229,.26); }
/* FIRED K9 dog detail → red ring + red paw (overrides the purple k9 rules) */
.detail-photo.k9.fired { box-shadow: 0 6px 22px -6px rgba(217,52,43,.6), 0 0 0 5px rgba(217,52,43,.16); }
.detail-photo.k9.fired.placeholder { background: rgba(217,52,43,.12); color: var(--danger); }
.detail-photo.k9.fired.clickable:hover { box-shadow: 0 9px 28px -6px rgba(217,52,43,.8), 0 0 0 6px rgba(217,52,43,.26); }

/* trooper detail header */
.detail-head {
  display: flex; align-items: center; gap: 20px; margin-bottom: 18px;
  padding: 20px; border-radius: 16px;
  background: var(--surface-2); border: 1px solid color-mix(in srgb, var(--accent) 50%, var(--border));
}
/* framed card wrapping the head + notes */
.detail-card {
  background: var(--surface-2); border-radius: 16px; padding: 18px 20px;
  border: 1px solid color-mix(in srgb, var(--accent) 50%, var(--border));
}
.detail-card.fired { border-color: color-mix(in srgb, var(--danger) 60%, var(--border)); }
.detail-card.icp { border-color: color-mix(in srgb, #14b8a6 62%, var(--border)); }
.detail-card.k9 { border-color: color-mix(in srgb, #8957e5 62%, var(--border)); }
.detail-card .detail-head { border: none; background: none; padding: 0; margin-bottom: 0; }
.detail-hist-label { display: block; font-size: 13px; color: var(--text-soft); font-weight: 600; letter-spacing: .3px; margin: 18px 0 6px; }
.detail-lastseen.hidden, .detail-emp.hidden { display: none; }
.hist-seg { flex-wrap: wrap; }   /* several tabs may not fit one row */
/* Last-seen "add a date" row: same date field as the Add-Pursuit form + room from the tabs above */
/* Trooper Sighting and Changes must start at the same height, or switching tabs nudges the whole list
   up and down (Rick, 2026-09-06). Changes is a bare .history-list, whose own `margin: 10px 0 0`
   sets the gap; Trooper Sighting is that list inside a wrapper, so the wrapper takes the 10 and the
   list inside it takes none. Both then stand exactly 10px under the tab bar. */
.detail-lastseen { margin-top: 10px; }
.detail-lastseen > .history-list { margin-top: 0; }
.ls-add-row { display: flex; align-items: flex-end; gap: 8px; margin: 0 0 12px; }
/* Last-seen rows carry one line and a label pill, so they do not need the change
   list's air: 4px a side against 8 takes 8px off each of them. The pill's own
   min-height is what is left, and it is not padding — leave it alone.
   ⚠ `li.ls-item`, not `.ls-item`: `.history-list li` sets the padding as a
   SHORTHAND, and a plain class loses to it however far down the file it stands
   (`.ls-item { align-items: center }` at the bottom of this file is that exact
   rule, and it has never applied). */
.history-list li.ls-item { padding-top: 4px; padding-bottom: 4px; }
.detail-head.fired { border-color: color-mix(in srgb, var(--danger) 60%, var(--border)); }
.detail-head.k9 { border-color: color-mix(in srgb, #8957e5 62%, var(--border)); }
.detail-head.k9.fired { border-color: color-mix(in srgb, var(--danger) 60%, var(--border)); }
.detail-head.icp { border-color: color-mix(in srgb, #14b8a6 62%, var(--border)); }
.detail-photo.icp { box-shadow: 0 6px 22px -6px rgba(20,184,166,.6), 0 0 0 5px rgba(20,184,166,.16); }
.detail-photo.icp.placeholder { background: rgba(20,184,166,.14); color: #14b8a6; }
.detail-photo.icp.clickable:hover { box-shadow: 0 9px 28px -6px rgba(20,184,166,.85), 0 0 0 6px rgba(20,184,166,.26); }
/* the unit-only trooper's window wears the same orange their card does */
.detail-card.unit { border-color: color-mix(in srgb, #f59e0b 62%, var(--border)); }
.detail-head.unit { border-color: color-mix(in srgb, #f59e0b 62%, var(--border)); }
.detail-photo.unit { box-shadow: 0 6px 22px -6px rgba(245,158,11,.6), 0 0 0 5px rgba(245,158,11,.16); }
.detail-photo.unit.placeholder { background: rgba(245,158,11,.14); color: #f59e0b; }
.detail-photo.unit.clickable:hover { box-shadow: 0 9px 28px -6px rgba(245,158,11,.85), 0 0 0 6px rgba(245,158,11,.26); }
.detail-photo {
  width: 112px; height: 112px; border-radius: 50%; object-fit: cover;
  border: 2px solid var(--glass-brd); flex-shrink: 0;
  box-shadow: 0 6px 22px -6px rgba(31,111,235,.55), 0 0 0 5px color-mix(in srgb, var(--accent) 14%, transparent);
}
.detail-photo.placeholder {
  display: flex; align-items: center; justify-content: center;
  background: var(--accent-soft); color: var(--accent); font-weight: 700; font-size: 36px;
  cursor: default; user-select: none;   /* initials aren't text to select → normal cursor */
}
.detail-photo.fired.placeholder { background: rgba(217,52,43,.12); color: var(--danger); }   /* fired → red initials, like the card */
.detail-info { flex: 1; min-width: 0; }
/* name: prominent, slightly highlighted with a soft accent underline */
.detail-info strong {
  font-size: 23px; font-weight: 800; letter-spacing: .2px; line-height: 1.15;
  display: inline-block; max-width: calc(100% - 34px); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  margin-bottom: 11px; padding-bottom: 5px;
  border-bottom: 2px solid color-mix(in srgb, var(--accent) 55%, transparent);
}
/* the copy button beside the name — a sibling of the <strong>, not inside it, so it keeps its own
   size while a long name is scaled down (Troopers.fitOne measures the <strong> alone). The name's
   max-width leaves exactly this button's room.
   The row is a flex box so the two line up by their middles: as an inline sibling the button hung
   off the text's BASELINE and sat visibly below the name's underline. Both carry the same bottom
   margin, so "middle" is measured over the same box the name has always had. */
.detail-nameline { display: flex; align-items: center; }
.detail-nameline > strong { min-width: 0; }
.name-copy {
  margin-left: 8px; margin-bottom: 11px;
  width: 26px; height: 26px; flex-shrink: 0;
  /* the glyph sits centred in the circle: the copy icon's own artwork leans up and left inside its
     box (the two sheets are drawn off-centre), so it is nudged back by hand */
  padding: 0; line-height: 0;
}
.name-copy .icon { position: relative; top: 1px; left: 1px; }
.name-copy.ok { color: var(--ok, #2ea043); border-color: currentColor; }
.detail-card.fired .detail-info strong { border-bottom-color: color-mix(in srgb, var(--danger) 60%, transparent); }
.detail-card.k9 .detail-info strong { border-bottom-color: color-mix(in srgb, #8957e5 62%, transparent); }
.detail-card.icp .detail-info strong { border-bottom-color: color-mix(in srgb, #14b8a6 62%, transparent); }
/* info as clean chips arranged in rows */
.detail-meta { display: flex; flex-direction: column; gap: 8px; }
.dm-row { display: flex; flex-wrap: wrap; gap: 8px; }
.dm-chip {
  display: inline-flex; align-items: baseline; gap: 7px;
  padding: 5px 12px; border-radius: 9px; font-size: 14px; line-height: 1.2;
  background: var(--surface); border: 1px solid var(--border);
}
.dm-chip .dm-k { color: var(--text-soft); font-size: 10.5px; text-transform: uppercase; letter-spacing: .5px; font-weight: 700; }
.dm-chip b { color: var(--text); font-weight: 700; }
/* clickable value inside a chip (K9 partner → opens the dog) — plain text colour, underline on hover */
.dm-link { color: var(--text); cursor: pointer; text-decoration: none; border-bottom: 1px solid transparent; transition: border-color .12s, filter .12s; }
.dm-link:hover { border-bottom-color: currentColor; filter: brightness(1.15); }
.detail-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; justify-content: flex-end; align-self: flex-start; }

.token {
  display: inline-flex; align-items: center; gap: 7px; background: var(--accent-soft);
  color: var(--accent); padding: 5px 11px; border-radius: 7px; font-size: 13.5px; font-weight: 600;
}
.token button { border: none; background: none; color: inherit; cursor: pointer; font-size: 14px; }
/* Involved Troopers is a LIST, not a box: the rows are the field, and the "+"
   under them is what adds to it (see .trp-add). No frame around the lot — an
   empty pursuit would otherwise open on an empty grey rectangle. */
.trp-list { display: flex; flex-direction: column; align-items: stretch; gap: 5px; }
.trp-list:not(:empty) { margin-bottom: 5px; }
/* the add row: the same full-width "+" a Last-seen row uses for its kind. 32px is
   the height every chip and every "+" in this form shares. */
/* button.trp-add, not .trp-add: .ls-label sets min-height further down the file */
button.trp-add { margin: 0; min-height: 32px; }
/* ONE height for the Add/Edit Pursuit form: every field, every chip and every "+"
   in it is 32px, so the eye reads a column of equal rows. Notes is the exception —
   it is a text area and has to be able to be more than one line. */
.pursuit-form .sm-input, .pursuit-form .combo-input, .pursuit-form .dp-text,
.pursuit-form .input-prefix, .pursuit-form input:not([type=file]) {
  height: 32px; min-height: 32px;
}
/* …the note is the exception, and so is the twin standing in for it: both wear
   .sm-input, which the rule above would otherwise cut down to one line */
.pursuit-form textarea, .pursuit-form #f-notes,
.pursuit-form .rich-note { height: auto; min-height: 60px; }
.pursuit-form .combo, .pursuit-form .dp { min-height: 32px; }
/* Event Tags: the chips stand a little clear of the field under them */
#f-flag-row:not(:empty) { margin-bottom: 8px; }
/* Troop: the chips and the "+" share ONE row — the "+" is the end of the row,
   not a bar of its own, so it takes only the width of a chip. Every chip is the
   same size (they are all "Troop x") with the label CENTRED: the ✕ sits on the
   right edge, and the same room is left empty on the left, so the text lands in
   the middle of the chip rather than in the middle of what the ✕ left over. */
.troop-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
/* .troop-row .troop-chip, not .troop-chip: .trp-token sets justify-content
   further down the file and would otherwise pull the label back to the left */
.troop-row .troop-chip {
  position: relative; width: 104px; height: 32px; padding: 0 22px;
  justify-content: center; text-align: center;
}
.troop-chip .trp-token-lbl { flex: 0 1 auto; text-align: center; font-size: 12.5px; }
.troop-chip button { position: absolute; right: 7px; top: 50%; transform: translateY(-50%); }
/* Event Tags wear the same chip, but their text is not one fixed shape — those
   size themselves to what they say, on one line, with air either side */
.troop-row .chip-auto { width: auto; min-width: 0; white-space: nowrap; }
/* no ✕ on it (Event Tags) → the same padding on both sides */
.chip-plain { padding: 0 14px; }
/* .ls-label is full-width by nature — in this row it is one item among the chips */
.troop-row .troop-add { flex: 0 0 auto; width: auto; min-width: 46px; height: 32px; min-height: 32px; padding: 5px 12px; }
.troop-add.hidden, #f-troop-combo.hidden, #f-veh-combo.hidden { display: none; }
.trp-add.hidden, #f-tcombo.hidden { display: none; }
/* a selected trooper's row is the same height as the "+" under it */
.trp-token {
  justify-content: flex-start; gap: 9px; padding: 2px 9px; min-height: 32px; border-radius: 9px;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text); font-weight: 500;
}
.trp-token:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.trp-token-ico {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 25px; height: 25px; border-radius: 50%; background: var(--accent-soft); color: var(--accent);
  border: 2px solid currentColor;   /* ring matches the icon colour (accent, or purple for dogs) */
  font-weight: 800; font-size: 12px;   /* also styles the Troop-letter badge */
}
.trp-token.dog .trp-token-ico { background: rgba(137,87,229,.16); color: #8957e5; }
.trp-token-lbl { flex: 1; font-size: 13.5px; font-weight: 600; color: var(--text); }
/* a trooper who has left: the name still reads as it did on the pursuit's day, the red says gone */
.trp-token.former .trp-token-lbl { color: var(--danger); }
.trp-token button { border: none; background: none; cursor: pointer; opacity: 0; transition: opacity .12s; color: var(--text-soft); flex-shrink: 0; font-size: 14px; }
.trp-token:hover button { opacity: .65; }
.trp-token button:hover { opacity: 1; color: var(--danger); }
/* a touch screen has no hover — there the ✕ is simply there, or a chip could not
   be taken off at all */
@media (hover: none) { .trp-token button { opacity: .65; } }

/* ===== ‹ › beside a window, for stepping through a list (App.applyNav) =====
   They hang off the window itself, so they follow it wherever it sits, and they
   are vertically centred on it. Off the outside edge on a wide screen; upright
   there is no outside left, so they move in over the window's own corners. */
.modal-nav {
  position: absolute; top: 50%; transform: translateY(-50%);
  width: 44px; height: 44px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border: 0; background: none; color: var(--text-soft);
  cursor: pointer; z-index: 2; opacity: .7;
  transition: color .14s, opacity .14s;
}
.modal-nav:hover { color: var(--text); opacity: 1; }
.modal-nav.is-off { opacity: .18; cursor: default; }
.modal-nav.is-off:hover { color: var(--text-soft); opacity: .18; }
.modal-nav-prev { left: -58px; }
.modal-nav-next { right: -58px; }
/* ===== Modal ===== */
.modal-overlay {
  position: fixed; inset: 0; background: rgba(8,10,16,.58); z-index: 100;
  display: flex; align-items: flex-start; justify-content: center; padding: 40px 16px; overflow-y: auto;
  /* Always keep the scrollbar's width reserved. The window is centred in here,
     so the moment this box became scrollable the bar appeared and shoved the
     whole window a few pixels left — most visibly when opening a calendar, which
     makes the form taller. Reserved either way, nothing moves. */
  scrollbar-gutter: stable;
}
.modal {
  position: relative;   /* the ‹ › beside it hang off this box */
  background: var(--surface); border: 1px solid var(--border); border-radius: 22px;
  width: 600px; max-width: calc(100vw - 32px);
  box-shadow: none;
}
/* the Watch window (App.openModal opts.wide): its content is a video, and 600px
   made a postage stamp of it. The phone is untouched — there the window already
   uses whatever width the screen has. */
/* the class sits on the OVERLAY (#modal), the width belongs to the card inside it */
.modal-wide .modal { width: 880px; }
.modal-wide .wt-list { max-height: 320px; }
.modal-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 16px 20px; border-bottom: 1px solid var(--border);
}
.modal-header h2 { margin: 0; font-size: 17px; display: flex; align-items: center; gap: 8px; flex: 1; }
/* tracks the title's own colour — it used to be a hardcoded #fff, which is invisible on the light
   theme's near-white modal header */
.modal-title-ico { display: inline-flex; color: inherit; }
/* back button (only shown when a modal was opened from another modal) */
.modal-back {
  display: inline-flex; align-items: center; justify-content: center;
  width: 30px; height: 30px; margin-right: 8px; padding: 0; border-radius: 8px;
  color: var(--text-soft); background: var(--surface-2); border: 1px solid var(--border);
  transition: background .14s, color .14s, border-color .14s;
}
.modal-back:hover { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
.modal-back svg { transform: rotate(180deg); width: 17px; height: 17px; }
.modal-back.hidden { display: none; }
/* position: relative so the phone sheet, which placeList moves in here, can
   be placed against this box (js/combo.js) */
.modal-body { padding: 16px; position: relative; }

/* settings modal: category sidebar (like the app-level settings) */
.modal:has(.settings-layout) { width: 800px; }

/* ===== Watch window (pursuit card click): embedded video + timestamp rows ===== */
/* Watch is the same window as Add/Edit Pursuit — you open one from the other and
   they are about the same record, so a 880px Watch snapping back to a 600px form
   read as two different places. Both take .modal's plain 600px now. */
.watch-body { display: flex; flex-direction: column; gap: 14px; }
/* Video | Pictures | Notes switch: centred, compact field, roomier buttons */
.wt-topbar { position: relative; display: flex; justify-content: center; align-items: center; }
.wt-seg { align-self: center; }
.wt-seg .seg-btn { min-width: 130px; padding: 6px 15px; text-align: center; }
/* Pop-up hand-off button: right edge of the window, aligned under the modal's ✕ (same 20px inset) */
.wt-pop { position: absolute; right: 4px; top: 50%; transform: translateY(-50%); }
.wt-pop.hidden { display: none; }
.wt-pane { display: flex; flex-direction: column; gap: 14px; }
.wt-pane.hidden { display: none; }
.wt-list { margin-top: 6px; }   /* a little air between the player and the Full-Video row */
/* the video's YouTube title, above the player. It is the video's own heading, so
   it reads as one: a line of text, no card around it. `hidden` until YouTube
   answers — nothing moves if it never does. */
.wt-vtitle {
  font-size: 14.5px; font-weight: 700; line-height: 1.35; color: var(--text);
  margin: -2px 2px -4px; overflow-wrap: anywhere;
}
.wt-vtitle.hidden { display: none; }
/* Pictures tab: larger thumbnails than the 96px default */
.wt-photos { display: flex; flex-wrap: wrap; gap: 10px; max-width: calc(5 * 150px + 4 * 10px); }
.wt-photos .photo-thumb { width: 150px; height: 150px; border-radius: 12px; }
/* Report tab: read-only display of the pursuit's write-up (preserves the author's line breaks) */
.wt-notes {
  background: var(--surface-2); border: 1px solid var(--border); border-radius: 12px;
  padding: 14px 16px; font-size: 14.5px; line-height: 1.55; color: var(--text);
  white-space: pre-wrap; overflow-wrap: anywhere; max-height: 340px; overflow-y: auto;
}
/* per-modal header buttons (Watch pop-up) keep a normal gap to the ✕ */
.modal-header .head-extra { margin-right: 8px; }
.wt-frame {
  position: relative; width: 100%; padding-top: 56.25%;   /* 16:9 */
  border-radius: 14px; overflow: hidden; background: #000; border: 1px solid var(--border);
}
.wt-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }
.wt-frame.wt-none { padding-top: 0; height: 110px; display: flex; align-items: center; justify-content: center; background: var(--surface-2); }
.wt-list { display: flex; flex-direction: column; gap: 8px; max-height: 276px; overflow-y: auto; padding-right: 2px; }
.wt-row {
  display: flex; align-items: center; gap: 14px; padding: 11px 14px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface-2);
  color: var(--text); font-family: inherit; text-align: left; cursor: pointer;
  transition: border-color .14s, background .14s, transform .1s;
}
.wt-row:hover { border-color: var(--accent); background: var(--accent-soft); transform: translateX(2px); }
.wt-row.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.wt-play {
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
  width: 30px; height: 30px; border-radius: 50%;
  background: var(--accent-soft); color: var(--accent);
}
.wt-time { font-weight: 800; font-size: 14.5px; min-width: 52px; letter-spacing: .3px; }
.wt-desc { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wt-chan { color: var(--text-soft); font-weight: 600; }
/* plain timestamps = accent blue */
.wt-row { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.wt-row .wt-time { color: var(--accent); }
/* the Full-Video row leads the list in red (video/live) */
.wt-row.full { border-color: color-mix(in srgb, #e5484d 50%, var(--border)); }
.wt-row.full .wt-play { background: #e5484d; color: #fff; }
.wt-row.full .wt-time { color: #e5484d; }
.wt-row.full:hover { border-color: #e5484d; background: color-mix(in srgb, #e5484d 10%, transparent); }
/* favourite timestamps stay yellow */
.wt-row.fav { border-color: color-mix(in srgb, #f5b301 45%, var(--border)); }
.wt-row.fav .wt-play { background: color-mix(in srgb, #f5b301 18%, transparent); color: #f5b301; }
.wt-row.fav .wt-time { color: #f5b301; }

/* ===== Floating video player (window → mini → collapsed; the iframe never reloads) ===== */
@keyframes vpFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes vpWindowIn { from { opacity: 0; transform: translate(-50%, -50%) scale(.96); } to { opacity: 1; transform: translate(-50%, -50%); } }
.vp-backdrop {
  position: fixed; inset: 0; z-index: 290;
  background: rgba(6, 9, 15, .62); backdrop-filter: blur(4px);
  animation: vpFade .16s ease;
}
.vp {
  position: fixed; z-index: 300; display: flex; flex-direction: column; overflow: hidden;
  background: var(--surface); border: 1px solid var(--glass-brd); border-radius: 18px;
  box-shadow: none;
  animation: popIn .22s cubic-bezier(.34,1.4,.64,1);
}
/* big centered window (its own keyframe — popIn would drop the centering transform) */
.vp-window { left: 50%; top: 50%; transform: translate(-50%, -50%); width: min(1240px, 94vw); animation: vpWindowIn .22s cubic-bezier(.34,1.4,.64,1); }
/* floating mini window — draggable, scalable by its corner, keeps playing while you use the app */
.vp-mini { width: 380px; box-shadow: none; }
.vp-mini .vp-head { cursor: move; }
/* corner grips — only the mini scales (evenly, 16:9 kept), from EVERY corner */
.vp-rs { display: none; }
.vp-mini .vp-rs { display: block; position: absolute; width: 18px; height: 18px; z-index: 3; touch-action: none; }
.vp-rs-nw { left: 0; top: 0; cursor: nwse-resize; }
.vp-rs-ne { right: 0; top: 0; cursor: nesw-resize; }
.vp-rs-sw { left: 0; bottom: 0; cursor: nesw-resize; }
.vp-rs-se {
  right: 0; bottom: 0; cursor: nwse-resize;
  background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--text) 38%, transparent) 50%);
  border-bottom-right-radius: 18px;
}

.vp-head {
  display: flex; align-items: center; gap: 9px; padding: 10px 12px;
  border-bottom: 1px solid var(--border); user-select: none; touch-action: none; flex-shrink: 0;
}
.vp-dot {
  width: 8px; height: 8px; border-radius: 50%; background: #e5484d; flex-shrink: 0;
  box-shadow: 0 0 8px 1px rgba(229,72,77,.55); animation: tag-fade 1.8s ease-in-out infinite;
}
/* paused video → the dot holds still */
.vp-dot.paused { animation: none; opacity: .5; box-shadow: none; }
.vp-title { font-weight: 800; font-size: 14.5px; letter-spacing: -.2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.vp-btns { display: flex; align-items: center; gap: 5px; margin-left: auto; }
.vp-btns .icon-btn { color: var(--text-soft); }
.vp-btns .icon-btn:hover { color: var(--text); }
.vp-frame { position: relative; width: 100%; padding-top: 56.25%; background: #000; }   /* 16:9 */
.vp-frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* collapsed → a slim tab on the right screen edge (iOS-style); the video stays loaded and keeps playing */
.vp-tab { display: none; }
.vp-collapsed {
  width: 34px; height: 180px; right: 0; border-radius: 14px 0 0 14px; border-right: none;
  background: linear-gradient(90deg, color-mix(in srgb, #e5484d 12%, var(--surface)), var(--surface));
  box-shadow: none;
}
.vp-collapsed .vp-head { display: none; }
.vp-collapsed .vp-body { position: absolute; left: -99999px; top: 0; width: 480px; }   /* off-screen, NOT display:none → keeps playing */
.vp-collapsed .vp-tab {
  position: relative; display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; color: var(--text-soft); cursor: pointer; touch-action: none; transition: color .14s;
}
.vp-collapsed .vp-tab:hover { color: #e5484d; }
/* the "‹" sits dead centre; the live dot + speaker (only while sound is playing) stack above it */
.edge-ind {
  position: absolute; left: 50%; transform: translateX(-50%); bottom: calc(50% + 16px);
  display: flex; flex-direction: column; align-items: center; gap: 9px;
}
/* the speaker keeps its slot (nothing jumps) and fades out when the sound stops */
.edge-spk {
  display: flex; opacity: 0; transform: scale(.82);
  transition: opacity .45s ease, transform .45s ease;
}
.edge-spk.on { opacity: 1; transform: none; transition: opacity .16s ease, transform .16s ease; }
.edge-ind .edge-spk svg { width: 17px; height: 20px; }   /* wins over the generic 20×20 tab-icon rule */

.settings-layout { display: flex; gap: 20px; min-height: 360px; }
.set-nav {
  display: flex; flex-direction: column; gap: 3px;
  width: 190px; flex-shrink: 0; border-right: 1px solid var(--border); padding-right: 14px;
}
.set-cat {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 12px; border: none; background: transparent; color: var(--text-soft);
  border-radius: 9px; cursor: pointer; font-size: 13.5px; font-weight: 500; text-align: left;
  transition: background .15s ease, color .15s ease;
}
.set-cat:hover { background: var(--surface-2); color: var(--text); }
.set-cat.active { background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.set-content { flex: 1; min-width: 0; }
.set-panel { display: flex; flex-direction: column; gap: 16px; }
.set-panel h3 { margin: 0 0 4px; font-size: 16px; }
/* keep controls content-width so they don't stretch across the panel */
.set-panel .seg { align-self: flex-start; }
/* Backup: tidy button rows */
.set-btnrow { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
/* Map settings sliders: scanner-style track + an accent value pill */
.set-slider { gap: 12px; max-width: 320px; }
.set-slider-val {
  min-width: 52px; text-align: center; padding: 4px 10px; border-radius: 999px;
  background: var(--accent-soft); color: var(--accent);
  font-size: 12.5px; font-weight: 700; flex-shrink: 0;
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}

/* ===== Stats ===== */
.stat-kpis { display: grid; grid-template-columns: repeat(6, 1fr); gap: 14px; margin-bottom: 18px; position: relative; z-index: 5; }
.stats-grid { position: relative; z-index: 1; }
.kpi { display: flex; align-items: center; gap: 13px; padding: 17px 18px; transition: transform .14s, box-shadow .14s; }
.kpi:hover { transform: translateY(-2px); box-shadow: inset 0 1px 0 rgba(255,255,255,.05); }
.kpi-ico { width: 48px; height: 48px; border-radius: 14px; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
/* Deceased KPI: slowly blinking red icon with a pulsing red glow */
.kpi-deceased .kpi-ico { animation: kpiBlink 2.2s ease-in-out infinite; }
@keyframes kpiBlink {
  0%, 100% { opacity: 1; }
  50% { opacity: .5; }
}
.kpi-body { min-width: 0; }
.kpi-num { font-size: 31px; font-weight: 800; line-height: 1.0; letter-spacing: -.7px; }
.kpi-lbl { font-size: 11.5px; color: var(--text-soft); text-transform: uppercase; letter-spacing: .5px; font-weight: 700; margin-top: 3px; }
.kpi-sub { font-size: 11px; color: var(--text-soft); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Escaped card hover popover — outcome breakdown mini bars */
.kpi-haspop { position: relative; }
.kpi-pop {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 30; width: 260px;
  padding: 12px 13px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--border); box-shadow: none;
  opacity: 0; visibility: hidden; transform: translateY(-4px);
  transition: opacity .15s, transform .15s, visibility .15s; pointer-events: none;
}
.kpi-haspop:hover .kpi-pop { opacity: 1; visibility: visible; transform: translateY(0); }
.kpi-pop-note { font-size: 11.5px; color: var(--text-soft); margin-bottom: 10px; line-height: 1.45; }
.kpi-bar-row { display: flex; align-items: center; gap: 9px; margin: 6px 0; }
.kpi-bar-lbl { font-size: 11px; width: 80px; flex-shrink: 0; color: var(--text); }
.kpi-bar-track { flex: 1; height: 8px; background: var(--surface-2); border-radius: 999px; overflow: hidden; }
.kpi-bar-fill { height: 100%; border-radius: 999px; transition: width .2s; }
.kpi-bar-val { font-size: 11px; width: 34px; text-align: right; color: var(--text-soft); font-weight: 700; }

.stats-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
/* two half-width doughnuts (Outcomes + TVI) sharing one grid column */
.stat-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; min-width: 0; }
.stat-pair .chart-box { min-width: 0; }
.chart-box { padding: 16px 18px 18px; }
.chart-box.wide { grid-column: 1 / -1; }
.chart-head { display: flex; align-items: center; gap: 9px; margin-bottom: 14px; }
.chart-head h3 { margin: 0; font-size: 15.5px; font-weight: 700; }
.chart-head .chart-ico { display: inline-flex; color: var(--accent); opacity: .85; }
.chart-wrap { position: relative; height: 248px; }
.chart-wrap.tall { height: 300px; }
.chart-box canvas { max-height: none; }

@media (max-width: 1100px) { .stat-kpis { grid-template-columns: repeat(3, 1fr); } }

/* ===== Map ===== */
#mapContainer { width: 100%; height: calc(100vh - 210px); min-height: 420px; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border); }
.map-popup strong { display: block; }
.map-token-warn { padding: 20px; }

.seg {
  display: inline-flex; border: 1px solid var(--glass-brd); border-radius: 12px; overflow: hidden;
  padding: 3px; gap: 3px; background: var(--surface);
}
.seg-btn {
  border: none; background: transparent; color: var(--text-soft); padding: 8px 15px; border-radius: 9px;
  cursor: pointer; font-size: 13px; font-weight: 600;
  transition: background .14s ease, color .14s ease;
}
.seg-btn:hover { background: var(--surface-2); color: var(--text); }
.seg-btn.active { background: var(--accent); color: var(--accent-ink); }
/* Employment Change tab in the Change panel — red */
.seg-btn.seg-danger { color: var(--danger); }
.seg-btn.seg-danger.active { background: var(--danger); color: #fff; }

/* constant-size red dot pins with a soft fade (do not scale with zoom) */
.map-pin {
  background: #e01b1b; border-radius: 50%; cursor: pointer; box-sizing: border-box;
  box-shadow: 0 0 0 2px rgba(224,27,27,.20), 0 0 7px 2px rgba(224,27,27,.40);
}
/* invisible padding so the pin is a bit easier to click (doesn't change the visible size / anchor) */
.map-pin::after { content: ''; position: absolute; inset: -6px; border-radius: 50%; }
.map-pin:hover { box-shadow: 0 0 0 3px rgba(224,27,27,.28), 0 0 10px 3px rgba(224,27,27,.55); }
/* normal arrow cursor over the map, hand pointer only over pins */
.mapboxgl-canvas-container.mapboxgl-interactive,
.mapboxgl-canvas-container.mapboxgl-interactive .mapboxgl-canvas { cursor: default; }
.mapboxgl-canvas-container.mapboxgl-interactive.mapboxgl-track-pointer,
.mapboxgl-canvas-container.mapboxgl-interactive:active { cursor: default; }

/* map spans the full content width */
#view-map .map-panel { max-width: none; margin: 0; }
.map-panel { position: relative; }
/* Full screen (the ⤢ button in the map's own controls). A class, not the
   Fullscreen API — iOS only grants that to a <video>. The panel covers
   everything including the rail; the button turns into ⤡ to come back. */
/* z-index 95: over the rail (70) and the right-edge panels (60/90), but UNDER the
   modal overlay (100) — a window opened from a pin's popup ("View Media") was
   painted behind the map. The floating video player (300) still wins too. */
.map-panel.map-full {
  position: fixed; inset: 0; z-index: 95; margin: 0; padding: 0;
  border-radius: 0; max-width: none; background: var(--bg);
}
.map-panel.map-full #mapContainer {
  height: 100%; min-height: 0; border-radius: 0; border: 0;
}

/* Layers flyout (bottom-left, Google-Maps style): base map + Heatmap + Jurisdictions */
.map-layers { position: absolute; bottom: 14px; left: 14px; z-index: 6; display: flex; align-items: flex-end; gap: 10px; }
/* move the Mapbox logo to the bottom-right (above the attribution), clear of the Layers panel */
#mapContainer .mapboxgl-ctrl-bottom-left, #pk-map .mapboxgl-ctrl-bottom-left { left: auto; right: 0; bottom: 22px; }
/* zoom / rotation controls: frosted milchglas (matches the other map controls) */
.mapboxgl-ctrl-group {
  background: var(--map-glass) !important;
  -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px);
  border: 1px solid var(--map-glass-brd);
  border-radius: 12px !important; overflow: hidden;
  box-shadow: inset 0 1px 0 var(--glass-hi) !important;
}
.mapboxgl-ctrl-group button { background: transparent !important; }
.mapboxgl-ctrl-group button:hover { background: color-mix(in srgb, var(--text) 12%, transparent) !important; }
.mapboxgl-ctrl-group button + button { border-top: 1px solid var(--map-glass-brd) !important; }
html[data-theme="dark"] .mapboxgl-ctrl-group button .mapboxgl-ctrl-icon { filter: invert(1) brightness(1.6); }
.map-layers-btn, .ml-tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  width: 86px; height: 82px; border-radius: 18px; cursor: pointer; flex-shrink: 0; text-align: center;
  padding: 6px 5px; overflow: hidden; box-sizing: border-box;
  background: var(--map-glass); border: 1px solid var(--map-glass-brd); color: var(--text-soft);
  -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px);
  box-shadow: inset 0 1px 0 var(--glass-hi);
  font-size: 11px; font-weight: 600; line-height: 1.1; transition: border-color .15s, color .15s, background .15s, box-shadow .15s;
}
.map-layers-btn { color: var(--text); }
.map-layers-btn:hover, .ml-tile:hover { color: var(--text); border-color: var(--accent); }
.map-layers-pop { display: flex; gap: 8px; }
.map-layers-pop.hidden { display: none; }
.ml-tile.active { color: var(--accent); border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 40%, transparent), inset 0 1px 0 var(--glass-hi); }
.ml-ic { height: 36px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
/* Pick-Location layers flyout is more compact than the main map's */
#pk-layers .map-layers-btn, #pk-layers .ml-tile { width: 62px; height: 58px; border-radius: 12px; font-size: 9.5px; gap: 3px; padding: 4px 3px; }
#pk-layers .ml-ic { height: 26px; }
/* long labels (e.g. "Zuständigkeiten") wrap to a second line instead of being cut off */
.ml-label { display: block; max-width: 100%; white-space: normal; overflow-wrap: anywhere; line-height: 1.12; }

/* floating icon controls in the map's top-right corner (History, Hide Pins) */
.map-ctrls { position: absolute; top: 14px; right: 14px; z-index: 6; display: flex; flex-direction: column; gap: 8px; }
.map-ctrl-btn {
  width: 40px; height: 40px; border-radius: 50%; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: var(--map-glass); border: 1px solid var(--map-glass-brd); color: var(--text);
  -webkit-backdrop-filter: saturate(160%) blur(14px); backdrop-filter: saturate(160%) blur(14px);
  box-shadow: inset 0 1px 0 var(--glass-hi);
  transition: background .15s, color .15s, border-color .15s;
}
.map-ctrl-btn:hover { border-color: var(--accent); }
.map-ctrl-btn.active { background: var(--accent); border-color: var(--accent); color: #fff; }

/* Map History panel (year selector, floats just left of the corner controls).
   Built to LOOK like the Statistics year popover — same surface, same radius,
   same rows (.shp-title / .shp-item) — because it is the same question asked in
   two places. The one difference is that this one takes several years at once,
   so a picked row stays filled in and "All" switches the lot. */
.map-history {
  position: absolute; top: 14px; right: 62px; width: 104px; z-index: 5;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: none;
  padding: 8px; color: var(--text); max-height: calc(100% - 28px); overflow-y: auto;
  display: flex; flex-direction: column; gap: 2px;
}
.map-history.hidden { display: none; }
/* the year rows are .shp-item like Statistics'; the count rides at the right end */
.map-history .shp-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.mh-empty { padding: 8px; font-size: 12.5px; }

/* Statistics year popover (opened by the clock-icon button, top-right) */
.stat-hist-wrap { position: relative; }
.stat-hist-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; min-width: 86px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: none; padding: 8px;
  display: flex; flex-direction: column; gap: 2px;
}
.stat-hist-pop.hidden { display: none; }
.shp-title { font-size: 11px; color: var(--text-soft); text-transform: uppercase; letter-spacing: .5px; padding: 2px 8px 4px; }
.shp-item {
  border: none; background: transparent; color: var(--text); text-align: left;
  padding: 7px 12px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600;
}
.shp-item:hover { background: color-mix(in srgb, var(--accent) 12%, transparent); }
.shp-item.active { background: var(--accent); color: var(--accent-ink); }

/* year shown after the Statistics heading */
.h1-sub { color: var(--text-soft); font-weight: 600; font-size: .68em; letter-spacing: .5px; }

/* remove yellow/native focus ring on ALL fields; clean accent ring instead */
#map-search { min-width: 200px; }
#map-search::-webkit-search-cancel-button { -webkit-appearance: none; display: none; }
input:focus, select:focus, textarea:focus, .dp-text:focus {
  outline: none !important; border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft);
}
.input-prefix:focus-within { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-soft); }
.input-prefix input:focus { box-shadow: none; }
input:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {
  -webkit-box-shadow: 0 0 0 40px var(--surface) inset; -webkit-text-fill-color: var(--text);
  caret-color: var(--text);
}

/* nicer map popup — themed for light + dark mode */
.mapboxgl-popup-content {
  border-radius: 20px; padding: 15px 17px 14px;
  background: var(--map-glass); color: var(--text); border: 1px solid var(--map-glass-brd);
  -webkit-backdrop-filter: saturate(170%) blur(20px); backdrop-filter: saturate(170%) blur(20px);
  box-shadow: inset 0 1px 0 var(--glass-hi);
}
.mapboxgl-popup-close-button {
  color: var(--text-soft); font-size: 18px; line-height: 22px; font-weight: 400;
  width: 24px; height: 24px; padding: 0; top: 6px; right: 6px; border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
}
.mapboxgl-popup-close-button:hover { background: var(--surface-2); color: var(--text); }
.mapboxgl-popup-anchor-top .mapboxgl-popup-tip { border-bottom-color: var(--surface); }
.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { border-top-color: var(--surface); }
.mapboxgl-popup-anchor-left .mapboxgl-popup-tip { border-right-color: var(--surface); }
.mapboxgl-popup-anchor-right .mapboxgl-popup-tip { border-left-color: var(--surface); }
.map-popup { font-size: 13px; line-height: 1.5; min-width: 210px; }
.mp-head { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 11px; }
.mp-head-main { min-width: 0; flex: 1; }
.mp-case { display: block; font-size: 16.5px; font-weight: 800; letter-spacing: -.3px; color: #fff; line-height: 1.15; }
html[data-theme="light"] .mp-case { color: var(--text); }
.mp-meta-line { display: flex; align-items: center; gap: 8px; margin-top: 4px; font-size: 13.5px; color: var(--text); font-weight: 700; letter-spacing: .2px; }
.mp-rating { display: flex; margin-top: 5px; }   /* stars sit on their own line under the date */
.mp-head-btns { display: flex; align-items: center; gap: 3px; margin-left: auto; flex-shrink: 0; }
.mp-hide-btn, .mp-close-btn, .mp-zoom-btn {
  width: 24px; height: 24px; padding: 0; border: none; cursor: pointer;
  background: color-mix(in srgb, var(--text) 7%, transparent);
  color: var(--text-soft); display: inline-flex; align-items: center; justify-content: center;
  border-radius: 8px; flex-shrink: 0; transition: background .14s, color .14s;
}
.mp-hide-btn:hover, .mp-close-btn:hover { color: #fff; background: var(--danger); box-shadow: 0 0 13px 2px color-mix(in srgb, var(--danger) 75%, transparent); }
.mp-zoom-btn:hover { color: var(--accent-ink); background: var(--accent); box-shadow: 0 0 13px 2px color-mix(in srgb, var(--accent) 80%, transparent); }
/* body: labeled sections with chips */
.mp-body { display: flex; flex-direction: column; gap: 11px; margin-bottom: 10px; }
.mp-sec { display: flex; flex-direction: column; align-items: flex-start; gap: 5px; min-width: 0; }   /* chips size to content (e.g. "BUS" stays centred in its pill, not stretched) */
.mp-lbl { font-size: 10px; font-weight: 800; letter-spacing: 1.1px; text-transform: uppercase; color: var(--text-soft); }
.mp-tchip {
  display: inline-flex; align-items: center; padding: 4px 10px; border-radius: 999px;
  background: color-mix(in srgb, var(--text) 8%, transparent); border: 1px solid var(--map-glass-brd);
  color: var(--text); font-size: 12px; font-weight: 600; white-space: nowrap;
}
/* troopers as chips, stacked one per line (chip look kept) */
.mp-tstack { flex-direction: column; align-items: flex-start; gap: 5px; }
/* outcome tag matches the chip sizing (like the other values) */
.mp-sec .tag { font-size: 12px; padding: 4px 11px; font-weight: 600; }

/* location pin → opens the pursuit on the in-app Pursuit Map */
.loc-link { border: none; background: none; padding: 4px; cursor: pointer; color: var(--accent); display: inline-flex; align-items: center; border-radius: 6px; }
.loc-link:hover { background: var(--accent-soft); }
/* unknown location: a dimmed, struck-through map-pin instead of a clickable link */
.loc-none { display: inline-flex; color: var(--text-soft); opacity: .55; cursor: default; }

/* sort control: a Newest/Oldest segmented toggle */
.sort-ctl { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
.sort-ctl .seg-btn { font-size: 15px; }
.case-pill {
  display: inline-flex; align-items: center; padding: 3px 10px; border-radius: 7px;
  background: var(--glass-strong); color: var(--text); font-weight: 700; font-size: 13px;
  border: 1px solid var(--glass-brd);
  -webkit-backdrop-filter: saturate(150%) blur(8px); backdrop-filter: saturate(150%) blur(8px);
  box-shadow: inset 0 1px 0 var(--glass-hi);
}
.case-pill.empty { color: var(--text-soft); font-weight: 400; }

/* Event Tags and anything else that takes SEVERAL values. The field itself is
   an ordinary search box — what is picked is NOT drawn into it, it is the check
   marks in the list. One line high whatever is on. */
.combo-opt.picked { color: var(--text); font-weight: 700; }
/* every check mark in the same column, whatever the label's length: the label
   takes the leftover width and the mark keeps its own fixed one */
.combo-opt.has-del .combo-opt-lbl, .combo-multi .combo-opt-lbl { flex: 1; }
.combo-multi .combo-opt { display: flex; align-items: center; gap: 8px; }
.combo-tick {
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; flex: 0 0 16px; color: var(--success);
}

/* Full Video ≈ same width as the Timestamps description field */
.full-video-field { width: calc(100% - 168px); }

/* timestamp editor (time + description + favorite) */
/* the "+" sits level with the FIRST line, not halfway down a growing list */
/* the add row under the list */
.ts-add-row { margin-top: 6px; }
.ts-list { flex: 1; min-width: 0; }
/* A timestamp is a narrow left column — the star, and under it the button that
   assigns a trooper — beside the line itself, with the assigned names on their
   own row under the description. Side by side, the names left the time and the
   description nowhere near enough width on a phone. */
.ts-item { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 8px; }
/* The star's column is as wide as the "+" that adds a line, so the two sit in
   the same vertical line — the star reads as the head of that column and the rest
   of the row starts to the right of it. */
.ts-side {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 5px;
  flex: 0 0 46px; width: 46px;
  /* …and on the middle of the line beside it: the row itself is top-aligned, so
     without this the star sat 6px above the centre of the time field */
  align-self: center;
}
.ts-main { flex: 1; min-width: 0; }
.ts-line { display: flex; align-items: center; gap: 8px; }
.ts-star { border: none; background: none; cursor: pointer; color: var(--text-soft); padding: 0; line-height: 0; display: inline-flex; flex: 0 0 auto; }
.ts-star.on { color: #f5b301; }
.ts-star.on svg { fill: #f5b301; }
.ts-time { font-variant-numeric: tabular-nums; font-weight: 500; width: 82px; flex: 0 0 82px; text-align: center; }
.ts-desc { flex: 1; min-width: 0; }
/* the first line has no ✕ — this keeps its description the same width as the rest */
.ts-rm-slot { width: 21px; flex: 0 0 21px; }
.ts-trp-slot { position: relative; line-height: 0; flex: 0 0 auto; }
/* Who is on the line: ONE round button at the end of it, as tall as the field
   beside it. Dashed and grey while nobody is assigned, solid green once somebody
   is — and the names themselves ride in its tooltip, so the line stays a line. */
.ts-trp-add {
  border: 1px dashed var(--border); background: none; color: var(--text-soft);
  /* 32px = the height every field in this form has, and the circle is as wide as
     it is tall */
  border-radius: 50%; width: 32px; height: 32px; cursor: pointer; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  transition: color .14s ease, border-color .14s ease, background .14s ease;
}
.ts-trp-add:hover { color: var(--accent); border-color: var(--accent); }
.ts-trp-add.on {
  border-style: solid; border-color: var(--success); color: var(--success);
  background: color-mix(in srgb, var(--success) 14%, transparent);
}
.ts-trp-menu {
  position: absolute; top: calc(100% + 4px); right: 0; z-index: 30; line-height: 1.5;
  min-width: 190px; max-height: 240px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  padding: 4px; box-shadow: 0 12px 30px -10px rgba(0,0,0,.45);
}
.ts-trp-menu button {
  display: block; width: 100%; text-align: left; border: none; background: none;
  color: var(--text); cursor: pointer; padding: 6px 9px; border-radius: 8px; font-size: 12.5px;
  /* a name is one line — it used to wrap into two and the list read as a paragraph */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.ts-trp-menu button:hover { background: var(--surface-2); color: var(--accent); }
/* the ones already on the line, so a second visit can take them off again */
.ts-trp-menu button.on { color: var(--success); font-weight: 700; }
.ts-trp-none { padding: 6px 9px; font-size: 12px; color: var(--text-soft); white-space: nowrap; }
/* …and the last row: a name that is on nobody's list (a deputy, another agency).
   It belongs to this timestamp alone and never joins the trooper list. */
.ts-trp-own { border-top: 1px solid var(--border); margin-top: 4px; padding: 6px 5px 3px; }
.ts-trp-own input { width: 100%; }
/* the empty-field reminder on save */
.cf-lead { display: block; margin-bottom: 6px; }
.cf-list { margin: 0; padding-left: 20px; text-align: left; display: inline-block; }
.cf-list li { margin: 1px 0; }
.ts-rm { border: none; background: none; color: var(--text-soft); cursor: pointer; font-size: 13px; padding: 2px 4px; }
.ts-rm:hover { color: var(--danger); }

/* History log */
.history-list { list-style: none; padding: 0; margin: 10px 0 0; }
.history-list li { padding: 8px 0; border-bottom: 1px solid var(--border); display: flex; gap: 10px; align-items: baseline; }
/* Delete on a history row: a bare ✕, not a button. One of these sits on every
   line of a long list, and a row of red buttons read as the point of the list
   rather than as the thing you reach for once. It says what it is on hover. */
.h-del {
  flex-shrink: 0; padding: 0; margin-right: 2px; border: 0; background: none;
  color: var(--text); opacity: .55; cursor: pointer;
  font-size: 12px; line-height: 1; width: 14px;
  transition: color .14s, opacity .14s;
}
.h-del:hover { color: var(--danger); opacity: 1; }
/* The ✕ on a Last-seen row only shows while the pointer is over that row — it is
   there for the rare correction, not for reading the list. Opacity, not display,
   so the row does not jump; and only where there IS a pointer, or a touch screen
   (which never hovers) would have no way to reach it at all. It still appears
   for the keyboard, on focus. */
@media (hover: hover) {
  .ls-item .h-del { opacity: 0; }
  .ls-item:hover .h-del { opacity: .55; }
  .ls-item .h-del:focus-visible { opacity: 1; }
  .ls-item:hover .h-del:hover { opacity: 1; }
}
.history-list .h-date { font-weight: 600; min-width: 92px; white-space: nowrap; }
/* Who they WERE on that row's date, between the date and what the row is about.
   As wide as the NAME and no wider: a fixed column left a different gap after
   every name, and a name is not a thing to cut short or wrap. The row's own
   10px gap then sits on both sides of it — the same one the date already has. */
.history-list .h-held {
  width: var(--ls-held-w, auto); min-width: var(--ls-held-w, 0); flex: 0 0 auto;
  white-space: nowrap; color: var(--text-soft); font-size: 12.5px;
}
/* The change itself is greyed the way a Last-seen row's "who they were" is: both are the record
   part of the line, and the date is what you read down the list by (Rick, 2026-09-05). */
.history-list .h-text { flex: 1; font-size: 12.5px; white-space: nowrap; color: var(--text-soft); }
/* Pursuit History rows: date · case · call sign · rank (clickable) */
/* No hover highlight in the Last-Seen and Pursuit-History lists — they read as
   a record, like the Change history above them, and a row lighting up under the
   cursor made them look like a menu. The rows that DO open something keep the
   pointer cursor, which is the part that actually says "clickable". */
.pursuit-history li { cursor: default; }
/* only the case number opens the pursuit, so only it reacts to the cursor */
.pursuit-history li[data-pursuit] [data-phopen] { cursor: pointer; }
.pursuit-history li[data-pursuit] [data-phopen]:hover { text-decoration: underline; }
.pursuit-history .ph-case { min-width: 104px; font-weight: 700; color: var(--accent); white-space: nowrap; }
/* all trooper / dog detail modals share the same fixed width */
.modal:has(.detail-modal-body) { width: 750px; max-width: calc(100vw - 32px); }
/* history tabs show ~10 rows, then scroll; long change notes wrap instead of overflowing */
.detail-modal-body .history-list { max-height: 342px; overflow-y: auto; }
.detail-modal-body .history-list .h-text { white-space: normal; }
/* ONE row height across all three tabs of the Details window (Rick, 2026-09-06). They were three
   different heights — 38 for a Changes line, 40.5 for a Last-seen line carrying a kind label, 30
   for one without — so switching tabs moved every row, and the list read as two lists rather than
   one record. The tallest of the three is the measure (the label pill is what sets it), and the
   rows are then centred on it: with the padding alone the shorter lines sat at the top of their
   own box. `min-height`, not `height`: a long change note still wraps here rather than being cut.
   `li.ls-item`, not `.ls-item` — `.history-list li` sets the padding as a shorthand and a bare
   class loses to it however far down this file it stands. */
.detail-modal-body .history-list li,
.detail-modal-body .history-list li.ls-item {
  min-height: 41px; box-sizing: border-box; align-items: center;
}

/* History view — aggregate change log (timeline, grouped by date) */
.hist-timeline { display: flex; flex-direction: column; gap: 18px; }
.hist-ghead { display: flex; align-items: center; gap: 9px; margin: 0 0 9px 2px; }
.hist-gdate { font-size: 12.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--text-soft); }
.hist-items { display: flex; flex-direction: column; gap: 8px; padding-left: 18px; border-left: 2px solid var(--border); }
/* each timeline row = entry card + a separate notes field beside it */
.hist-row { display: flex; align-items: stretch; gap: 80px; max-width: 1150px; }
/* The change itself sits in its own sunken field, the way a deletion request
   shows what it is about (.req-detail) — so the card carries the lighter
   surface and the field the darker one, exactly the pair those rows use. The
   height follows the field instead of being fixed at 70. */
.hist-item {
  position: relative; display: flex; align-items: center; flex: 0 0 570px; width: 570px; min-width: 0;
  min-height: 70px; padding: 9px 14px; border-radius: 15px;
  background: var(--surface-2); border: 1px solid var(--border); border-left: 4px solid var(--accent);
  transition: transform .12s ease, border-color .14s ease, box-shadow .14s ease;
}
.hist-item::before {
  content: ""; position: absolute; left: -25px; top: 50%; transform: translateY(-50%);
  width: 11px; height: 11px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px var(--bg);
}
.hist-item:hover { transform: translateX(2px); box-shadow: none; }
.hist-body { min-width: 0; flex: 1; cursor: pointer; display: flex; flex-direction: column; justify-content: center; }
.hist-top { display: flex; align-items: center; gap: 9px; margin-bottom: 3px; flex-wrap: wrap; }
.hist-name { font-weight: 700; font-size: 14px; }
.hist-note {
  font-size: 12.5px; color: var(--text-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  padding: 7px 10px; border-radius: 9px; margin-top: 2px;
  background: var(--surface); border: 1px solid var(--glass-brd);
}
/* the arrow between the two states is drawn by CSS, so the same markup can lay
   itself out sideways on a wide screen and stacked on a phone (see the upright
   block, where it becomes a ↓ on its own line) */
.hn-arrow::before { content: '→'; margin: 0 .4em; }
/* the little note button in an entry's bottom-right corner — the notes field
   only appears once it is pressed */
.hist-note-btn {
  position: absolute; right: 8px; bottom: 6px;
  width: 22px; height: 22px; padding: 0; display: inline-flex;
  align-items: center; justify-content: center;
  border: 1px solid var(--glass-brd); border-radius: 7px;
  background: var(--surface-2); color: var(--text-soft); cursor: pointer;
  /* dimmed, not hidden-until-hover: there is no hover on the phone this is
     mostly used on, and a control you cannot find is not a control */
  opacity: .55; transition: opacity .14s, color .14s, border-color .14s;
}
.hist-item:hover .hist-note-btn { opacity: 1; }
.hist-note-btn:hover { color: var(--accent); border-color: var(--accent); }
/* One switch, both directions: a row with a note shows the field and hides the
   button; a row without shows the button and no field. Emptying a note is then
   a class off, not a re-render. */
.hist-row:not(.has-memo) .hist-memo { display: none; }
.hist-row.has-memo .hist-note-btn { display: none; }
/* Keep clear of the button instead of running under it. The field is a box now,
   so it is the BOX that has to stop short — padding would only move the text and
   leave the outline passing behind the button. */
.hist-row:not(.has-memo) .hist-item.has-note-btn .hist-note { margin-right: 26px; }
/* notes field: a display box (clickable links) that turns into a textarea on click */
.hist-memo {
  width: 500px; flex: 0 0 500px; align-self: stretch; box-sizing: border-box;
  height: 70px; overflow-y: auto; cursor: text;
  font-size: 12.5px; line-height: 1.4; padding: 7px 10px; font-family: inherit;
  border: 1px solid var(--border); border-radius: 15px; background: var(--surface-2); color: var(--text);
  white-space: pre-wrap; word-break: break-word;
}
.hist-memo.editing { padding: 0; overflow: hidden; }
.hist-memo-ph { color: var(--text-soft); }
.hist-memo-input {
  width: 100%; height: 100%; box-sizing: border-box; resize: none; border: none; outline: none;
  background: transparent; color: var(--text); font: inherit; padding: 7px 10px;
}
.hist-memo-input::placeholder { color: var(--text-soft); }
.hist-type {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .4px;
  padding: 2px 8px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 18%, transparent); color: var(--accent);
}
/* per-type colours: left accent bar + timeline dot + label pill */
.hist-item.type-k9 { border-left-color: #8957e5; }
.hist-item.type-k9::before { background: #8957e5; }
.hist-type.type-k9 { background: color-mix(in srgb, #8957e5 20%, transparent); color: #8957e5; }
.hist-item.type-callsign { border-left-color: var(--accent); }
.hist-item.type-callsign::before { background: var(--accent); }
.hist-item.type-rank { border-left-color: var(--warn); }
.hist-item.type-rank::before { background: var(--warn); }
.hist-type.type-rank { background: color-mix(in srgb, var(--warn) 22%, transparent); color: var(--warn); }
/* employment status change (Fired / Retired / Resigned / Shot & Killed) → red */
.hist-item.type-status { border-left-color: var(--danger); }
.hist-item.type-status::before { background: var(--danger); }
.hist-type.type-status { background: color-mix(in srgb, var(--danger) 18%, transparent); color: var(--danger); }
/* rehired → green */
.hist-item.type-rehired { border-left-color: var(--success); }
.hist-item.type-rehired::before { background: var(--success); }
.hist-type.type-rehired { background: color-mix(in srgb, var(--success) 18%, transparent); color: var(--success); }

/* required-field highlight on save */
.field-invalid { border-color: var(--danger) !important; box-shadow: 0 0 0 2px rgba(217,52,43,.18) !important; }
/* the two timestamp controls that are not boxes: the round trooper button and the
   star. They say the same thing in their own shape — red, no ring around it. */
.ts-trp-add.field-invalid { color: var(--danger) !important; box-shadow: none !important; }
.ts-star.field-invalid { color: var(--danger) !important; border: none !important; box-shadow: none !important; }
.ts-star.field-invalid svg { fill: none; }

/* nicer scrollbars everywhere */
* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; }
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; border: 2px solid var(--bg); }
::-webkit-scrollbar-thumb:hover { background: var(--text-soft); }

/* ===== Scanner view (dashboard) ===== */
.scan-master { display: flex; align-items: center; flex-wrap: wrap; gap: 14px 16px; padding: 16px 20px; margin-bottom: 16px; }   /* full width, like the feed grid under it */
.scan-master .spacer { flex: 1; min-width: 16px; }
.scan-mbtn { flex-shrink: 0; }
.scan-mbtn.on { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, var(--glass-brd)); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.scan-allbtn {
  display: inline-flex; align-items: center; gap: 9px; flex-shrink: 0;
  padding: 12px 20px; border-radius: 999px; cursor: pointer; font-size: 14px; font-weight: 700;
  border: 1px solid color-mix(in srgb, var(--accent), #fff 22%); color: #fff;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent), #fff 13%), var(--accent));
  box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--accent) 85%, transparent);
  transition: filter .15s, transform .1s, background .15s;
}
.scan-allbtn.on { background: var(--danger); border-color: var(--danger); box-shadow: 0 6px 18px -8px color-mix(in srgb, var(--danger) 85%, transparent); }
.scan-allbtn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.scan-master-vol { width: 240px; max-width: 100%; flex-shrink: 0; }
.scan-master-label { font-size: 12.5px; color: var(--text-soft); font-weight: 600; margin-bottom: 9px; }
.scan-master-label b { color: var(--text); font-weight: 800; }
.scan-live { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 700; color: var(--text-soft); white-space: nowrap; flex-shrink: 0; }
.scan-live-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--border); flex-shrink: 0; }
.scan-live-dot.on { background: var(--danger); box-shadow: 0 0 8px 1px color-mix(in srgb, var(--danger) 70%, transparent); animation: tag-fade 1.8s ease-in-out infinite; }

/* Up to five feeds across. `auto-fill` on its own would keep adding columns on a
   very wide screen; the cap is what keeps a feed card a readable size rather
   than a thin strip. Below that it still falls back to as many as fit. */
.scan-grid {
  display: grid; gap: 16px;
  grid-template-columns: repeat(auto-fill, minmax(max(240px, (100% - 4 * 16px) / 5), 1fr));
}
/* the Pursuit dashboard's gutter on BOTH sides — it had the left one already,
   but kept the generic 40px on the right, so the page sat off-centre against
   every other view */
#view-scanner { padding-left: 150px; padding-right: 154px; }
.scanf {
  /* feed cards use their own accent (red) — change this one line to retune */
  --accent: #e5484d;
  display: flex; flex-direction: column; gap: 12px; min-height: 210px; cursor: pointer; position: relative;
  background: var(--surface); border: 1px solid var(--glass-brd); border-radius: 18px; padding: 20px 22px;
  transition: border-color .14s, box-shadow .14s, opacity .14s, transform .14s;
}
.scanf:hover { border-color: color-mix(in srgb, var(--accent) 32%, var(--glass-brd)); }
.scanf.playing { border-color: color-mix(in srgb, var(--accent) 55%, var(--glass-brd)); box-shadow: 0 12px 30px -16px color-mix(in srgb, var(--accent) 60%, transparent); }
.scanf.offline { opacity: .55; }
/* dragged card: clean lifted look regardless of playing/audible glow */
.scanf.dragging { opacity: .5; transform: scale(.99); box-shadow: none !important; border-color: color-mix(in srgb, var(--accent) 45%, var(--glass-brd)) !important; }
/* insertion marker = a glowing bar in the gutter (a pseudo-element, so it shows even on playing/audible feeds whose box-shadow is taken by their glow) */
.scanf.drop-before::before, .scanf.drop-after::before {
  content: ''; position: absolute; top: 12%; bottom: 12%; width: 4px; border-radius: 4px;
  background: var(--accent); box-shadow: 0 0 10px 1px color-mix(in srgb, var(--accent) 70%, transparent);
}
.scanf.drop-before::before { left: -10px; }
.scanf.drop-after::before { right: -10px; }
/* the feed you actually hear stands out clearly */
.scanf.audible { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), 0 14px 34px -14px color-mix(in srgb, var(--accent) 60%, transparent); }
/* interactive controls opt out of the card's pointer cursor */
.scanf .scanf-play, .scanf .scanf-mute, .scanf .scanf-solo, .scanf .scan-range { cursor: pointer; }

.scanf-head { display: flex; align-items: center; gap: 10px; min-width: 0; }
.scanf-name { flex: 1; min-width: 0; font-weight: 800; font-size: 20px; line-height: 1.2; white-space: nowrap; overflow: hidden; }
.scanf.muted .scanf-name { opacity: .6; }

.scan-badge { font-size: 9.5px; font-weight: 800; letter-spacing: .7px; padding: 3px 9px; border-radius: 999px; flex-shrink: 0; }
.scan-badge.live { color: var(--danger); background: color-mix(in srgb, var(--danger) 16%, transparent); animation: tag-fade 1.8s ease-in-out infinite; }
.scan-badge.off { color: var(--danger); background: color-mix(in srgb, var(--danger) 14%, transparent); }
.scan-badge.on { color: var(--success); background: color-mix(in srgb, var(--success) 15%, transparent); }
.scan-badge.load { color: var(--text-soft); background: color-mix(in srgb, var(--text-soft) 18%, transparent); }

/* always reserve the meta row's height so the controls line up whether or not a feed has a location */
.scanf-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 16px; min-height: 17px; font-size: 12.5px; color: var(--text-soft); }
.scanf-loc, .scanf-time { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap; min-width: 0; }
.scanf-loc { overflow: hidden; }
.scanf-loc span { overflow: hidden; text-overflow: ellipsis; }
.scanf-loc svg, .scanf-time svg { opacity: .65; flex-shrink: 0; }
.scanf-time b { color: var(--text); font-weight: 700; font-variant-numeric: tabular-nums; }

/* the level meter gets its own full-width box: a fuller equalizer look */
.scanf-viz {
  flex: 1; min-height: 64px; display: flex; align-items: stretch;
  padding: 12px 14px; border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--accent) 20%, var(--glass-brd));
  background: color-mix(in srgb, var(--accent) 7%, var(--surface-2));
}
.scan-meter { display: flex; align-items: flex-end; justify-content: space-between; gap: 3px; width: 100%; height: 100%; }
.scan-meter span {
  flex: 1; min-width: 0; height: 10%; border-radius: 3px 3px 1px 1px;
  background: linear-gradient(to top, color-mix(in srgb, var(--accent) 55%, transparent), color-mix(in srgb, var(--accent) 18%, transparent));
  transition: height .12s ease;
}
.scanf.playing .scan-meter span { background: linear-gradient(to top, var(--accent), color-mix(in srgb, var(--accent) 45%, transparent)); }
.scan-meter.js span { transition: height .07s ease; }

/* bottom control bar: play (neutral) + volume + mute + solo */
.scanf-bar { display: flex; align-items: center; gap: 12px; }
.scanf-play {
  width: 46px; height: 46px; border-radius: 13px; flex-shrink: 0;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text);
  display: inline-flex; align-items: center; justify-content: center;
  transition: background .14s, border-color .14s, transform .1s;
}
.scanf-play svg { width: 21px; height: 21px; }
.scanf-play:hover { background: color-mix(in srgb, var(--surface-2) 78%, var(--text)); border-color: var(--text-soft); }
.scanf-play.on { background: color-mix(in srgb, var(--text) 13%, var(--surface-2)); border-color: color-mix(in srgb, var(--text) 32%, var(--border)); }
.scanf-play:active { transform: scale(.94); }
.scanf-bar .scan-range { flex: 1; }
/* mute matches the solo button (bordered .icon-btn.sm); muted = red-filled */
.scanf-mute.on, .scanf-mute.on:hover { background: var(--danger); border-color: var(--danger); color: #fff; box-shadow: 0 0 12px 1px color-mix(in srgb, var(--danger) 55%, transparent); }
.scanf-solo:hover { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); background: var(--accent-soft); }
.scanf-solo.on, .scanf-solo.on:hover { background: var(--accent); border-color: var(--accent); color: #fff; }

/* range sliders (volume + the Settings pin/icon sliders reuse this look) */
.scan-range { -webkit-appearance: none; appearance: none; width: 100%; height: 5px; border-radius: 999px; background: var(--surface-2); cursor: pointer; }
.scan-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--accent); border: 2px solid var(--surface); cursor: pointer; }
.scan-range::-moz-range-thumb { width: 16px; height: 16px; border: 2px solid var(--surface); border-radius: 50%; background: var(--accent); cursor: pointer; }

/* feed form: location row */
.scanf-form .sf-row { display: flex; gap: 12px; }

/* Codes side-panel (reuses .switch-panel chrome) */
.codes-head { align-items: center; }
.codes-head-actions { display: flex; align-items: center; gap: 8px; }
.codes-head-l { display: flex; align-items: center; gap: 8px; }
.codes-body { display: flex; flex-direction: column; gap: 14px; margin-top: 14px; flex: 1; min-height: 0; }
.codes-view-label { font-size: 12px; font-weight: 600; color: var(--text-soft); margin-bottom: -6px; }
.codes-view { flex: 1; min-height: 160px; overflow-y: auto; border: 1px solid var(--glass-brd); border-radius: 12px; background: var(--surface-2); padding: 14px 16px; }
.codes-note { white-space: pre-wrap; font-size: 14px; line-height: 1.7; color: var(--text); }

/* Manage Codes: assignment + collapsible (accordion) code lists that fill the panel height */
.codes-mgr { display: flex; flex-direction: column; gap: 14px; margin-top: 14px; flex: 1; min-height: 0; }
.cm-assign { display: flex; flex-direction: column; gap: 10px; padding: 14px; border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--glass-brd)); border-radius: 14px; background: color-mix(in srgb, var(--accent) 7%, var(--surface-2)); }
.cm-assign label { font-size: 12.5px; color: var(--text-soft); font-weight: 700; }
.cm-assign label b { color: var(--text); }
.cm-lists-head { font-size: 11.5px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; color: var(--text-soft); }
.codes-mgr .cm-add { align-self: flex-start; }
#cm-list { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px; overflow-y: auto; }
.cm-row { display: flex; flex-direction: column; border: 1px solid var(--glass-brd); border-radius: 12px; background: var(--surface-2); overflow: hidden; }
.cm-row.open { flex: 1; min-height: 200px; }
.cm-head { display: flex; align-items: center; gap: 8px; padding: 9px 11px; cursor: pointer; }
.cm-toggle { border: none; background: none; color: var(--text-soft); cursor: pointer; display: inline-flex; padding: 2px; flex-shrink: 0; transition: transform .16s ease, color .14s; }
.cm-toggle:hover { color: var(--text); }
.cm-row.open .cm-toggle { transform: rotate(180deg); }
.cm-head .cm-name { flex: 1; min-width: 0; font-weight: 700; font-size: 14px; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 8px; padding: 7px 10px; font-family: inherit; }
.cm-head .cm-name:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.cm-body { display: none; flex: 1; min-height: 0; padding: 0 11px 11px; }
.cm-row.open .cm-body { display: flex; }
.cm-text { flex: 1; width: 100%; min-height: 120px; resize: none; line-height: 1.6; font-size: 14px; font-family: inherit; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 10px; padding: 11px 13px; }
.cm-text:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
/* Manage Codes assign: feed + list row; combo dark (not white in dark mode) */
.cm-assign-row { display: flex; gap: 10px; }
.cm-assign-cell { flex: 1; min-width: 0; }
.cm-assign .combo-input { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 10px; }

/* floating mini player — draggable + resizable + collapsible, stays on-screen on other tabs while a feed plays */
.scan-mini {
  --accent: #e5484d;   /* match the scanner (red) theme for the meter/solo */
  position: fixed; right: 16px; z-index: 120; width: 288px; max-height: 80vh;
  background: var(--surface); border: 1px solid var(--glass-brd); border-radius: 16px;
  box-shadow: none; padding: 12px 14px;
  display: flex; flex-direction: column; gap: 10px; overflow: hidden;
  animation: popIn .2s cubic-bezier(.34,1.4,.64,1);
}
.scan-mini-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; cursor: move; user-select: none; touch-action: none; flex-shrink: 0; }
.scan-mini-live { display: inline-flex; align-items: center; gap: 8px; font-size: 12.5px; font-weight: 700; color: var(--text-soft); white-space: nowrap; }
.scan-mini-btns { display: flex; align-items: center; gap: 5px; }
.scan-mini-open { display: inline-flex; align-items: center; gap: 5px; padding: 5px 9px; border-radius: 9px; cursor: pointer; font-size: 12px; font-weight: 700; border: 1px solid var(--border); background: var(--surface-2); color: var(--text); white-space: nowrap; transition: background .14s, border-color .14s; }
.scan-mini-open:hover { background: var(--accent-soft); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.scan-mini-list { display: flex; flex-direction: column; gap: 10px; flex: 1; min-height: 0; overflow-y: auto; }
.scan-mini-feed { display: flex; flex-direction: column; gap: 8px; padding: 9px 10px; border: 1px solid var(--glass-brd); border-radius: 11px; background: var(--surface-2); }
.scan-mini-name { font-weight: 700; font-size: 14px; line-height: 1.2; white-space: nowrap; overflow: hidden; }
.scan-mini-ctrl { display: flex; align-items: center; gap: 7px; }
.scan-mini-meter { flex: 1; min-width: 0; height: 28px; display: flex; align-items: stretch; padding: 5px 8px; border-radius: 9px; border: 1px solid color-mix(in srgb, var(--accent) 22%, var(--glass-brd)); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); }
/* collapsed → a slim 30×180 tab on the right screen edge (iOS-style); tap the "‹" to pull it out, drag to move it up/down */
.scan-mini.collapsed {
  width: 30px !important; height: 180px !important; max-height: none; padding: 0; gap: 0;
  right: 0 !important; left: auto !important; bottom: auto !important;
  border-radius: 13px 0 0 13px; border-right: none;
  background: linear-gradient(90deg, color-mix(in srgb, var(--accent) 12%, var(--surface)), var(--surface));
  box-shadow: none;
}
.scan-mini-tab { position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; background: none; border: none; color: var(--text-soft); cursor: pointer; transition: color .14s; touch-action: none; }   /* "‹" centred, dot + speaker above it (.edge-ind) */
.scan-mini-tab:hover { color: var(--accent); }
.scan-mini-tab svg { width: 20px; height: 20px; }

/* ===== Notes: floating round button + right-edge panel ===== */
.notes-fab {
  /* above the band under the rail, not partly behind it */
  position: fixed; bottom: calc(var(--rail-gap) + 16px); right: 24px; z-index: 66;
  width: 54px; height: 54px; border-radius: 50%;
  background: color-mix(in srgb, var(--fab-bg) 16%, var(--surface)); color: var(--fab-fg);
  border: 1px solid color-mix(in srgb, var(--fab-bg) 55%, transparent); cursor: pointer;
  display: flex; align-items: center; justify-content: center;
  box-shadow: none;
  transition: transform .14s ease, background .14s ease, color .14s ease, border-color .14s ease;
}
/* no shadow, on hover either — the button carries its own outline and fill, and
   the drop shadow was the only one left on a floating control in the app */
.notes-fab:hover { transform: translateY(-2px); background: var(--fab-bg); color: #fff; border-color: var(--fab-bg); box-shadow: none; }
/* blocked while the Change panel holds the right edge — mirrors .btn:disabled (pointer-events:none
   so the :hover fill above can't win over it) */
.notes-fab:disabled { opacity: .38; pointer-events: none; }
/* the year pickers switch themselves off when the data holds only one year —
   same fade as every other disabled control, so it reads as "nothing to pick" */
.icon-btn:disabled, .map-ctrl-btn:disabled { opacity: .38; pointer-events: none; }
/* the panel floats on the right edge like the Change panel (fixed → same in every tab) */
.switch-panel.notes-panel { position: fixed; top: 14px; right: 14px; height: calc(100vh - 28px); margin: 0; z-index: 60; }
/* 250px (Rick, 2026-09-12) — the New Note screen is a screen of its own now, so the field can be
   the size a note is actually written in rather than the three lines it had while it sat in a box
   at the top of the list. Still draggable taller. */
.notes-text { height: 250px; min-height: 250px; resize: vertical; line-height: 1.5; }
/* clear separation: the New-Note form is a raised card with an accent edge; saved notes sit under a bold divider */
.notes-create-box { display: flex; flex-direction: column; gap: 8px; background: var(--surface); border: 1px solid var(--panel-div); border-left: 3px solid var(--accent); border-radius: 12px; padding: 11px 13px; box-shadow: none; }
/* Save shares a row with the picture field instead of costing a line of its own —
   the form is the smaller half of this panel and the saved notes are the point. */
.notes-create-foot { display: flex; align-items: flex-end; gap: 10px; }
.notes-create-foot .field { flex: 1; min-width: 0; }
.notes-create-foot .btn { flex-shrink: 0; }
.notes-section-label { display: flex; align-items: center; gap: 7px; font-size: 11px; font-weight: 800; letter-spacing: .8px; text-transform: uppercase; color: var(--text-soft); }
.notes-saved-head { display: flex; align-items: center; gap: 9px; margin: 20px 0 10px; padding-top: 16px; border-top: 2px solid var(--panel-div); }
/* The New Note form is its own screen now (the + in the head), so on the LIST the saved notes are
   the first thing under the heading — the bold divider that used to separate them from the form
   above has nothing left to separate. */
.switch-head + .notes-saved-head { margin-top: 12px; padding-top: 0; border-top: 0; }
/* …and on that screen the form is the whole body, so it drops the raised-card look it needed while
   it stood at the top of the list. */
.note-open.notes-create-box { background: transparent; border: 0; border-radius: 0; padding: 0; }
/* dragging the notes into order: the dragged card fades, and a glowing bar shows where it lands.
   The list is a column, so the marker lies across the card rather than beside it. */
.note-card.dragging { opacity: .5; }
.note-card.drop-before::after, .note-card.drop-after::after {
  content: ''; position: absolute; left: 6%; right: 6%; height: 4px; border-radius: 4px;
  background: var(--accent); box-shadow: 0 0 10px 1px color-mix(in srgb, var(--accent) 70%, transparent);
}
.note-card.drop-before::after { top: -7px; }
.note-card.drop-after::after { bottom: -7px; }
.notes-saved-count { font-size: 11px; font-weight: 800; color: var(--text-soft); background: var(--surface-2); border: 1px solid var(--panel-div); border-radius: 999px; padding: 1px 8px; }
.notes-list { display: flex; flex-direction: column; gap: 9px; overflow-y: auto; flex: 1; min-height: 0; }
.notes-empty { text-align: center; color: var(--text-soft); padding: 18px 0; }
.note-card { position: relative; background: var(--surface); border: 1px solid var(--panel-div); border-radius: 12px; padding: 11px 42px 11px 14px; cursor: pointer; transition: border-color .12s, background .12s; }
.note-card:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--panel-div)); background: var(--surface-2); }
.note-del { position: absolute; top: 7px; right: 7px; width: 26px; height: 26px; opacity: 0; transition: opacity .12s; }
.note-card:hover .note-del { opacity: 1; }
.note-date { font-size: 11px; font-weight: 700; color: var(--text-soft); letter-spacing: .3px; margin-bottom: 4px; }
.note-title { font-size: 14.5px; font-weight: 800; color: var(--text); margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.note-preview { font-size: 13px; color: var(--text-soft); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* opened note: editable title + text (auto-save), heading is "Notes", no date/pic meta */
.notes-head-l { display: flex; align-items: center; gap: 8px; min-width: 0; }
.notes-head-l strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.notes-head-r { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.note-open { display: flex; flex-direction: column; gap: 12px; overflow-y: auto; flex: 1; }
.note-open-title { background: transparent; border: none; padding: 2px 0; font-size: 21px; font-weight: 800; color: var(--text); outline: none; }
.note-open-title::placeholder { color: var(--text-soft); opacity: .55; }
.note-open-text { flex: 1; min-height: 180px; background: var(--surface); border: 1px solid var(--panel-div); border-radius: 12px; padding: 13px 15px; font-size: 14.5px; line-height: 1.6; color: var(--text); font-family: inherit; resize: vertical; outline: none; }
.note-open-text:focus { border-color: color-mix(in srgb, var(--accent) 45%, var(--panel-div)); }

/* empty state */
.empty { text-align: center; padding: 50px 20px; color: var(--text-soft); }

/* global button tooltip (matches the sidebar-toggle pill) */
.tip-pop {
  position: fixed; z-index: 410; transform: translate(-50%, -100%);
  background: #0d1f3a; color: #eaf1fb; border: 1px solid rgba(255,255,255,.16);
  padding: 5px 9px; border-radius: 7px; font-size: 12px; font-weight: 500; white-space: nowrap;
  opacity: 0; pointer-events: none;
  transition: opacity .18s ease, transform .18s ease;
  box-shadow: none;
}
.tip-pop.show { opacity: 1; }
.tip-pop.left { transform: translate(-100%, -50%); }
/* pops out to the RIGHT of its anchor — the rail's tab names on a phone, where
   there is no hover and the CSS tooltip inside the rail would be clipped. It
   fades AND slides out of the icon, the same move the wide rail's own tooltip
   makes, so it reads as coming from the button rather than blinking into place. */
.tip-pop.right { transform: translate(-5px, -50%); }
.tip-pop.right.show { transform: translate(0, -50%); }

/* ===== Switch panel (drag & drop, beside the dashboard) ===== */
.switch-panel {
  flex-shrink: 0; width: 450px; max-width: calc(100vw - 28px);
  position: sticky; top: 14px; align-self: flex-start;
  height: calc(100vh - 28px); overflow-y: auto;
  margin: 14px 14px 14px 4px;
  background: var(--panel-bg);   /* matches the sidebar in dark; its own light surface in light */
  border: 1px solid var(--panel-brd); border-radius: 24px;
  box-shadow: none;
  padding: 18px 18px; z-index: 40;
  display: flex; flex-direction: column;
  animation: popIn .2s cubic-bezier(.34,1.4,.64,1);
}
.switch-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.switch-head strong { font-size: 20px; font-weight: 800; letter-spacing: -.3px; }
.switch-head-actions { display: flex; align-items: center; gap: 8px; }   /* Manage Ranks (Rank tab) + close ✕ */
/* full-panel drop zone shown until a card is dropped */
.sw-dropzone {
  flex: 1; min-height: 160px; margin-top: 12px; border: 2px dashed var(--border); border-radius: 20px;
  background: color-mix(in srgb, var(--surface-2) 40%, transparent);
  display: flex; align-items: center; justify-content: center; text-align: center;
  color: var(--text-soft); transition: border-color .12s, background .12s, color .12s;
}
.sw-dropzone.drag {
  border-color: var(--accent); background: var(--accent-soft); color: var(--accent);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent);
}
.sw-dz-inner { display: flex; flex-direction: column; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; }
.sw-dz-inner svg { transform: rotate(90deg); opacity: .8; }

/* 🗑 The panel's old front door (.sw-panepick / .sw-panebtn) and the old Last seen half
   (.ls-card / .ls-datefield / .ls-as* / .ls-tglbtn / .ls-seenfield / .ls-seenpick) are gone with
   the 2026-09-06 rebuild — one screen, no choice screen, no second half. Removed 2026-09-13. */

/* the panel's own copy of the sighting list: same rows as the Details window,
   read-only, and it scrolls rather than pushing the buttons above it away */
.ls-list { max-height: 240px; overflow-y: auto; }
.sw-slots { display: flex; align-items: stretch; gap: 8px; margin-top: 8px; }
/* the slot cards are scaled down a touch so two (Call Sign A → B) fit evenly in the 450px panel */
/* The card in a slot is deliberately ONE fixed size on every screen — the phone
   block shrinks the dashboard's cards but must not touch these, or the same
   trooper would be a different size in the Change panel depending on the window.
   0.92 of the 195×270 card = 179×248, a little wider than the 0.84 it was. */
.sw-slot .sw-card { zoom: 0.92; }
.sw-slot {
  flex: 1; min-height: 96px; border: 2px dashed var(--border); border-radius: 18px;
  background: color-mix(in srgb, var(--surface-2) 40%, transparent);
  display: flex; align-items: center; justify-content: center; padding: 12px;
  transition: border-color .12s, background .12s, box-shadow .12s;
}
/* a filled slot is just a frame for the real dashboard card — no extra chrome (the card carries its own colour) */
.sw-slot.filled { border-color: transparent; background: transparent; padding: 6px; }
.sw-slot.drag { border-color: var(--accent); background: var(--accent-soft); }
/* the three-way call-sign picker on Add Trooper: it names three kinds, it is not
   a control bar — so it takes the width of its labels and stops there */
.cs-mode-seg { align-self: flex-start; }
.cs-mode-seg .seg-btn { padding: 7px 11px; font-size: 12.5px; }
/* Slot B takes a card, a typed unit number, or nothing at all. "Only Unit Number" stands ABOVE
   the slot and is a TOGGLE: on, the whole slot is the field (an empty one = a unit trooper whose
   number is not known yet); off, the drop zone is back. Inside the empty drop zone sits the other
   Unknown — no call sign at all, on a card that stays blue. */
.sw-unit-btn {
  margin-top: 8px; border: 1px dashed var(--border); background: transparent;
  color: var(--text-soft); font-family: inherit; font-size: 11.5px; font-weight: 600;
  padding: 5px 10px; border-radius: 999px; cursor: pointer;
  transition: color .12s, border-color .12s, background .12s;
}
/* orange on hover, not blue: what this button produces is a unit card, and a unit card is orange
   (.tcard.unit / .t-cs-label.unit) — the colour says which kind of call sign is being made */
.sw-unit-btn:hover { color: #f59e0b; border-color: #f59e0b; background: rgba(245,158,11,.14); }
.sw-unit-btn.on {
  border-style: solid; border-color: #f59e0b; color: #f59e0b;
  background: color-mix(in srgb, #f59e0b 14%, transparent);
}
/* the toggle above the slot: its own row on the LEFT, with room to breathe between it and the slot
   (Rick, 2026-09-05 — centred and tight against the slot it read as part of the drop zone) */
.sw-unitmode-row { display: flex; justify-content: flex-start; margin-bottom: 10px; }
.sw-unitmode-row .sw-unit-btn { margin-top: 0; }
/* …and the plain Unknown wears NO orange: it makes an ordinary blue trooper card, not a unit one */
.sw-unknown-btn:hover { color: var(--text); border-color: var(--text-soft); background: transparent; }
.sw-unknown-btn.on {
  border-style: solid; border-color: var(--accent); color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
}
.sw-unit-wrap { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px; }
/* the box itself is copied from `.field input` — this one does not stand in a
   .field, so it would otherwise render as a bare browser text box */
.sw-unit-input {
  width: 100%; padding: 7px 13px; font-size: 13px; text-align: center;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text); font-family: inherit;
  transition: border-color .16s ease;
}
.sw-unit-input:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
/* the empty Call Sign slot, pressed: the call signs that exist, typable. Same shape as the unit
   field above it — the slot IS the field while it is open — with its two ways out underneath. */
.sw-cspick { position: relative; width: 100%; display: flex; flex-direction: column; align-items: center; gap: 8px; }
.sw-cspick .combo { width: 100%; }
/* The field reads like the call sign it is about to hold: centred, bold, spaced, and TYPED IN
   CAPITALS whichever way it was typed (Rick, 2026-09-12 — "a-12" and "A-12" are the same sign).
   The placeholder stays in its own case, or it would shout. */
.sw-cspick .combo-input {
  text-align: center; text-transform: uppercase; font-weight: 700; font-size: 14px;
  letter-spacing: .6px; padding: 9px 30px; height: auto; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border);
  color: var(--text);   /* what is typed is the plain text colour, not the muted one (Rick) */
  transition: border-color .14s, box-shadow .14s, background .14s;
}
.sw-cspick .combo-input::placeholder { text-transform: none; font-weight: 600; letter-spacing: 0; color: var(--text-soft); }
.sw-cspick .combo-input:hover { border-color: color-mix(in srgb, var(--accent) 40%, var(--border)); }
.sw-cspick .combo-input:focus {
  outline: none; background: var(--surface);
  border-color: color-mix(in srgb, var(--accent) 65%, var(--border));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
/* the list belongs to the field: same rounding, a little more air per row */
.sw-cspick .combo-list { border-radius: 12px; padding: 5px; }
.sw-cspick .combo-opt { border-radius: 9px; padding: 7px 10px; }
.sw-cspick .combo-opt-lbl { width: 100%; }
/* on the drop-in screen the field stands alone above the drop zone, not inside a slot */
.sw-cspick-screen { margin: 10px 0 12px; }
/* …and its list is FLAT there (Rick, 2026-09-12). That window keeps its own small size — the
   dashboard behind it is where the card comes from — so the list is what gives way; hanging out of
   the bottom edge it read as part of the page underneath. !important because the dropdown places
   itself against the screen and writes its own height into the element. */
.sw-cspick-screen .combo-list { max-height: 150px !important; }
/* 🔴 THE WINDOW IS THE SAME SIZE WITH THE FIELD OPEN AS WITHOUT IT (Rick, 2026-09-12). The field is
   laid OVER the drop zone rather than above it, and the drop zone keeps its space while it is
   hidden — so pressing the slot opens a field and moves nothing. */
.sw-pickbody { position: relative; }
.sw-pickbody.picking .sw-cspick-screen { position: absolute; left: 0; right: 0; top: 0; margin: 0; z-index: 2; }
.sw-pickbody.picking .sw-dropzone { visibility: hidden; }
.sw-cspick-foot { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
.sw-cspick-foot .sw-unit-btn { margin-top: 0; }
/* one option: the sign, and the name right BESIDE it (Rick, 2026-09-12 — pushed to the far right
   the two read as two columns with a hole between them). A FREE sign says nothing at all. */
.cs-opt { display: flex; align-items: baseline; gap: 10px; width: 100%; }
.cs-opt b { font-weight: 800; letter-spacing: .4px; }
.cs-opt-who { color: var(--text-soft); font-size: 11.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sw-drop { text-align: center; font-size: 12px; line-height: 1.5; display: flex; flex-direction: column; align-items: center; }
.sw-card { position: relative; width: max-content; margin: 0 auto; transition: transform .14s; }
body.switch-active .sw-card .tcard { cursor: grab; }
/* the hover-lift moves to the wrapper so the ✕ rides along with the card (the card itself keeps its glow/border) */
.sw-slot .tcard { transform: none !important; }
.sw-card:hover { transform: translateY(-3px); }
/* drag image when pulling a card out of a slot = the real card, just parked off-screen */
.sw-card-ghost { position: fixed; top: -1000px; left: -1000px; pointer-events: none; }
.sw-card-rm {
  position: absolute; top: 9px; right: 9px; z-index: 3;
  width: 22px; height: 22px; border-radius: 50%; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-soft); cursor: pointer; font-size: 12px; line-height: 1;
  display: flex; align-items: center; justify-content: center; box-shadow: none;
}
.sw-card-rm:hover { color: #fff; background: var(--danger); border-color: var(--danger); }
/* removing K9 status → the trooper card in the slot turns red */
.sw-slot.k9held .tcard { border-color: var(--danger) !important; box-shadow: 0 10px 30px -14px rgba(217,52,43,.45); }
.sw-slot.k9held .t-av { color: var(--danger); background: rgba(217,52,43,.12); box-shadow: 0 6px 22px -6px rgba(217,52,43,.5), 0 0 0 5px rgba(217,52,43,.14); }
/* hover in a k9held slot stays red — a K9-Dog card in the slot would otherwise pick up the purple
   .k9card:hover glow (higher specificity), showing red + purple at once */
.sw-slot.k9held .tcard:hover { border-color: var(--danger) !important; box-shadow: 0 12px 32px -14px rgba(217,52,43,.5) !important; }
.sw-slot.k9held .tcard:hover .t-av { box-shadow: 0 9px 28px -6px rgba(217,52,43,.7), 0 0 0 6px rgba(217,52,43,.22) !important; }
/* !important: beats the dark-theme .t-cs-label.k9 override (html[data-theme="dark"] adds specificity
   that would otherwise leave the "K9-Dog" pill text purple instead of red here) */
.sw-slot.k9held .t-cs-label { background: rgba(217,52,43,.16) !important; color: var(--danger) !important; }
.sw-summary { margin-top: 12px; padding: 12px 14px; background: var(--surface-2); border-radius: 10px; }
.sw-sum-row { display: flex; align-items: center; gap: 8px; }
/* overview pills use the SAME tinted backgrounds as the call-sign labels on the trooper cards */
.cs-pill { display: inline-flex; align-items: center; padding: 3px 11px; border-radius: 999px; font-weight: 800; letter-spacing: .5px; background: color-mix(in srgb, var(--tc-blue) 15%, transparent); color: var(--tc-blue); border: 2px solid color-mix(in srgb, currentColor 42%, transparent); }
.cs-pill.hot.red { background: rgba(217,52,43,.16); color: var(--danger); }   /* employment / rehire preview */
.cs-pill.red { background: rgba(217,52,43,.16); color: var(--danger); }        /* current former-status label in the rehire overview */
/* overview pills are colour-matched to their tab: Call Sign/Rank/K9 blue (the default), ICP
   turquoise, and the K9 dog name purple — backgrounds match the card labels' tints */
.cs-pill.teal { background: rgba(20,184,166,.18); color: var(--icp-teal); }
.cs-pill.purple { background: rgba(137,87,229,.18); color: var(--k9-purple); }
/* dark-mode overview pills use fixed tinted backgrounds (per label colour) */
html[data-theme="dark"] .cs-pill { background: #212c42; }
html[data-theme="dark"] .cs-pill.teal { background: #183639; }
html[data-theme="dark"] .cs-pill.purple { background: #2d2544; }
html[data-theme="dark"] .cs-pill.red,
html[data-theme="dark"] .cs-pill.hot.red { background: #381e23; }

/* Rank / ICP tabs: a scrollable list of small draggable "pick" cards, replacing the old dropdown —
   drag one into the right-hand slot (Rank) or the ICP slot (Unit Number) to assign it */
.sw-picklist-wrap { margin-top: 18px; }
.sw-picklist-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
.sw-picklist-label { font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .6px; color: var(--text-soft); }
/* padding-top (offset by a matching negative margin) gives the first card's hover-lift room so it
   isn't clipped by the scroll box's own edge */
.sw-picklist { display: flex; flex-direction: column; gap: 8px; max-height: 315px; overflow-y: auto; padding: 10px 4px 4px; margin-top: -10px; }
/* the instruction line that sits directly under the slots (Rank / ICP / Employment) — left-aligned like Call Sign's */
.sw-slot-hint { margin-top: 10px; }
.sw-pickcard {
  display: flex; align-items: center; justify-content: center;
  padding: 10px 14px; border-radius: 12px; cursor: grab;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text);
  font-weight: 700; font-size: 16px; letter-spacing: .2px;
  transition: border-color .14s, transform .14s, box-shadow .14s, opacity .14s;
}
.sw-pickcard:hover { transform: translateY(-2px); }
/* the row being reordered: flattened + dimmed, so the drag image reads as the "real" one */
.sw-pickcard.dragging { opacity: .35; transform: none; box-shadow: none; }
.sw-pickcard:active { cursor: grabbing; }
/* a taken ICP unit — greys out until it's freed up again */
.sw-pickcard.used { opacity: .4; cursor: not-allowed; }
.sw-pickcard.used:hover { transform: none; box-shadow: none; }
/* a taken unit is greyed in edit mode too, but its rename/delete controls stay usable */
.sw-pickcard.sw-pickcard-edit.used { cursor: default; }
.sw-pickcard-label { pointer-events: none; }
/* Every list is colour-coded by what it assigns — Rank = orange, Employment = red, ICP = turquoise.
   All three read like a mini version of the card in the slot: the trooper card's background, a 2px
   border, and the list's own colour on both the border and the text, plus a matching hover glow. */
.sw-pickcard[data-rank] {
  background: var(--surface); border: 2px solid var(--tc-blue); color: var(--tc-blue);
}
.sw-pickcard[data-rank]:hover {
  box-shadow: 0 0 15px 1px color-mix(in srgb, var(--tc-blue) 50%, transparent);
}
.sw-pickcard[data-emp] {
  background: var(--surface); border: 2px solid var(--danger); color: var(--danger);
}
.sw-pickcard[data-emp]:hover {
  box-shadow: 0 0 15px 1px color-mix(in srgb, var(--danger) 50%, transparent);
}
.sw-pickcard[data-icpunit] {
  background: var(--surface); border: 2px solid var(--icp-teal); color: var(--icp-teal);
}
.sw-pickcard[data-icpunit]:hover {
  box-shadow: 0 0 15px 1px color-mix(in srgb, var(--icp-teal) 50%, transparent);
}

/* "Add Rank" / "New Unit" row — a dashed tray that reads as the list's own footer */
.sw-pickadd {
  display: flex; gap: 8px; margin-top: 10px; padding: 8px;
  border: 1px dashed color-mix(in srgb, var(--border) 85%, var(--text-soft));
  border-radius: 14px; background: color-mix(in srgb, var(--surface-2) 45%, transparent);
}
.sw-pickadd input {
  flex: 1; min-width: 0; padding: 8px 12px; border-radius: 10px;
  background: var(--surface); border: 1px solid var(--border); color: var(--text);
  font: inherit; font-size: 13.5px; font-weight: 600;
}
.sw-pickadd input:focus { outline: none; border-color: var(--accent); }
.sw-pickadd .btn { flex-shrink: 0; padding: 8px 16px; border-radius: 10px; }

/* edit toggle (header of the pick-list) — pencil to enter edit mode; while editing it's a close "x"
   that goes red on hover (like the app's other close/delete controls) */
.sw-sort-toggle.on { color: var(--text-soft); }
.sw-sort-toggle.on:hover { color: #fff; background: var(--danger); border-color: var(--danger); }
/* unlocked ("sort mode") pick-card: grip handle + editable name + delete, instead of the plain centred label */
.sw-pickcard.sw-pickcard-edit { justify-content: flex-start; gap: 9px; cursor: default; padding: 7px 8px 7px 10px; }
.sw-pickcard.sw-pickcard-edit:hover { transform: none; box-shadow: none; }
.sw-pick-grip { cursor: grab; color: var(--text-soft); display: inline-flex; flex-shrink: 0; }
.sw-pick-grip:hover { color: var(--text); }
.sw-pick-grip:active { cursor: grabbing; }
.sw-pickcard-input {
  flex: 1; min-width: 0; background: transparent; border: none; color: inherit;
  font: inherit; font-weight: 700; padding: 4px 2px; border-radius: 6px;
}
.sw-pickcard-input:focus { outline: none; background: var(--surface); box-shadow: 0 0 0 1px var(--accent); }
.sw-pickcard-flex { flex: 1; text-align: left; }
.sw-pick-del { flex-shrink: 0; }
.switch-head { border-bottom: 1px solid var(--border); padding-bottom: 12px; }
/* ===== The Change panel's accordion =====
   The six tabs became six fields standing under each other, each drawn like the Last seen /
   Changes buttons on the screen before it (.sw-panebtn) so the two screens read as one place.
   Only one opens at a time; a field that carries a change keeps a tick and an accent edge, which
   is what tells you at a glance what the Save at the foot is about to write. */
.sw-acc { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; align-self: stretch; }
.sw-accitem {
  border: 2px solid var(--glass-brd); border-radius: 16px; background: var(--surface-2);
  overflow: hidden; transition: border-color .14s, background .14s;
}
/* Each field carries the colour of the kind of value it holds — the same colours the cards and
   pills use, so the field, the pill inside it and the card it produces all agree:
   blue call sign, yellow rank (the History label's yellow), purple K9, teal ICP, red for an end. */
.sw-accitem { --tone: var(--accent); }
.sw-accitem.tone-rank { --tone: var(--note-corner); }
.sw-accitem.tone-k9   { --tone: #8957e5; }
.sw-accitem.tone-icp  { --tone: var(--icp-teal); }
.sw-accitem.tone-red  { --tone: var(--danger); }
.sw-accitem.set { border-color: color-mix(in srgb, var(--tone) 55%, var(--glass-brd)); }
.sw-accitem.open { border-color: var(--tone); background: var(--surface); }
.sw-accitem.open .sw-accico, .sw-accitem.set .sw-accico { color: var(--tone); opacity: 1; }
.sw-acchead:hover { background: color-mix(in srgb, var(--tone) 10%, transparent); }
/* the head: icon, two lines of text, then the marks — same grid as .sw-panebtn, one row shorter */
.sw-acchead {
  display: grid; grid-template-columns: 26px 1fr auto auto; align-items: center; gap: 2px 12px;
  width: 100%; text-align: left; cursor: pointer; padding: 11px 14px;
  background: none; border: 0; color: var(--text); font-family: inherit;
}
.sw-accico { display: flex; align-items: center; justify-content: center; opacity: .85; }
.sw-acctext { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.sw-acctext b { font-size: 14.5px; font-weight: 800; }
.sw-acctext small { color: var(--text-soft); font-size: 11.5px; font-weight: 600; }
.sw-accmark { display: flex; color: var(--tone); }
.sw-accchev { display: flex; color: var(--text-soft); transition: transform .16s; }
.sw-accitem.open .sw-accchev { transform: rotate(180deg); }
.sw-accbody { padding: 2px 14px 14px; display: flex; flex-direction: column; }
/* inside an open field the slot no longer has a partner beside it — it stands on its own */
.sw-accbody .sw-slot { align-self: stretch; }
/* the Assign / Mark as buttons stand clear of the slot under them */
.sw-accbody .sw-secbtn { margin-bottom: 12px; }
/* every option is visible: the list IS the control, so nothing may sit out of reach in it */
.sw-accbody .sw-picklist { max-height: none; overflow: visible; }
/* …and the options are a touch narrower than the field, so the list reads as a list of choices */
.sw-accbody .sw-pickcard { padding-left: 10px; padding-right: 10px; }
.sw-accbody .sw-picklist { padding-left: 6px; padding-right: 6px; }
/* the chosen option wears the field's own colour — except Rank, whose cards are blue like the
   trooper cards they set, and where yellow-on-yellow read as a warning rather than a choice */
.sw-accbody .sw-pickcard.on {
  border-color: var(--tone); color: var(--tone);
  background: color-mix(in srgb, var(--tone) 14%, transparent);
}
/* Rank reads the other way round (2026-09-05, Rick's call): every rank is greyed out and the ONE
   he picked is the field's own yellow. The list is long — a dozen ranks — and the chosen one had
   to be hunted for among eleven equally bright blue cards; dimming the rest makes it the only
   thing on the field with any colour in it. Edit-mode rows keep their colour: that is where a
   rank is renamed, and a greyed-out text field is not a field you type in. */
.sw-accitem.tone-rank .sw-pickcard[data-rank]:not(.on):not(.sw-pickcard-edit) {
  border-color: var(--border); color: var(--text-soft); opacity: .55;
}
.sw-accitem.tone-rank .sw-pickcard[data-rank]:not(.on):not(.sw-pickcard-edit):hover {
  opacity: 1; box-shadow: none;
}
/* …and the chosen one glows in the field's own yellow. The blue glow came from the base
   `.sw-pickcard[data-rank]:hover` rule, whose colour is the trooper-card blue. */
.sw-accitem.tone-rank .sw-pickcard[data-rank].on,
.sw-accitem.tone-rank .sw-pickcard[data-rank].on:hover {
  box-shadow: 0 0 15px 1px color-mix(in srgb, var(--tone) 55%, transparent);
}
/* the options are a row of choices, not a stack of cards — shorter than they were */
.sw-accbody .sw-pickcard { min-height: 0; padding-top: 6px; padding-bottom: 6px; }
/* "Killed" is one Employment Status option, so it is drawn as one — same red card, same glow */
.sw-killedlist { margin-bottom: 4px; }
.sw-killedlist .sw-pickcard {
  background: var(--surface); border: 2px solid var(--danger); color: var(--danger);
}
.sw-killedlist .sw-pickcard:hover {
  box-shadow: 0 0 15px 1px color-mix(in srgb, var(--danger) 50%, transparent);
}
/* the yes/no fields (K9 status, ICP, Shot & Killed) have nothing to drop — the field is the switch */
.sw-secbtn {
  align-self: flex-start; padding: 9px 14px; border-radius: 12px; cursor: pointer;
  background: var(--surface-2); border: 2px solid var(--glass-brd); color: var(--text);
  font-family: inherit; font-size: 13px; font-weight: 700; transition: border-color .14s, background .14s, color .14s;
}
.sw-secbtn:hover { border-color: var(--tone, var(--accent)); }
/* …except the one that TAKES something away: "Remove from K9" is red under the hand as well as
   pressed, because red is what it does (Rick, 2026-09-13). The field's own purple said the
   opposite of the button standing in it. */
.sw-secbtn.danger:hover { border-color: var(--danger); }
/* pressed, it wears the FIELD's own colour rather than the app accent — "Assign to K9" comes out
   purple like everything else about K9, "Assign to ICP" turquoise (Rick, 2026-09-05) */
.sw-secbtn.on {
  border-color: var(--tone, var(--accent)); color: var(--tone, var(--accent));
  background: color-mix(in srgb, var(--tone, var(--accent)) 16%, transparent);
}
.sw-secbtn.danger.on { border-color: var(--danger); background: rgba(217, 52, 43, .14); color: var(--danger); }
/* The head of the panel: the card as it stands, an arrow, and the card as the Save would leave it.
   A GRID, not a flex row — the two slots have different content (one carries a ✕, one does not)
   and as flex items they came out different widths, which left the pair sitting 8px off centre. */

/* ===== Trooper Sighting: ONE card at the top, not a before-and-after pair =====
   (Rick, 2026-09-06 — see the note on .sw-slots in troopers.js.) The card is centred on its own
   rather than filling the panel: at full width it read as a banner, and it is a card. It still
   takes a drop (another card swaps him in) and still carries its ✕. */
.sw-one { justify-content: center; }
.sw-one .sw-slot { flex: 0 1 auto; min-width: 0; }
.sw-one .sw-slot:not(.filled) { flex: 1 1 auto; }   /* empty, it is a drop zone and wants the width */

/* Where he was seen, and the day — one row, the Seen field on the left. They are one sentence,
   and the day is what the whole screen turns on. */
/* Seen and its Link share one row; the day stands UNDER them (Rick, 2026-09-12), where it reads as
   the line the two above it belong to rather than as a third thing beside them. */
.sw-whenrow { display: flex; align-items: flex-end; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.sw-whenrow .field { margin: 0; }
/* Seen keeps ONE width whether or not the Link field is standing beside it (Rick, 2026-09-12) —
   half the row, minus half the gap between them. Answering it used to shrink it to make room, so
   the field moved under the hand that had just used it. */
.sw-seenfield { flex: 0 0 calc(50% - 5px); min-width: 0; }
.sw-seenfield .combo { width: 100%; }
/* the address only exists once Seen has an answer, and it takes the rest of the row */
.sw-linkfield { flex: 1 1 150px; min-width: 0; }
.sw-linkfield .sm-input { width: 100%; }
.sw-datefield { width: 150px; flex: 0 0 auto; margin-top: 10px; }

/* The two lists under the Save. Same switch Trooper Details uses, because it is the same pair of
   lists — sightings, and the changes they wrote. */
.sw-histtabs { margin: 16px 0 0; width: 100%; }
.sw-histtabs .seg-btn { flex: 1 1 0; }
.sw-histtabs + .history-list { margin-top: 10px; max-height: 260px; overflow-y: auto; }
/* The panel is a flex COLUMN, so every child shrinks by default once the content
   is taller than the panel — which on a phone squashed the tab row down to a bare
   line as soon as a tab with a pick-list was chosen. The panel scrolls; its
   fixed-height furniture must not shrink.
   Change panel only: the Notes panel WANTS its saved-note list to take the
   leftover height on a desktop, and gets its own rule in the phone block. */
.switch-panel:not(.notes-panel) > * { flex-shrink: 0; }
/* FIRED (and other danger) buttons glow on hover */
.btn.danger:hover { box-shadow: 0 0 0 1px var(--danger), 0 6px 20px -4px rgba(217,52,43,.6); }

/* ===== Responsive ===== */
@media (max-width: 820px) {
  main { padding: 20px 16px; }
  /* auto-collapse the rail to icons on small screens */
  .sidebar { width: 76px; padding: 16px 12px; }
  .sidebar .brand-text, .sidebar .nav-label { display: none; }
  /* stack the toggle above a centered logo, same as the collapsed rail */
  .sidebar .side-head { flex-direction: column-reverse; align-items: center; gap: 12px; padding: 0 0 8px; }
  .sidebar .side-brand { justify-content: center; flex: initial; padding: 0; }
  .sidebar .nav-btn { justify-content: center; gap: 0; padding: 11px 0; }
  .sidebar .nav-btn.active::after { display: none; }
  .sidebar .side-foot { flex-direction: column; align-items: center; }
  .stats-grid { grid-template-columns: 1fr; }
  .stat-kpis { grid-template-columns: repeat(2, 1fr); }
}

/* ===== Phones =====
   The desktop layout is a fixed 288px rail beside a content column with wide
   gutters, and several views place things at absolute pixel offsets. None of
   that survives a 375px screen, so below 700px the rail becomes a slim icon
   strip, every gutter drops to 10px, the fixed grids wrap, and the windows and
   right-edge panels take the whole screen.
   Everything here is layout only — no colours, no behaviour.

   The second condition catches the same phones turned sideways: wide enough to
   miss the 700px cap, but only ~400px tall, where a 288px rail and 40px gutters
   are just as wrong. The 950px cap keeps tablets and short desktop windows out
   of it. */
@media (max-width: 700px),
       (max-height: 500px) and (max-width: 950px) and (orientation: landscape) {
  /* the rail sits further off the edge here than on a desktop — and the strip
     under it (.under-rail) grows with it, because it reads the same token */
  :root { --rail-gap: 18px; }
  /* --- the rail ---
     Icons only, whatever the class list says: the "collapsed" rules are tied to
     a class the user can toggle and to the 820px query, and neither is reliable
     on a phone held sideways (wider than 820, still no room for labels). */
  /* `.sidebar.collapsed` (0,2,0) outranks a plain `.sidebar` (0,1,0), so opening
     a right-edge panel — which adds that class — made the rail jump to its
     104px collapsed width. Both selectors, one size: on a phone the rail is
     only ever the icon strip. */
  .sidebar, .sidebar.collapsed {
    width: 56px; padding: 8px 6px; gap: 2px;
    /* The gap above and below the rail is the same 18px — it used to be a 8px
       `top` plus an 8px margin at the top and nothing at all at the bottom, so
       the rail sat against the bottom edge. Everything else on the page lines up
       with this number (see .view-head's card below). */
    margin: 0 3px 0 8px; border-radius: 18px;
    /* FIXED, and sized in dvh — the two belong together.
       dvh is the height right now, so the rail grows and shrinks exactly with
       the screen as iOS slides its address bar away and back. That is what it
       has to do to keep reaching the bottom edge.
       The catch, when it was `sticky`, was that a sticky box is IN the flow: its
       height counted towards the document's, so a dvh rail measured with the bar
       collapsed left ~16px of scrollable overhang once the bar came back. svh
       fixed that but stopped it growing at all, which is the wrong trade.
       `fixed` takes it out of the flow entirely — it can then be exactly as tall
       as the screen is at this moment and still add nothing to scroll. `main`
       carries the 67px the rail no longer occupies (8 margin + 56 + 3). */
    /* safe area: on a home-screen app the viewport runs under the status bar and
       the home indicator, so the rail keeps its 18px gap to what is USABLE
       rather than to the edge of the screen. Both insets are 0 in a browser tab,
       where this is simply the gap. */
    /* left, too: held sideways the notch eats a strip off one of the long edges,
       and with viewport-fit=cover the page runs straight under it. The rail is
       the leftmost thing there is, so it is the one that ends up behind it. The
       inset is 0 upright and 0 in a browser tab, where this is plain `left: 0`. */
    position: fixed; left: env(safe-area-inset-left, 0px);
    top: calc(var(--rail-gap) + env(safe-area-inset-top, 0px));
    height: calc(100vh - var(--rail-gap) * 2);
    height: calc(100dvh - var(--rail-gap) * 2 - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
    /* if it ever does not fit (a short landscape screen), the rail scrolls
       rather than swallowing the buttons at the bottom */
    overflow-y: auto; overflow-x: hidden; scrollbar-width: none;
  }
  .sidebar::-webkit-scrollbar { display: none; }
  /* the room the fixed rail no longer takes out of the row itself — plus the
     notch's strip, which the rail has just moved off (see above). The right
     inset is the same strip when the phone is turned the other way round. */
  main { padding-left: calc(67px + env(safe-area-inset-left, 0px)); padding-right: env(safe-area-inset-right, 0px); }
  /* The emblem sits in the same amount of space above it as below it, on the
     same vertical line as the icons under it, and is big enough to read as the
     badge it is. The rail's own 8px top padding plus 9px here is what puts it
     halfway between the top edge and the first nav icon. */
  .sidebar .side-head, .sidebar.collapsed .side-head {
    padding: 9px 0 9px; margin-bottom: 0; gap: 4px; border-bottom: 0;
    flex-direction: row; justify-content: center;
  }
  .sidebar .side-brand { justify-content: center; flex: initial; padding: 0; }
  .sidebar .brand-emblem { width: 42px; height: 42px; display: block; margin: 0 auto; }
  .sidebar .brand-text, .sidebar .nav-label, .side-nav::before { display: none; }
  .sidebar .side-toggle { display: none; }        /* nothing to expand into */
  .sidebar .nav-btn, .sidebar.collapsed .nav-btn { padding: 9px 0; justify-content: center; gap: 0; }
  /* Former Troopers is a nav item of its own here; like everywhere else it shows
     only while Troopers or Former Troopers is the open page (App.syncFormerTab). */
  .sidebar .nav-btn.nav-sub, .sidebar.collapsed .nav-btn.nav-sub { padding: 9px 0; }
  /* it shares the Troopers icon, and there are no labels here to tell them
     apart — the sub-item wears it a size smaller, the way it is indented on a
     wide screen */
  .sidebar .nav-btn.nav-sub .nav-ico svg { width: 15px; height: 15px; opacity: .78; }
  .sidebar .nav-btn.active::after { display: none; }
  .sidebar .nav-ico svg { width: 19px; height: 19px; }
  /* four buttons stacked — two per row does not fit across a 56px rail */
  .sidebar .side-foot {
    flex-direction: column; align-items: stretch; gap: 4px;
    padding: 6px 0 0; margin-top: auto; flex-shrink: 0;
  }
  .side-foot .icon-btn { flex: initial; width: 100%; height: 30px; border-radius: 10px; }
  .side-foot .icon-btn svg { width: 15px; height: 15px; }
  .btn-badge { top: -5px; right: -5px; min-width: 16px; height: 16px; font-size: 9.5px; }

  /* --- content gutters: one value everywhere, including the two views that
         carry their own wide ones --- */
  .view { padding: 0 10px 60px; }
  #view-troopers, #view-former, #view-history, #view-stats,
  #view-map, #view-scanner { padding-left: 10px; padding-right: 10px; }
  #view-pursuits { padding-left: 10px; padding-right: 10px; }
  /* Pursuits used to keep its own flush-to-the-edge header; it now uses the same
     recessed card as every other view (see .view-head below). */
  /* These four carry their own wide-screen recess (Pursuits 150/154, History 26,
     Statistics and Map 150/154); on a phone every header gets the same 10px one
     as the rest. All four have to be named: the wide-screen rules are written
     with an #id, so a plain `.view-head` here loses to them however far down it
     stands. Statistics and Map were missing from this list and kept the wide
     screen's 30px padding — 88px and 79px tall against everything else's 64 —
     and with it a right margin of -154px hanging off the side of the window. */
  #view-pursuits .view-head, #view-history .view-head,
  #view-stats .view-head, #view-map .view-head { margin: 0 -10px; padding: 24px 17px 12px; }
  #view-pursuits .view-head::before, #view-history .view-head::before,
  #view-stats .view-head::before, #view-map .view-head::before { inset: 18px 10px 6px; left: 10px; }
  /* One row, every view: title, count, then whatever buttons the view has. They
     shrink rather than wrap, so a header with two buttons is exactly as tall as
     one with none. */
  /* The header card starts 18px down — the same gap the rail keeps to the top
     edge, so the two line up. The extra 6px of padding is the card's own inner
     spacing, unchanged.
     The BOX breaks out of the view's 10px side padding to the true edges (the
     rail on the left, the screen on the right) while the CARD stays inside them:
     the opaque box is what hides the content scrolling past beside the card.
     17 SIDEWAYS, not 20, and the same 17 on every view: the card starts 10px in,
     so this is what sets how far the accent bar and the last button sit inside
     it — 7px. Troopers, Former Troopers and Scanner had always used 17 to buy
     room for their extra button while the other four used 20, which is the 3px
     step you could see between Pursuits and Troopers. 17 is the value that fits
     everywhere: at 20 the Scanner heading is 2px short of fitting a 375px phone
     and Troopers is left with 3px of slack in its row. */
  .view-head { margin: 0 -10px; padding: 24px 17px 12px; gap: 7px; flex-wrap: nowrap; }
  .view-head::before { inset: 18px 10px 6px; }
  /* one gap under the header card and the same one under the search row, on
     every view: 8px of margin + the card's 6px inset reads as 14px, which is
     what .filters already keeps below itself */
  .filters { margin-top: 8px; margin-bottom: 14px; }
  /* ONE heading size, one accent bar, one left indent, on every view — the bar is
     sized in em, so it follows the size rather than being set twice.
     nowrap: a heading that runs onto a second line makes ITS header taller than
     the rest (Former Troopers does exactly that in German), and the ellipsis the
     two-button views already ask for only works with nowrap anyway. */
  .view-head h1 { font-size: 19px; padding-left: 10px; flex-shrink: 0; white-space: nowrap; }
  .view-head h1::before { width: 4px; }
  /* The count sits on the heading's baseline on a wide screen. In a 52px phone
     header that reads as "stuck to the bottom" next to a 19px title, and it did
     not even land the same way in every view (a header with two buttons is
     taller than one with none). Centred on the title, everywhere — and it never
     truncates: "46 Cards" is the whole point of the line. Where a header is
     genuinely too narrow (Troopers/Former carry two buttons), the HEADING is the
     one that gives way, so h1 below trades its flex-shrink: 0 for an ellipsis. */
  .head-count {
    font-size: 11px; min-width: 0; white-space: nowrap; flex-shrink: 0;
    align-self: center; margin-bottom: 0;
  }
  /* Troopers/Former are the only headers with TWO buttons, and "Troopers ·
     46 Cards · Change · Add Card" does not fit a 375px line at the normal sizes.
     The buttons and the gaps give up a few px each — enough for the heading to
     stay whole — and the heading keeps an ellipsis as the last resort for a
     narrower phone still. */
  /* only the GAP is theirs now — the 17px side padding they used to set here is
     what every header uses (see .view-head above) */
  #view-troopers .view-head, #view-former .view-head { gap: 5px; }
  #view-troopers .view-head .btn, #view-former .view-head .btn { padding: 0 7px; font-size: 11px; }
  /* The heading itself keeps the ONE size every other header uses. It used to
     drop to 17px here (and the accent bar with it, since the bar is sized in em)
     to buy room for the second button — which made these two headers read a size
     smaller than the rest of the app. The buttons above already give up enough on
     their own: measured at 19px with the longest data to hand, Troopers still has
     24px of slack in the row and Former Troopers 35. The ellipsis stays as the
     last resort for a narrower phone than any of that was measured on. */
  #view-troopers .view-head h1, #view-former .view-head h1 {
    flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis;
  }
  /* one height for every control in a header, so the header itself is one
     height too, whatever a view puts in it */
  .view-head .btn {
    height: 28px; padding: 0 8px; display: inline-flex; align-items: center;
    font-size: 11.5px; white-space: nowrap; flex-shrink: 0;
  }
  .view-head .seg-btn { height: 28px; padding: 0 9px; font-size: 11.5px; }
  .view-head .icon-btn { width: 28px; height: 28px; }
  .view-head .icon-btn svg { width: 15px; height: 15px; }
  /* …except Trooper Management, which is a box of its own beside the header rather than a button
     in it: at 28px it sat as a small square inside a 38px red box with a ring of empty red around
     it. It fills its box on a phone the same way it does on a desktop. */
  .view-head .head-side-btn { width: 100%; height: 100%; }
  /* …and it lines up with both its neighbours: top and bottom with the header block beside it
     (which is inset 18px/6px here), width and right edge with the eye below it (34px, ending
     10px from the edge). `right: 0` and 38px wide made it overhang both. */
  #view-troopers .head-side, #view-former .head-side {
    right: 10px; width: 34px; top: 18px; bottom: 6px;
  }
  /* Scanner's three labelled buttons do not fit across a phone — icons only, and
     even then the row needs the same tighter gaps Troopers/Former use or the
     last one hangs 3px past the edge and the page scrolls sideways */
  #view-scanner .view-head .btn span { display: none; }
  /* 4px, not the 5 the other tight header uses: Scanner carries three icon
     buttons AND a labelled one, and at 19px the heading came out exactly ONE
     pixel short of fitting on a 375px phone. This is the pixel. */
  #view-scanner .view-head { gap: 4px; }
  #view-scanner .view-head .btn { padding: 0 7px; }
  #view-scanner .view-head h1 { flex-shrink: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
  /* One height for every header, whether or not the view has buttons in it —
     History and the Map have none, and were coming out 9px shorter than the rest
     (24 + 28px button + 12 is what a header with buttons measures). */
  .view-head { min-height: 64px; align-content: center; }

  /* search and filter: one under the other. Pursuits is the exception — its only
     filter is the two-button date sort, which is narrow enough to sit beside the
     search field.
     ONE search width across every view: Pursuits sets it (it is the tightest,
     with the sort pair beside it) and the others match, so switching tabs does
     not resize the field under your thumb. --ph-search is what the field has left
     once the sort pair and the gap are taken off the row. */
  :root { --ph-search: calc(100% - 126px); }
  .filters { flex-direction: column; align-items: stretch; gap: 8px; }
  #view-pursuits .filters { flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 8px; }
  /* The search field takes the SAME width here as in every other view (it is not
     flexible, or it would come out a few px off and the eye catches that when you
     switch tabs); the sort pair takes whatever is left and its two buttons split
     that evenly. */
  #view-pursuits .search-wrap { flex: 0 0 var(--ph-search); width: var(--ph-search); min-width: 0; }
  #view-pursuits .sort-ctl { flex: 1 1 auto; min-width: 0; }
  #view-pursuits .sort-ctl .seg { padding: 2px; gap: 2px; width: 100%; }
  #view-pursuits .sort-ctl .seg-btn { flex: 1; padding: 0 6px; height: 34px; font-size: 12px; }
  #view-pursuits .search-field { padding-right: 8px; }
  .search-wrap { width: var(--ph-search); }
  .search-field { min-width: 0; width: 100%; height: 38px; font-size: 14.5px; padding: 0 13px 0 36px; }
  .search-ico svg { width: 14px; height: 14px; }
  .jfilter { padding: 3px; border-radius: 12px; }
  .jbtn { min-width: 27px; height: 27px; padding: 0 8px; font-size: 12px; }
  /* Former Troopers upright: the search field runs the full width of the header card and the
     filter under it, instead of stopping at the width Pursuits needs for its sort pair — nothing
     sits beside it on this page, so the room was simply going to waste (Rick, 2026-09-05). */
  #view-former .search-wrap { width: 100%; flex: 1 1 auto; }
  /* …and the five status buttons share the filter's width between them rather than leaving a gap
     at the right end. `flex: 1` on each, and the padding drops out of the way of the longest. */
  #view-former .jfilter { display: flex; flex-wrap: nowrap; }
  #view-former .jbtn { flex: 1 1 auto; min-width: 0; padding: 0 4px; }
  .filters .spacer { display: none; }
  .ph-toggle { align-self: flex-end; }
  /* No hover state on a phone: a tap leaves :hover STUCK on a touch screen, so
     the eye flipped to "hidden" while the placeholders were plainly still
     there, and stayed that way until something else was touched. On and off is
     all this button has here — the .off class says which. */
  .ph-toggle:hover {
    background: var(--surface-2); color: var(--text-soft); border-color: var(--border);
  }
  .ph-toggle:hover .ph-eye-on { display: inline-flex; }
  .ph-toggle:hover .ph-eye-off { display: none; position: absolute; }
  .ph-toggle.off:hover .ph-eye-on { display: none; }
  .ph-toggle.off:hover .ph-eye-off { display: inline-flex; position: static; }

  /* --- pursuit cards: the desktop card is a wide row of labelled columns ---
     The bottom keeps the fold arrow's own room (the shorthand here was resetting the 26px the
     arrow needs, so it sat ON the Troop / Vehicle / Event Tags labels — Rick, 2026-09-05). */
  .pcard { padding: 12px 14px 26px; }
  /* the desktop card is one row of labelled columns split by dividers; let it
     wrap instead of stacking outright, so a phone held sideways still gets two
     or three of them per line */
  .pcard-body { flex-wrap: wrap; gap: 10px 16px; }
  /* a section keeps its content's width and wraps as a block — shrinking them
     made the chips spill over the section next to it */
  .pcard-sec { flex: 0 0 auto; }
  .pcard-sec.grow { flex: 1 1 100%; }        /* the trooper list gets its own line */
  /* a long name ("(A-16) K9-Trp. Austin Duncan Fleminster") is wider than the
     card on a phone — let the chip wrap rather than run off the edge */
  .pcard-chips .tag { white-space: normal; }
  /* On a phone every trooper gets a LINE of their own. Side by side, two names of
     that length wrapped into each other and the card read as one grey block —
     which is on it is the thing the card is looked at for. The chips keep their
     own width (align-items: flex-start), they just no longer share a row. */
  .pcard-row2 .pcard-sec.grow .pcard-chips { flex-direction: column; align-items: flex-start; gap: 6px; }
  .pcard-sec + .pcard-sec { border-left: 0; padding-left: 0; }
  .pcard-locwrap { border-left: 0; padding-left: 0; margin-left: 0; }
  .pcard-oc { display: none; }                    /* floats into a gutter that no longer exists */

  /* ===== Troopers' jurisdiction filter on a phone =====
     Twelve letters plus All, ICP and K9 wrap into a ragged four-line block on a
     375px screen. Here it is one deliberate shape instead:

         ┌─────┬─────────────────┬─────┐
         │     │ A  B  C  D  E  F│ ICP │
         │ All ├─────────────────┼─────┤
         │     │ G  H  I  J  K  L│ K9  │
         └─────┴─────────────────┴─────┘

     The buttons are placed by position, so this depends on the order render()
     builds them in: All, A…L, ICP, K9 (js/troopers.js). Store.JURISDICTIONS is
     the twelve letters — if that list ever changes, so must the nth-child spans.
     The eye (hide placeholders) sits OUTSIDE the block, to its right, and only
     exists while a letter is picked — hence the grid on .filters. */
  /* The eye's column exists only when the eye does (`has-ph`, set in
     js/troopers.js). Reserving it either way kept the block one size, but it also
     stopped the filter 42px short of the right edge for a button that was not
     there — so with no eye the block runs the full width, and picking a letter
     pulls it in by exactly the eye it makes room for. */
  #view-troopers .filters {
    display: grid; grid-template-columns: 1fr; align-items: center; gap: 8px;
  }
  #view-troopers .filters.has-ph { grid-template-columns: 1fr 34px; }
  /* The search field stands in column 1 — the same column the letter block below it fills — so the
     two end on one line. It used to span the eye's column as well, which made it the widest thing
     on the page: wider than the letters under it and wider than the header above it. */
  #view-troopers .search-wrap { grid-column: 1; width: auto; }
  /* row 2 explicitly: the filter row is row 2 (the search field spans row 1), and
     leaving it to auto-placement put the eye a row lower wherever something else
     in .filters is visible and takes a cell — which is exactly what the spacer
     does sideways. It is not a grid item here at all. */
  #view-troopers .filters .spacer { display: none; }
  #view-troopers .ph-toggle { grid-row: 2; grid-column: 2; align-self: center; width: 34px; height: 34px; }
  #view-troopers .jfilter {
    grid-column: 1;
    display: grid; grid-template-columns: auto repeat(6, 1fr) auto;
    gap: 2px 0; justify-items: stretch; width: 100%;
    /* the base rule centres its items; here they must fill their cell, or All
       spans two rows as a 14px-tall label floating in the middle of them */
    align-items: stretch;
  }
  #view-troopers .jbtn { min-width: 0; height: 28px; padding: 0 2px; font-size: 12.5px; }
  /* All spans both rows on the left (height: auto, or it sits 28px tall in the
     middle of them); ICP sits over K9 on the right. The hairlines between the
     three areas are borders on those buttons rather than separate elements. */
  #view-troopers .jbtn:nth-child(1) {
    grid-row: 1 / 3; grid-column: 1; padding: 0 12px; height: auto;
    border-right: 1px solid var(--border); margin-right: 5px;
  }
  #view-troopers .jbtn:nth-child(n+2):nth-child(-n+7)  { grid-row: 1; }   /* A–F */
  #view-troopers .jbtn:nth-child(n+8):nth-child(-n+13) { grid-row: 2; }   /* G–L */
  /* the two of them keep a hair of daylight between their pills */
  #view-troopers .jbtn-icp { grid-row: 1; grid-column: 8; padding: 0 7px; margin-bottom: 2px; }
  #view-troopers .jbtn-k9  {
    grid-row: 2; grid-column: 8; padding: 0 7px; margin-top: 2px;
    gap: 3px; justify-content: center;
  }
  #view-troopers .jbtn-k9 svg { width: 12px; height: 12px; flex-shrink: 0; }
  #view-troopers .jbtn-k9, #view-troopers .jbtn-icp {
    border-left: 1px solid var(--border); margin-left: 5px; align-self: stretch;
  }

  /* --- trooper / K9 cards: wrap instead of ten fixed columns (fitGrid()
         steps aside below 700px, see js/troopers.js) --- */
  /* 130px lets two cards sit side by side on a 375px screen; wider phones fit
     three without any further help */
  /* Fixed 130px columns packed from the left: the gap BETWEEN two cards is the
     same 10px the rows keep between each other. The left column keeps the view's
     own 10px gutter to the rail; whatever is left over stays on the right. */
  /* TWO across upright, and they fill the column: `repeat(auto-fill, 130px)` left
     the row short of the right edge by whatever did not divide evenly, so the
     cards sat left-packed with a ragged gap on the right. Two equal fractions
     reach both edges, and the 10px between them is the same 10px as before. */
  .trooper-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px; zoom: 1 !important;
  }
  /* 🔴 A PHONE CARD IS THE DESKTOP CARD, SCALED — nothing here is re-proportioned (Rick,
     2026-09-06). It keeps its 195x270 markup and everything inside it, and Troopers.fitGrid puts
     ONE `zoom` on it for whatever the column is actually wide. That is the same trick the Change
     panel's slots use (fitSlotCards), and for the same reason: the card is a design, not a set of
     boxes that happen to be near each other. What stood here before was the other approach — a
     130:190 outline with the photo, the type and the padding each given their own phone value —
     and it drifted: the card came out at 0.684 against the desktop's 0.722, with the photo filling
     66% of the width against 62%. Anything set per-element here starts that drift again. */
  .trooper-grid .tcard, .trooper-grid .tcard-ph { margin: 0; justify-self: center; }
  /* (the photo, the type and the spacing inside a card are the desktop's, scaled with it — see
     the note above; the only thing left to say here is that a touch screen does not hover) */
  .tcard:hover, .tcard.tcard-ph:hover { transform: none; }

  /* --- history, stats, scanner --- */
  /* the timeline is two fixed-width columns (570 + 500) side by side */
  .hist-row { flex-direction: column; gap: 6px; max-width: 100%; }
  .hist-item { flex: 0 0 auto; width: 100%; height: auto; min-height: 56px; }
  .hist-note { white-space: normal; }
  .hist-memo { width: 100%; flex: 0 0 auto; height: auto; min-height: 44px; }
  .hist-items { padding-left: 14px; }
  /* minmax(0,…) everywhere here: a plain 1fr column cannot shrink below its
     content, and a chart canvas keeps whatever width it was last drawn at */
  .stats-grid { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .stat-pair { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .chart-box { padding: 12px 12px 14px; }
  .chart-wrap { height: 200px; }
  .chart-wrap.tall { height: 240px; }
  /* minmax(0,…) rather than 1fr: a plain 1fr column cannot shrink below its
     content, and the KPI's icon + number + label add up to ~197px each */
  .stat-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  .kpi { padding: 11px 12px; gap: 9px; }
  .kpi-ico { width: 36px; height: 36px; border-radius: 11px; }
  .kpi-ico svg { width: 18px; height: 18px; }
  .kpi-num { font-size: 22px; }
  .kpi-lbl { font-size: 10px; }
  .kpi-sub { font-size: 10px; }
  .kpi-pop { display: none; }              /* a hover popover has no meaning on a touch screen */
  /* minmax(0,…), not a plain 1fr: a 1fr track cannot shrink below its content's
     min-content, and a feed card's row of controls measures ~418px — so the
     Scanner was the one view you could still push sideways on a phone. Same trap
     as the two stats grids above. */
  .scan-grid { grid-template-columns: minmax(0, 1fr); }
  .scanf, .scanf-head, .scanf-meta, .scanf-viz, .scan-meter { min-width: 0; }
  /* Every other view puts a .filters row under the header and that row carries
     the 8px which — with the header card's own 6px inset — reads as the 14px gap
     the whole app uses. The Scanner has no filters row; the master controls sit
     straight under the header and were 6px short of everywhere else. */
  .scan-master { margin-top: 8px; }

  /* --- windows ---
     EVERY window on a phone is the Add/Edit Pursuit window now: the same 18px
     inset off all four edges, the same fixed height, the same opaque backdrop,
     and its own body scrolling inside a frame that does not move.
     This used to be opt-in — only the two long forms passed opts.fullPhone — so
     Administration, Watch and the rest floated as cards of whatever height their
     content happened to be, over a page that was still scrolling behind them.
     One arrangement for all of them means no window on a phone ever behaves
     differently from the last one you opened. */
  .modal-overlay {
    padding: 18px 8px; align-items: stretch; justify-content: stretch;
    background: var(--bg); overflow: hidden; overscroll-behavior: contain;
  }
  .modal, .modal:has(.settings-layout), .modal:has(.detail-modal-body) {
    width: 100%; max-width: 100%;
    height: calc(100dvh - 36px); max-height: calc(100dvh - 36px);
    display: flex; flex-direction: column;
  }
  .modal-header { flex-shrink: 0; }
  /* no arrows on a phone: the window is swiped sideways instead (App wires the
     gesture on #modalBody), and a button hanging over the content would only be
     in the way of the thing it is pointing at */
  .modal-nav { display: none; }
  /* the one thing that scrolls. overscroll-behavior so reaching its end does not
     hand the gesture on to whatever is underneath */
  .modal-body {
    flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative;   /* relative anchors the phone sheet, see .modal-body */
    overscroll-behavior: contain;
  }
  /* Change history scrolls as ONE page inside that frame — heading, explainer,
     the search/filter bar and the entries all move together. The list used to
     have a scroller of its own (.aud-scroll, capped at 52vh), which left the
     three of them pinned above a small moving window. */
  /* the id, not the class: the base .aud-scroll rule is further down the file and
     a media query adds no specificity of its own, so a bare class would lose */
  #aud-list.aud-scroll { max-height: none; overflow: visible; }

  /* …and the bar in it keeps the desktop field, at the desktop size. The phone
     search metrics exist so every VIEW's search field matches its neighbours;
     this one lives inside a window and has no neighbours to match. */
  #aud-filter .search-wrap { width: auto; }
  #aud-q { min-width: 240px; width: 100%; height: 42px; font-size: 16px; padding: 0 15px 0 40px; }
  #aud-filter .search-ico svg { width: 15px; height: 15px; }

  /* --- Pick Location on a phone ---
     It used to be a desktop dialog dropped onto a phone: a card floating in the
     middle over a dim backdrop, with a map sized at 82vh that ran past the
     bottom edge. Here it is the SAME window as the form it was opened from —
     the numbers below are lifted from .modal-full-phone above and .modal's
     22px radius on purpose, so it lands exactly where Add/Edit Pursuit was
     standing instead of on top of it. Upright and sideways both: the geometry
     is dvh-based, so a rotation just re-measures. */
  .picker-overlay {
    padding: 18px 8px; align-items: stretch; justify-content: stretch;
    background: var(--bg); overflow: hidden; overscroll-behavior: contain;
  }
  .picker-panel {
    width: 100%; max-width: 100%;
    height: calc(100dvh - 36px); max-height: calc(100dvh - 36px);
    border-radius: 22px;
  }
  /* head and foot keep their height; the map takes everything that is left,
     which is what makes the panel end exactly where the form's did */
  .picker-map-wrap { flex: 1; min-height: 0; }
  .picker-map { height: 100%; min-height: 0; }
  .picker-head, .picker-foot { flex-shrink: 0; }
  .picker-foot #pk-coords { font-size: 11.5px; }

  /* The form underneath is HIDDEN, not closed — every field it holds is still
     standing, and Save hands its value straight back to it. visibility (not
     display) keeps the layout and the scroll position it had; pointer-events
     stops a touch that misses the picker from reaching a field behind it. */
  body.picker-open .modal-overlay { visibility: hidden; pointer-events: none; }
  body.picker-open .modal-overlay .modal-body { overflow: hidden; }
  .modal-full-phone .pursuit-form, .modal-full-phone .trooper-form { max-width: 100%; }
  .modal-full-phone .row-date, .modal-full-phone .row-vl,
  .modal-full-phone .inline-row { flex-wrap: wrap; }
  .modal-full-phone .rating-field { margin-left: 0; }
  /* A YouTube link with its ?si= tail is long, and at calc(100% - 168px) the end
     of it was always out of sight on a phone. Upright the field takes the whole
     width — there is nothing beside it to leave room for. */
  .modal-full-phone .full-video-field { width: 100%; }
  .modal-full-phone .troopers-field, .modal-full-phone .troopers-field.troop-field { max-width: none; }
  /* The fields keep the sizes and column counts the form was written with — the
     inline `grid-template-columns` stay. All they need is permission to shrink:
     a `1fr` track is `minmax(auto, 1fr)`, and that `auto` floor is the item's
     min-content, which is what pushed the window 140px wider than the phone.
     `min-width: 0` on the items lowers the floor and the columns fit. */
  .modal-full-phone .field, .modal-full-phone .compact-row > *,
  .modal-full-phone .sm-input, .modal-full-phone input, .modal-full-phone .combo { min-width: 0; }
  /* Five picture slots across a phone leave each one ~34px wide — the Choose
     button did not fit inside its own drop zone and hung out over the edge of
     it. Three across, so a slot is about the size of the thumbnail it will
     hold, and the label fits with it. */
  .pursuit-form #f-photo-zone .photo-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 7px; }
  .pursuit-form #f-photo-zone .dropzone.photo-add .muted { font-size: 10.5px; line-height: 1.2; }
  .pursuit-form #f-photo-zone .dropzone.photo-add .file-btn { font-size: 11px; padding: 4px 8px; }
  /* Reason is a free-text combo with the longest values in the form — on a phone
     it drops under Outcome and takes the whole width, like the Notes box below
     it. (Its 235px is an inline style, hence the !important.) */
  .modal-full-phone .row-vl { flex-wrap: wrap; }
  .modal-full-phone .reason-field { width: 100% !important; flex: 1 0 100%; }
  .modal-header { padding: 12px 14px; }
  .modal-body { padding: 14px; }
  /* the settings/admin window: categories become a scrollable row on top */
  .settings-layout { flex-direction: column; gap: 12px; min-height: 0; }
  .set-nav {
    flex-direction: row; width: auto; overflow-x: auto; gap: 6px;
    padding-bottom: 4px; border-right: 0; -webkit-overflow-scrolling: touch;
  }
  .set-cat { flex: 0 0 auto; white-space: nowrap; }
  /* Trooper / K9 Details keep their wide-screen layout on a phone — photo beside
     the facts, same sizes. (They were stacked and centred here for a while; the
     window is scrollable either way and the familiar shape reads better.) */
  /* Last seen History and Change History: one line per entry, dragged sideways
     when it is longer than the window — never wrapped over three lines.
     The two jobs sit on two elements ON PURPOSE (troopers.js wrapHistScrollers
     builds the box). The LIST is as wide as its longest entry, which is what
     makes every rule under a row run that same length instead of a ladder; an
     element sized to its own content has nothing left to scroll, so the BOX
     around it does that. Both on the list at once is what stopped this working:
     it grew past the window body, and that is `overflow-x: hidden` on a phone. */
  /* Sideways only. No cap on the height and no vertical scroller of its own:
     the whole entry list stands there at full length and the WINDOW carries it,
     which is the one thing that scrolls up and down on a phone now.
     overflow-y is hidden rather than visible on purpose — a box with visible on
     one axis and auto on the other is turned back into auto by the browser, and
     the inner scroller would be right back. With no max-height there is nothing
     to clip, so hidden costs nothing. */
  .detail-modal-body .hist-scroll {
    max-height: none; overflow-x: auto; overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior-x: contain;   /* the window behind it does not move along */
  }
  .detail-modal-body .history-list { max-height: none; width: max-content; min-width: 100%; }
  /* the held-at-the-time column gives up its fixed width here: the rows scroll
     sideways on a phone anyway, so a column that lines up costs reach on the
     case number at the end of the row and buys nothing */
  .detail-modal-body .history-list .h-held { min-width: 0; }
  .detail-modal-body .history-list li { width: 100%; }
  .detail-modal-body .history-list .h-text { white-space: nowrap; }
  .hist-seg .seg-btn { padding: 6px 10px; font-size: 12px; }

  /* (no cap on .aud-scroll here — the whole panel scrolls as one page on a
     phone; see the --- windows --- block above) */
  /* Change history rows keep their normal shape too — only the deletion-request
     and account rows, which carry stacked action buttons, need to wrap here. */
  .req-row:not(.aud-row) { flex-wrap: wrap; }
  .req-row:not(.aud-row) .req-actions-col { flex-direction: row; align-items: center; width: 100%; }
  /* the badge gives up its fixed column so the name keeps the width instead */
  .usr-row .aud-op { min-width: 0; }

  /* --- the map's pursuit popup is sized for a desktop map: on a phone it
         covered the map it is pointing at --- */
  .mapboxgl-popup-content {
    max-width: min(70vw, 230px) !important; padding: 10px 11px; border-radius: 15px;
    /* it anchors at the pin and grew straight off the top of the map. Upright the
       pin now sits at three quarters of the height (MapView.pinOffset), so there
       is room for a much taller popup before it needs to scroll at all. */
    max-height: 62vh; overflow-y: auto;
  }
  .mp-tchip { white-space: normal; }
  .mp-case { font-size: 13.5px; }
  .mp-meta-line { font-size: 11.5px; margin-top: 2px; }
  .mp-body { gap: 7px; margin-bottom: 7px; }
  .mp-head { margin-bottom: 7px; gap: 5px; }
  .mp-lbl { font-size: 9px; letter-spacing: .8px; }
  .mp-tchip, .mp-sec .tag { font-size: 10.5px; padding: 2px 8px; }
  .mp-hide-btn, .mp-close-btn, .mp-zoom-btn { width: 22px; height: 22px; }
  /* the History panel and the Layers tiles are sized for a desktop map and took
     a third of a phone screen between them */
  /* The map fills what is left of the screen instead of running past the bottom
     of it: the page itself does not scroll, and the map's lower edge lines up
     with the rail's. #view-map becomes the column that hands the leftover height
     to the panel. */
  /* svh, not dvh: dvh is whatever the viewport is RIGHT NOW, so a page sized
     with it while iOS had its toolbar collapsed comes out taller than the
     viewport the moment the toolbar comes back — which it does, because this
     page does not scroll. The header ended up behind the address bar. svh is the
     height with the toolbar SHOWING, the smallest it ever gets, so the page fits
     without the toolbar ever needing to move. dvh first as the fallback for
     engines that do not know svh. */
  #view-map { display: flex; flex-direction: column; height: 100dvh; height: 100svh; padding-bottom: 18px; }
  #view-map .map-panel { flex: 1; min-height: 0; }
  #view-map #mapContainer { height: 100%; min-height: 0; }
  #view-map .view-head { position: relative; top: auto; }

  /* the same narrow year list the desktop has — it holds four-digit years, nothing else */
  .map-history { width: 104px; right: 58px; padding: 8px; }
  .map-layers { bottom: 10px; left: 10px; gap: 7px; }
  .map-layers-btn, .ml-tile {
    width: 62px; height: 58px; border-radius: 13px; font-size: 9.5px; gap: 3px; padding: 4px 3px;
  }
  .map-layers-btn .ml-ic, .ml-tile .ml-ic { height: 24px; }
  .map-layers-btn .ml-ic svg, .ml-tile .ml-ic svg { width: 20px; height: 20px; }
  .mp-details { padding: 6px 10px; font-size: 12px; }
  .mp-rating .star svg { width: 11px; height: 11px; }

  /* --- the Change panel ---
     A whole task in a small window: the tab row, the slots, the hint and the
     pick-list all give up a few px so more than one of them is on screen at
     once. And nothing in it may be wider than it is — a horizontal scrollbar
     inside a panel you also drag cards into is one gesture too many. */
  #switchPanel { overflow-x: hidden; }
  #switchPanel > * { max-width: 100%; }
  .switch-head { margin-bottom: 8px; padding-bottom: 8px; }
  .switch-head strong { font-size: 17px; }
  .sw-slot-hint { margin: 8px 0 2px; font-size: 12px; }
  .sw-picklist-wrap { margin-top: 10px; }
  /* No cap and no scroller of its own on a phone: the panel already scrolls,
     and a list scrolling inside it made two scrollbars for one gesture. Every
     option is on screen and the panel carries the length (Troopers.fitPickList
     leaves the inline max-height off to match). */
  .sw-picklist { max-height: none; overflow-y: visible; gap: 6px; }
  .sw-pickcard { padding: 8px 10px; font-size: 13px; }
  .sw-summary { margin-top: 8px; padding: 9px 11px; }

  /* --- the Change panel's slots: side by side, both orientations --- */
  .sw-slots { gap: 6px; flex-direction: row; align-items: stretch; }
  .sw-slot { padding: 6px; min-height: 0; flex: 1; min-width: 0; }
  /* Two 195px cards will not fit a phone side by side, so a slot card is SCALED rather than
     re-proportioned: it keeps the real card's markup and its 195×270, and Troopers.fitSlotCards
     puts a `zoom` on the wrapper to whatever the slot is actually wide.
     It used to be re-proportioned instead — width 100%, a flat min-height — and that made a slot
     card 95×164, 0.58 against the card's own 0.72, so a trooper in a slot was a different shape
     from the same trooper on the dashboard. Giving it `aspect-ratio` alone fixed the outline and
     then cut the call-sign badge off the bottom, because the innards were still full size. One
     scale factor keeps the shape AND everything in it (Rick, 2026-09-05).
     No per-part font sizes here any anymore for the same reason: zoom carries them. */
  .sw-slot .sw-card { width: auto; }
  .sw-slot .tcard { border-width: 2px; }

  /* --- right-edge panels (Change / Notes) ---
     They start where the rail ends, never on top of it, and they leave the
     dashboard below them: a card has to stay reachable to be dragged into a
     Change slot. */
  .switch-panel, .switch-panel.notes-panel {
    position: fixed; top: 18px; right: 8px; bottom: auto; left: 70px;
    width: auto; max-width: none;
    height: auto; max-height: calc(100dvh - 36px); margin: 0; border-radius: 18px; z-index: 90;
  }
  body.notes-active #view-troopers, body.notes-active #view-former,
  body.notes-active #view-pursuits { padding-right: 10px; }
  /* clear of the band under the rail — at bottom:14px the button's lower edge sat
     inside it and came out with a slice taken off (--rail-gap is 18px here) */
  .notes-fab { right: 14px; bottom: calc(var(--rail-gap) + 10px); }

  /* The saved-note list is `flex: 1` so it fills a tall desktop panel. On a phone —
     and especially sideways, where the panel is only ~360px tall — the New Note
     form alone is taller than that, so the list was squeezed to zero height and
     the notes looked like they had vanished. Here the list keeps its natural
     height and the PANEL is what scrolls. */
  .notes-create-box, .notes-saved-head, .notes-list { flex: 0 0 auto; }
  .notes-list { overflow: visible; }

  /* Cards are dragged by press-and-hold; holding on their TEXT started a text
     selection instead, so the drag never began. Nothing in a card is worth
     selecting anyway. */
  /* …and buttons: a long press on one used to start a text selection over its
     label instead of just being a press. */
  .tcard, .pcard, .sw-pickcard, .sw-card, .note-card, .scanf,
  button, .btn, .jbtn, .seg-btn, .icon-btn, .nav-btn, .set-cat, .map-ctrl-btn {
    -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
  }

  /* --- the floating video player ---
     The width used to be pinned here with !important, which made the pop-up the
     full width of the screen AND unresizable: the corner handles had nothing to
     move, because the stylesheet was overruling them on every frame. js/video.js
     sizes it now (about half the screen upright, a third sideways) and the
     corners work here exactly as they do with a mouse — the handles already
     listen for pointer events and set touch-action: none. */
  .vp-mini .vp-rs { width: 26px; height: 26px; }   /* a finger is not a cursor */

  /* Expanded, it is the Add/Edit Pursuit window: same 8px sides, same 18px top
     and bottom, so the big player and the form it was opened from stand in
     exactly the same place. */
  .vp-window {
    left: 8px; right: 8px; top: 18px; bottom: 18px;
    width: auto; max-width: none; transform: none;
  }

  /* Three tabs at 130px each plus the Pop-up button do not fit across a phone —
     the button came down on top of "Notes". The tabs take only what their labels
     need and the row keeps the right-hand gap free for the button. */
  .wt-topbar { padding-right: 40px; }
  .wt-seg .seg-btn { min-width: 0; padding: 6px 11px; font-size: 12.5px; }
}

/* ===== Troopers / Former, phone held sideways =====
   Upright, the A–L filter is a deliberate block: All spanning two rows on the
   left, A–F over G–L in the middle, ICP over K9 on the right, with the eye
   outside it — a grid, built by the rules above. Sideways there is width for the
   plain desktop row again, so the whole grid is undone here and the base
   inline-flex look comes back.

   The right-hand panels do NOT change that any more. Change and Notes take about
   half the width with them sideways, and the answer to that is not the upright
   two-row block — it is the same row, wrapped: the search keeps the first line to
   itself and the filter and the eye take the second. The A–L pills shrink to fit
   whatever is left, which is what they were built to do.

   Specificity here ties with the grid rules above (one id, two classes, one
   element); this block sits after them, so it wins on order. */
@media (max-height: 500px) and (max-width: 950px) and (orientation: landscape) {
  body #view-troopers .filters,
  body #view-former .filters {
    /* nowrap: the filter's natural width is wider than the row, and with wrapping
       allowed it dropped to a second line before anything shrank. Everything on
       this row can give width up, so one line is always reachable. */
    display: flex; flex-direction: row; flex-wrap: nowrap; align-items: center; gap: 8px;
  }
  /* an explicit width, not `auto` plus a flex-basis: the phone block sizes this
     wrapper with a percentage (--ph-search), and a percentage width still sets
     the item's min-content floor, which the basis then loses to */
  body #view-troopers .search-wrap,
  body #view-former .search-wrap {
    width: 210px; flex: 0 0 210px; min-width: 0;
  }
  /* The filter takes everything between the search field and the eye, so the row
     runs edge to edge: search on the left, the eye hard against the right, and
     the A–L block filling what is between them. */
  body #view-troopers .jfilter,
  body #view-former .jfilter {
    display: inline-flex; flex-wrap: nowrap; align-items: center; gap: 3px;
    /* basis 0, not auto: with a panel out the row is allowed to wrap, and at its
       content width this block claimed the whole second line and pushed the eye
       down to a third. From zero it takes only what is left once the eye has its
       34px, then grows into it. Unwrapped (no panel) the two behave the same. */
    grid-column: auto; width: auto; flex: 1 1 0; min-width: 0;
  }
  /* every button back to a plain pill: no grid placement, no dividers, no
     row-spanning All — those are all the block's doing */
  body #view-troopers .jbtn,
  body #view-former .jbtn,
  body #view-troopers .jbtn:nth-child(1),
  body #view-troopers .jbtn-icp,
  body #view-troopers .jbtn-k9 {
    grid-row: auto; grid-column: auto;
    height: 27px; min-width: 0; flex: 1 1 auto; padding: 0 4px; font-size: 12px;
    border: none; margin: 0; align-self: auto;
  }
  /* no spacer: the eye belongs beside the filter it acts on, not pushed out to
     the far edge of the row with the width of the screen between them */
  body #view-troopers .filters .spacer,
  body #view-former .filters .spacer { display: none; }
  body #view-troopers .ph-toggle,
  body #view-former .ph-toggle {
    grid-row: auto; grid-column: auto; align-self: center; flex: 0 0 34px;
  }

  /* --- with the Change or Notes panel out ---
     Same row, wrapped onto two lines. There is no width for the search field AND
     fifteen letters side by side once half the screen has gone, so the search
     takes the first line on its own and the filter and the eye take the second —
     the filter still stretching to the edge, the eye still hard against it. */
  body.switch-active #view-troopers .filters,
  body.switch-active #view-former .filters,
  body.notes-active #view-troopers .filters,
  body.notes-active #view-former .filters { flex-wrap: wrap; row-gap: 8px; }
  body.switch-active #view-troopers .search-wrap,
  body.switch-active #view-former .search-wrap,
  body.notes-active #view-troopers .search-wrap,
  body.notes-active #view-former .search-wrap { flex: 1 1 100%; width: auto; }
  /* Thirteen of these are one character and shrink to almost nothing without
     complaint; ICP and K9 are not, and on the narrowest sideways screen they were
     being squeezed under their own text until the two ran together. They keep
     enough to be read and the letters give up the difference. */
  body #view-troopers .jbtn-icp, body #view-former .jbtn-icp { min-width: 26px; padding: 0 2px; }
  body #view-troopers .jbtn-k9, body #view-former .jbtn-k9 { min-width: 34px; padding: 0 2px; }
  /* the Notes button moves out towards the edge, so the card row beside it is not
     reaching right up against it */
  .notes-fab { right: calc(6px + env(safe-area-inset-right, 0px)); }

  /* --- how many cards fit across, sideways ---
     A FIXED count, not `repeat(auto-fill, 130px)`. auto-fill makes the number a
     function of the screen width, so a 667px phone got five and a 930px one six
     and the row never looked the same twice. Five is what a phone held sideways
     shows; with the Change or Notes panel out there is about half the width left,
     which is three.
     The cards keep their designed 130px wherever there is room for it and shrink
     together where there is not — a cell narrower than the card is what the
     max-width/width pair handles, so five stay five even on a short phone. */
  #view-troopers .trooper-grid, #view-former .trooper-grid {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
  body.switch-active #view-troopers .trooper-grid,
  body.switch-active #view-former .trooper-grid,
  body.notes-active #view-troopers .trooper-grid,
  body.notes-active #view-former .trooper-grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
  /* (the card is the desktop card scaled — its width comes from fitGrid's `zoom`, capped at the
     130px this layout was designed around; nothing inside it is sized separately, see the note on
     .trooper-grid .tcard in the phone block above) */
  #view-troopers .trooper-grid .tcard, #view-troopers .trooper-grid .tcard-ph,
  #view-former .trooper-grid .tcard, #view-former .trooper-grid .tcard-ph {
    justify-self: center;
  }

  /* Add/Edit Pursuit's picture row, sideways. Five slots across a landscape
     phone came out 132px each — on a screen only 390px tall that is a third of
     the height gone to an empty drop zone. Capping the row keeps five across at
     a size that leaves room for the form above it. */
  .pursuit-form #f-photo-zone .photo-grid { max-width: 540px; }
}

/* ===== Phones, upright =====
   The pursuit card's top band is Case · Date · Rating · pin in one row. Upright
   that becomes a small grid: the two facts stacked on the left, the rating in
   the bottom-right and the pin in the corner above it.
   The children are addressed by position — the row is built in a fixed order
   (Case, Date, spacer, Rating, pin) in Pursuits.rowHtml. */
@media (max-width: 700px) and (orientation: portrait) {
  /* Upright the Change panel is not a floating panel at all — it sits at the TOP
     of the content column, ABOVE the view header, and everything else (header,
     search, filters, cards) moves down under it. Nothing is covered and the page
     scrolls as one.
     To get it there the column has to become a real column: the rail is taken out
     of the flow (it is a fixed 56px strip here anyway), body stacks instead of
     sitting side by side, and the panel is ordered before <main>. Sideways it
     stays the floating right-hand column it is, so the dashboard cards behind it
     can still be dragged into a slot. */
  /* Notes is one window whether you are looking at the list or at one note: same
     place, same width, same height, same gap to the rail. Without a bottom it
     took its content's height and an opened note came out half as tall as the
     list it came from. */
  .switch-panel.notes-panel, .switch-panel.notes-panel.notes-detail {
    top: 18px; right: 8px; bottom: 18px; left: 70px;
    width: auto; height: auto; max-height: none; border-radius: 18px;
    align-self: stretch; padding: 18px 18px;
  }

  body { flex-direction: column; }
  .sidebar, .sidebar.collapsed { position: fixed; left: 0; }
  /* the 820px query gives main 20px top/bottom and 16px right padding; upright
     the views carry their own gutters and the header has to start at the very
     top, so only the room for the rail is left */
  /* 8 margin + 56 rail + 3 margin, and the safe-area insets kept in — upright
     they are 0 on a phone, but this must not be the rule that quietly drops them */
  main {
    padding: 0 env(safe-area-inset-right, 0px) 0 calc(67px + env(safe-area-inset-left, 0px));
  }
  /* `#switchPanel`, not `.switch-panel:not(.notes-panel)`: the Scanner's Codes
     panel shares that class, and pulling THAT into the flow made the page scroll
     sideways. It stays the floating panel it is. */
  #switchPanel {
    /* ⚠ No `position` here. It used to say `sticky; top: 18px` with a comment explaining why —
       and a LATER rule in this same block sets `position: static` (see the "was sticky because the
       page scrolled under it" rule below), so both did nothing and the comment argued for the
       opposite of what the page does. Static is the one that holds: the panel takes the top of the
       column and the view scrolls inside itself.
       The z-index STAYS. It is not dead with `static`: this is a flex item, and a flex item's
       z-index applies without a position — taking it out let the panel come forward from 80 to the
       base rule's 90. */
    z-index: 80;
    order: -1; align-self: stretch;
    /* Flush with the header CARD, not with the column: main's padding puts the column at 67px and
       the card is inset another 10px inside it, so the panel used to sit 10px further left than
       everything it stands above — 3px from the rail where the header has 13 (Rick, 2026-09-05).
       Right edge matched too, so the panel and the header card are one column. */
    margin: 18px 10px 0 77px; width: auto; max-width: none;
    /* It ends where the rail beside it ends — 18px of air top and bottom, the same
       --rail-gap, so the two line up (Rick, 2026-09-05). One height for every field
       open in it, which is what the panel used to measure for itself; a fixed one
       does the same job and says what it is. The pick-lists show every option here
       (no inner scroller any more), so the panel is what scrolls. */
    height: calc(100svh - 36px); max-height: calc(100svh - 36px); overflow-y: auto;
    border-radius: 18px; padding: 14px 16px;
  }
  /* …except a screen whose whole job is to be dropped onto: those stay only as tall
     as they are, so the cards on the dashboard below them are still in reach. */
  /* …and they stand clear of the view header underneath: at the header's own 18px the two
     surfaces met edge to edge and read as one block stuck together (Rick, 2026-09-05). */
  #switchPanel.sw-compact { height: auto; margin-bottom: 16px; }

  /* Trooper / K9 Details: photo left, the name and the fact chips in a column
     beside it, Notes underneath the whole block. The photo comes down to 56px so
     that column is wide enough for two chips side by side; the name's underline
     is only as wide as the name (inline-block), not the column. */
  .detail-head, .detail-card .detail-head { align-items: flex-start; gap: 11px; }
  /* One line, and the underline stops at the last letter — which needs an
     inline-block only as wide as its text, so it must not wrap. A name too long
     for the room is scaled down instead (Troopers.fitOne). The right margin
     keeps it clear of the Edit button pinned in the card's corner. */
  .detail-info strong {
    font-size: 18px; white-space: nowrap; width: fit-content;
    /* not 100%: `fit-content` measures against the CONTAINER, so a long name ran
       straight under the Edit button in the corner. This is the room actually
       left beside it — and it is also what Troopers.fitOne() measures, so an
       over-long name is scaled down rather than cut. */
    /* …minus the copy button's own room as well (34px), or a long name pushes it
       onto a line of its own under the name */
    max-width: calc(100% - 108px);
    overflow: hidden; text-overflow: ellipsis;
    margin: 0 0 8px; padding-bottom: 4px;
  }
  .name-copy { margin-bottom: 8px; }
  /* Upright the picture is the biggest thing on the card — 84, not the 64 it
     was: the window is a person, and the face is what you are looking at. The
     name beside it keeps its own room (Troopers.fitOne scales a long one down). */
  .detail-photo { width: 84px; height: 84px; }
  .detail-photo.placeholder { font-size: 26px; }
  .detail-photo.placeholder svg { width: 38px; height: 38px; }
  /* Edit comes OUT of the header row and pins itself to the card's top-right
     corner. As a third flex item it took ~72px, which is exactly what Rank and
     Call sign needed to sit on one line. The name keeps clear of it via padding. */
  .detail-card { padding: 13px; position: relative; }
  .detail-head .detail-actions {
    position: absolute; top: 11px; right: 11px; margin: 0;
  }
  .detail-actions .btn { padding: 0 8px; height: 26px; font-size: 11px; }
  /* The History tabs: all the same width as each other, but the row only as
     wide as they need — the proportion a wide screen has, where .seg is
     inline-flex and stops at its content. Stretched to the full window (which
     it was for a while) they read as three big slabs. 84px is the widest label
     ("Last seen") plus its padding, so nothing is cut and none of them is wider
     than the rest. Four of them (a former trooper has a Details tab too) still
     fit a 375px phone. */
  .hist-seg { display: flex; flex-wrap: nowrap; width: fit-content; max-width: 100%; }   /* nowrap: the base rule wraps, and a fit-content row that wraps puts one tab on a line of its own */
  .hist-seg .seg-btn { flex: 1 1 auto; min-width: 84px; text-align: center; white-space: nowrap; }
  /* Rank + Call sign share a row; a K9 partner takes one of its own under them;
     Badge Number + Class share the next (or sit straight under Rank + Call sign
     when there is no dog). No value breaks mid-word.
     Every value has to fit its chip on one line AND two chips have to fit one
     row (Rank + Call sign, Badge Number + Class) in a ~230px column. "BADGE
     NUMBER #421" alone is 133px at the wide-screen sizes, so the label, the gap
     and the padding all come down a step here. */
  .dm-chip { white-space: nowrap; padding: 3px 7px; gap: 5px; font-size: 12.5px; }
  .dm-chip .dm-k { font-size: 9px; letter-spacing: .2px; }
  /* A grid, not a wrapping flex row: `flex-basis: 100%` put the K9 partner on a
     line of its own but also stretched its pill across the whole width. Two
     content-width columns, and the partner spans both while staying as wide as
     what is in it. */
  /* Each chip stays exactly as wide as what is in it and wraps to the next line
     when two of them do not fit — no stretching, no ellipsis. The K9 partner has
     its own .dm-row in the markup (js/troopers.js), which is what puts it on a
     line of its own without being stretched across it. */
  .dm-row { gap: 6px; }
  .dm-chip { max-width: 100%; }

  .pcard > .pcard-body:not(.pcard-row2) {
    display: grid; grid-template-columns: 1fr auto;
    grid-template-areas: "case pin" "date rating";
    align-items: start; gap: 8px 12px;
  }
  .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(1) { grid-area: case; }
  .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(2) { grid-area: date; }
  .pcard > .pcard-body:not(.pcard-row2) > .grow { display: none; }
  .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(4) {
    grid-area: rating; align-items: flex-end; text-align: right;
  }
  .pcard > .pcard-body:not(.pcard-row2) > .pcard-locwrap {
    grid-area: pin; justify-self: end; align-self: start;
  }

  /* --- History entries, upright ---
     A phone column is too narrow for "(A-6) Trp. Hall → (A-9) Sgt. Hall" on one
     line, and letting it wrap broke it wherever the words happened to run out —
     often mid-state, so neither the old nor the new one read as a unit. Here the
     two states get a line each and the arrow turns to point down between them.
     Same markup as the wide screen; only the direction changes. */
  /* --- the page scrolls below its header, not behind it ---
     The header is no longer part of what scrolls: the view is a fixed-height
     column, the header takes the top of it, and everything else lives in a box
     that scrolls on its own starting at the header's bottom edge. Nothing can
     appear above the header because nothing scrolls up there any more — which
     is what three attempts at COVERING that strip could not achieve.
     The box is built by App.wrapScroller after each render.
     The page itself is already nailed to the screen by the base `html, body`
     rule, so there is no page scroll left to carry anything anywhere. What is
     left to do here is hand the leftover height down — body → main → view → the
     box — and make the box the only thing in the chain that scrolls. */
  main { display: flex; flex-direction: column; min-height: 0; }
  /* The Change panel was sticky because the page scrolled under it. It does not
     any more: the panel takes the top of the column, main takes what is left and
     the cards scroll inside the view. Static keeps it out of the scroll for
     real, instead of it merely holding its ground while the page moves. */
  #switchPanel { position: static; flex: 0 0 auto; }
  .view {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column; overflow: hidden;
    padding-bottom: 0;
  }
  /* relative, NOT static: the header's card is a ::before pinned to its four
     edges, so the header has to stay the box that pseudo-element is measured
     against. Made static, the card jumped out to the viewport, grew to the full
     height of the screen and painted its opaque surface over the whole view —
     which is why the content measured perfectly and still never appeared. */
  .view-head { position: relative; flex: 0 0 auto; }
  .view-scroll {
    flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative;   /* relative anchors the phone sheet, see .modal-body */
    /* the gap the floating rail leaves under itself, so the last card clears it */
    padding-bottom: calc(var(--rail-gap) + 46px);
    -webkit-overflow-scrolling: touch;
  }

  /* ---- with a drop-in window over the page, the page header is GONE ----
     Normally the header sits above the scrolling box and holds its ground — which is right when it
     is the top of the screen. With the Trooper Sighting / Call Sign / K9 Partner window standing above
     it, it is the top of nothing: it named a page you are not looking at, and it cost a card row.
     First it was made to scroll away with the search box; Rick's answer on 2026-09-06 is that it
     should not be there at all — the page starts at the search box. `display: none` and not
     `visibility`, so the row it stood in goes with it.
     Named by id — the wide-screen rules for these use one, and a bare class loses to them. */
  body.switch-active #view-troopers > .view-head,
  body.switch-active #view-former > .view-head { display: none; }

  /* --- Notes, upright ---
     The panel takes the whole screen next to the rail here, so what is behind it
     is a page you cannot reach but could still scroll and still half see through
     the gaps. It holds still and it goes away, the same as a full-screen window.
     The rail stays: the panel starts to the right of it and it is still the way
     out of this view. */
  body.notes-active main, body.notes-active .under-rail,
  body.notes-active .notes-fab { visibility: hidden; }

  /* --- the pursuit card's second band, upright ---
     Its sections do not shrink, so a row of six Event Tags measured 400px inside
     a 303px card and simply ran out past the edge. Letting them shrink is all it
     takes: .pcard-chips already wraps, it just never had a reason to. */
  .pcard-row2 .pcard-sec { flex-shrink: 1; min-width: 0; }

  /* --- the Scanner's master controls, upright ---
     Play All / Mute All / Reload on one line. At their natural widths the three
     come to ~364px against the ~263 the panel has, so Reload dropped to a row of
     its own. `flex: 1 1 0` makes them share what there is instead of asking for
     what they would like, and the volume slider takes a full-width row under
     them rather than competing for the same one. */
  #scanAll, #scanMute, #scanReload {
    flex: 1 1 0; min-width: 0; padding: 0 6px; font-size: 11px; gap: 5px;
    white-space: nowrap; overflow: hidden;
    /* the same height as Add Feed in the header above — they were 22 and 18
       against its 28, which read as three smaller, lesser buttons */
    height: 28px;
  }
  #scanAll svg, #scanMute svg, #scanReload svg { flex-shrink: 0; }
  .scan-master-vol { flex: 0 0 100%; width: 100%; }

  /* --- the Watch window's Pictures tab, upright ---
     Two across, and the pair fills the pane exactly. As a wrapping row of fixed
     150px thumbnails only two fitted anyway, but whatever the row could not use
     was left over on the right — so the pictures sat off-centre in a window that
     has the same padding on both sides. Two 1fr columns cannot do that. */
  .wt-photos {
    display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 10px; max-width: none;
  }
  .wt-photos .photo-thumb { width: 100%; height: auto; aspect-ratio: 1; }

  .hist-note { white-space: nowrap; }
  .hn-side { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  /* centred, so it sits between the two states rather than hanging off the left
     edge under the first one */
  .hn-arrow { display: block; line-height: 1.1; text-align: center; }
  .hn-arrow::before { content: '↓'; margin: 0; }
  /* no hover on a touch screen — the note button stays legible on its own */
  .hist-note-btn { opacity: .75; }

  /* ===== Trooper cards, upright =====
     Nothing left to set. The card is the desktop card and Troopers.fitGrid scales it to the
     column, so the photo, the type, the mark in the corner and the "last seen" date all keep the
     proportions they have on a wide screen (Rick, 2026-09-06). What stood here was a long list of
     per-element phone sizes, each one an answer to "this looks wrong now" — the photo raised to
     92px to fill a card it no longer filled, the type raised to match the photo, the date shrunk
     to fit what was left over. They were all the same problem, and the scale is the one answer. */
}

/* ===== Phones, landscape =====
   Same width rules apply (the media query above still matches on most phones
   turned sideways); what changes is that vertical space is the scarce one. */
@media (max-height: 500px) and (orientation: landscape) {
  /* Sideways the rail is ~354px tall and the seven tabs plus four footer buttons
     do not always fit. The TABS scroll; the footer (theme, settings, admin, sign
     out) stays put at the bottom — it is the one part you must always be able to
     reach. The rail itself no longer scrolls, or the footer would scroll away
     with them. */
  .sidebar, .sidebar.collapsed { overflow: hidden; }
  .sidebar .side-nav {
    flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
    scrollbar-width: none;
  }
  .sidebar .side-nav::-webkit-scrollbar { display: none; }
  .sidebar .side-head, .sidebar.collapsed .side-head { flex-shrink: 0; }
  .sidebar { padding: 6px 6px; gap: 1px; }
  /* Same emblem as upright, in the same spot: centred on the icons under it, and
     the padding above and below it equal so it sits halfway between the top edge
     and the first nav icon. Sideways the rail is short, so it scrolls if it has
     to — that is better than a 20px badge nobody can make out. */
  .sidebar .side-head, .sidebar.collapsed .side-head { padding: 7px 0; margin-bottom: 0; }
  .sidebar .brand-emblem { width: 42px; height: 42px; }
  .sidebar .nav-btn, .sidebar.collapsed .nav-btn,
  .sidebar .nav-btn.nav-sub, .sidebar.collapsed .nav-btn.nav-sub { padding: 4px 0; }
  .sidebar .nav-ico svg { width: 16px; height: 16px; }
  .sidebar .side-foot { gap: 3px; padding: 4px 0 0; }
  .side-foot .icon-btn { height: 25px; }
  .side-foot .icon-btn svg { width: 14px; height: 14px; }
  /* Sticky costs a third of the screen here, but it must stay POSITIONED: the
     header's background card is an absolutely placed ::before, and with `static`
     it escaped to the nearest positioned ancestor and painted a huge grey panel
     behind the whole view. */
  /* The card still starts 18px down, level with the rail — only the room under
     the text comes off. It STAYS sticky here: the header follows the page down
     like it does upright. (`position: static` is the one thing it must never be —
     its background card is an absolutely placed ::before and would escape to the
     nearest positioned ancestor, painting a grey slab behind the whole view.) */
  .view-head { padding: 24px 17px 8px; position: sticky; top: 0; }
  .view-head::before { inset: 18px 10px 4px; }
  /* The four that carry a wide-screen recess are written with an #id in the phone
     block above, and an id beats a class however far down it stands — so they
     kept the upright 12px/6px here while the other three took the 8px/4px on the
     line above, and their headings sat 2px higher than the rest. Named again so
     all seven land on the same line sideways. */
  #view-pursuits .view-head, #view-history .view-head,
  #view-stats .view-head, #view-map .view-head { padding: 24px 17px 8px; }
  #view-pursuits .view-head::before, #view-history .view-head::before,
  #view-stats .view-head::before, #view-map .view-head::before { inset: 18px 10px 4px; }
  .view-head h1 { font-size: 19px; }
  /* sideways there IS room for a row — search back beside the filter, so the
     list starts higher up */
  .filters { flex-direction: row; flex-wrap: wrap; align-items: center; }
  /* Same search width as upright — and upright means the SHORT edge of the phone,
     which sideways is 100vh. 213px is what upright takes off it: the 67px rail,
     20px of view padding and the 126px sort pair + gap. */
  :root { --ph-search: calc(100vh - 213px); }
  .search-wrap, #view-pursuits .search-wrap { flex: 0 0 var(--ph-search); width: var(--ph-search); min-width: 0; }
  .search-field { width: 100%; height: 34px; }
  /* Troopers' letter filter is the same block as upright — same shape, same
     width — instead of being stretched across the whole row, with the eye beside
     it either way. 129px is what upright takes off the screen width: the 67px
     rail, 20px of view padding, the eye's 34px column and its 8px gap. On a real
     phone 100vh here IS the upright width, so the two come out identical. */
  #view-troopers .filters { grid-template-columns: calc(100vh - 129px) 34px; justify-content: start; }
  /* Same three alignments sideways: the eye's right edge and width, the header's own 18px/4px */
  #view-troopers .head-side, #view-former .head-side {
    right: 20px; width: 34px; top: 18px; bottom: 4px;
  }
  /* …and the header card has to stop before it. The card's own `right: 52px` is written with an
     #id (see the desktop rule) so it survives every phone block, but sideways the box sits 10px
     further in than it does upright — 20 + 34 = 54, two pixels PAST the card's edge, which is why
     the button read as stuck to the header with no gap at all. 62 puts the same 8px of air
     between them that the upright header has. */
  #view-troopers .view-head::before { right: 62px; }
  #view-troopers .view-head { padding-right: 80px; }
  .view-head .head-side-btn { width: 100%; height: 100%; }

  /* upright the sort pair fills whatever the search field leaves; sideways the
     row is wide and it would stretch to half the screen — back to its own size */
  #view-pursuits .sort-ctl { flex: 0 0 auto; }
  #view-pursuits .sort-ctl .seg { width: auto; }
  #view-pursuits .sort-ctl .seg-btn { flex: initial; padding: 0 12px; }
  .filters .spacer { display: block; flex: 1; }
  .view { padding-bottom: 24px; }
  /* The windows keep the ONE arrangement set up in the phone block above — same
     18px inset, same full height, same body-scrolls-inside-a-fixed-frame. This
     block used to give them a 6px inset, top alignment and a content-height cap
     of their own, which is why Administration, Watch and Pick Location each came
     out a different shape sideways while Add Pursuit stood still. Only the chrome
     is tightened here, because a sideways screen is short. */
  .modal-header { padding: 8px 12px; }
  .modal-header h2 { font-size: 15px; }
  .modal-body { padding: 10px 12px; }
  .settings-layout { min-height: 0; }
  /* Sideways the top row does not wrap, so the link field stops where the second
     star begins: Date 165 + 12 + Case Number 125 + 12 puts the rating at 314, and
     two stars are 50 more. Measured, not guessed — the row is built from fixed
     widths, so the number holds at any sideways size. */
  .modal-full-phone .full-video-field { width: 364px; max-width: 100%; }

  /* Sideways the Change and Notes panels are a right-hand column — and they no
     longer LIE ON the page: the view shrinks by exactly their width, so the
     header, the search row and the cards all stay visible beside them.
     The column is as wide as the panel is when the phone is upright, and upright
     means the phone's SHORT edge, which sideways is 100vh: the full width minus
     the 67px rail and the 8px gap on each side. */
  :root { --ph-panel: calc(100vh - 91px); }
  .switch-panel, .switch-panel.notes-panel {
    /* the right inset for the same reason the rail carries the left one: turned
       the other way round, the notch is on THIS edge and the panel is what runs
       under it */
    top: 18px; bottom: 18px; right: calc(8px + env(safe-area-inset-right, 0px)); left: auto;
    /* a real height, not `auto` + a cap: the panel is exactly as tall as the rail
       beside it whether it holds two slots or a full pick-list */
    width: var(--ph-panel); height: calc(100dvh - 36px); max-height: calc(100dvh - 36px);
    overflow-y: auto;
  }
  /* on <main>, not on the views: each view sets its own padding-right through an
     #id, which would outrank anything written with classes here */
  body.notes-active main, body.switch-active main, body.codes-active main {
    /* the inset rides along: this replaces the padding-right that was carrying it,
       and the panel it makes room for has moved in by the same strip */
    padding-right: calc(var(--ph-panel) + 16px + env(safe-area-inset-right, 0px));
  }
  /* An opened note is the same window as the list it came from here too — same
     column, same width. (It used to take the whole screen beside the rail; the
     page is no longer hidden behind the panel, so there is nothing to gain.)
     `align-self: stretch`: body is a flex container and the panel inherits
     `align-self: flex-start`, which collapses a fixed box with top AND bottom
     set back to its content height in Chrome. */
  .switch-panel.notes-panel.notes-detail { align-self: stretch; }
  .switch-panel.notes-panel .note-open { flex: 1; min-height: 0; }
  .switch-panel.notes-panel .note-open-text { flex: 1; min-height: 120px; }

  /* Sideways the map is a short, wide strip: a scrollable popup body sitting on
     top of it swallowed every drag meant for the map, and reading a note in a
     42vh box was worse than just letting it be as tall as it is. */
  /* …and the whole popup is scaled down rather than re-sized piece by piece: the
     map strip is only ~370px tall here. `zoom` shrinks the box AND everything in
     it in one step, so the buttons and labels keep their proportions instead of
     spilling out of a box that got smaller around them. */
  .mapboxgl-popup-content {
    max-height: none; overflow: visible;
    zoom: 0.68; max-width: 250px !important;
  }

  /* the Notes button lives in the bottom-right corner — exactly where the Change
     panel is here, so it would sit under it doing nothing */
  body.switch-active .notes-fab { display: none; }

  /* With Notes open the content column is as narrow as it is upright, so the
     pursuit card takes its upright shape too: case + date stacked on the left,
     rating bottom-right, pin above it. (Same rules as the portrait block.) */
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) {
    display: grid; grid-template-columns: 1fr auto;
    grid-template-areas: "case pin" "date rating";
    align-items: start; gap: 8px 12px;
  }
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(1) { grid-area: case; }
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(2) { grid-area: date; }
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) > .grow { display: none; }
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) > .pcard-sec:nth-child(4) {
    grid-area: rating; align-items: flex-end; text-align: right;
  }
  body.notes-active .pcard > .pcard-body:not(.pcard-row2) > .pcard-locwrap {
    grid-area: pin; justify-self: end; align-self: start;
  }
}

/* ===== Phones, sideways: the page scrolls below its header too =====
   The same rebuild as upright, and here for the same reason — a page that can
   scroll is a page that can put something above the header. The chain is one
   link shorter sideways: body is a row, so the height goes screen → main →
   view → the box, with no panel above main to make room for.
   Its own block rather than a line in the landscape section above, because that
   section has no max-width and would take a short desktop window with it. This
   is the phone held sideways and nothing else.
   Both traps from the upright build apply unchanged: .view-head must stay
   POSITIONED (its card is an absolutely placed ::before that escapes to the
   viewport and paints over everything if the header goes static), and the map
   is never wrapped (App.wrapScroller skips it — its panel needs to be a direct
   child to take the leftover height). */
@media (max-height: 500px) and (max-width: 950px) and (orientation: landscape) {
  main { display: flex; flex-direction: column; min-height: 0; }
  .view {
    flex: 1; min-height: 0;
    display: flex; flex-direction: column; overflow: hidden;
    padding-bottom: 0;
  }
  /* relative, not sticky: there is nothing left for it to stick to */
  .view-head { position: relative; flex: 0 0 auto; }
  .view-scroll {
    flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; position: relative;   /* relative anchors the phone sheet, see .modal-body */
    /* the band the rail leaves along the bottom, so the last card clears it */
    padding-bottom: var(--rail-gap);
    -webkit-overflow-scrolling: touch;
  }
}

/* ===== Light-mode glow boost =====
   The shared glow rules were tuned against the dark canvas: a large negative spread keeps the halo
   tight, and a low alpha still reads against near-black. On the warm light page those same values
   wash out to almost nothing, so here the halos get a much smaller negative spread (they actually
   reach past the element) and more alpha. Scoped to light only — dark keeps its tuning.
   `html:not([data-theme="dark"])` also outranks the base rules, so position in the file is moot. */
html:not([data-theme="dark"]) .tcard:hover {
  box-shadow: 0 12px 34px -4px color-mix(in srgb, var(--tc-blue) 55%, transparent);
}
html:not([data-theme="dark"]) .tcard.fired:hover {
  box-shadow: 0 12px 34px -4px color-mix(in srgb, var(--danger) 52%, transparent);
}
html:not([data-theme="dark"]) .tcard.k9card:not(.fired):hover {
  box-shadow: 0 12px 34px -4px color-mix(in srgb, var(--k9-purple) 52%, transparent);
}
html:not([data-theme="dark"]) .pcard:hover {
  box-shadow: 0 10px 30px -4px color-mix(in srgb, var(--oc, var(--accent)) 50%, transparent);
}
/* avatar rings — the most visible "glow" on the page, so they carry the most alpha */
html:not([data-theme="dark"]) .tcard:hover .t-av {
  box-shadow: 0 6px 20px -4px color-mix(in srgb, var(--tc-blue) 62%, transparent),
              0 0 0 6px color-mix(in srgb, var(--tc-blue) 34%, transparent);
}
html:not([data-theme="dark"]) .tcard.icp .t-av {
  box-shadow: 0 6px 20px -4px color-mix(in srgb, var(--icp-teal) 55%, transparent),
              0 0 0 5px color-mix(in srgb, var(--icp-teal) 24%, transparent);
}
html:not([data-theme="dark"]) .tcard.icp:hover .t-av {
  box-shadow: 0 8px 26px -4px color-mix(in srgb, var(--icp-teal) 72%, transparent),
              0 0 0 6px color-mix(in srgb, var(--icp-teal) 32%, transparent);
}
html:not([data-theme="dark"]) .t-av.k9 {
  box-shadow: 0 6px 20px -4px color-mix(in srgb, var(--k9-purple) 55%, transparent),
              0 0 0 5px color-mix(in srgb, var(--k9-purple) 24%, transparent);
}
html:not([data-theme="dark"]) .tcard.k9card:hover .t-av.k9 {
  box-shadow: 0 8px 26px -4px color-mix(in srgb, var(--k9-purple) 72%, transparent),
              0 0 0 6px color-mix(in srgb, var(--k9-purple) 32%, transparent);
}
html:not([data-theme="dark"]) .tcard.fired .t-av,
html:not([data-theme="dark"]) .tcard.k9card.fired .t-av {
  box-shadow: 0 6px 20px -4px color-mix(in srgb, var(--danger) 55%, transparent),
              0 0 0 5px color-mix(in srgb, var(--danger) 24%, transparent);
}
html:not([data-theme="dark"]) .tcard.fired:hover .t-av,
html:not([data-theme="dark"]) .tcard.k9card.fired:hover .t-av {
  box-shadow: 0 8px 26px -4px color-mix(in srgb, var(--danger) 72%, transparent),
              0 0 0 6px color-mix(in srgb, var(--danger) 32%, transparent);
}
/* buttons */
html:not([data-theme="dark"]) .btn.primary:hover {
  box-shadow: 0 0 0 1px var(--btn-brand), 0 6px 18px -2px color-mix(in srgb, var(--btn-brand) 55%, transparent);
}
html:not([data-theme="dark"]) .btn.success:hover {
  box-shadow: 0 0 0 1px var(--success), 0 6px 18px -2px color-mix(in srgb, var(--success) 55%, transparent);
}
html:not([data-theme="dark"]) .btn.danger:hover {
  box-shadow: 0 0 0 1px var(--danger), 0 6px 18px -2px color-mix(in srgb, var(--danger) 55%, transparent);
}
/* light used to add a drop shadow back here; the button carries no shadow in
   either theme now, so only the 1px ring that separates it from a pale page stays */
html:not([data-theme="dark"]) .notes-fab:hover {
  box-shadow: 0 0 0 1px var(--fab-bg);
}

/* A handful of spots hardcode the dark-tuned brand hues (#14b8a6 ICP, #8957e5 K9) as TEXT. Those
   were picked to glow on the dark canvas and only reach 2.1–3.6:1 on this one, so light routes them
   through the deepened tokens instead. Light-only: the raw hues stay exactly as they are in dark. */
html:not([data-theme="dark"]) .jbtn-icp { color: var(--icp-teal); }
html:not([data-theme="dark"]) .t-cs-label.icp {
  background: color-mix(in srgb, var(--icp-teal) 11%, transparent); color: var(--icp-teal);
}
html:not([data-theme="dark"]) .t-cs-label.unit {
  background: color-mix(in srgb, var(--unit-orange) 11%, transparent); color: var(--unit-orange);
}
html:not([data-theme="dark"]) .jbtn-k9 { color: var(--k9-purple); }
html:not([data-theme="dark"]) .t-cs-label.k9 {
  background: color-mix(in srgb, var(--k9-purple) 15%, transparent); color: var(--k9-purple);
}
html:not([data-theme="dark"]) .hist-type.type-k9 {
  background: color-mix(in srgb, var(--k9-purple) 15%, transparent); color: var(--k9-purple);
}
/* the Rank pill's own 22% amber tint was darkening its background enough to eat the contrast */
html:not([data-theme="dark"]) .hist-type.type-rank {
  background: color-mix(in srgb, var(--warn) 10%, transparent); color: #8a4008;
}
/* the history type pill puts the accent on its own 18% tint — deep sister + a lighter tint so the
   uppercase 10.5px label clears 4.5:1 */
html:not([data-theme="dark"]) .hist-type {
  background: color-mix(in srgb, var(--accent) 13%, transparent); color: var(--accent-deep);
}

/* ===== Login / register / waiting screen =====
   Shown before App.init() runs, so it cannot rely on anything the app sets up
   later — Auth applies the stored theme itself before painting this. */
.auth-screen {
  position: fixed; inset: 0; z-index: 500;
  display: flex; align-items: center; justify-content: center; padding: 24px;
  background-color: var(--bg);
  background-image: var(--app-grad);
  background-attachment: fixed;
  overflow-y: auto;
}
/* the app chrome lives outside #authScreen, so hide it rather than let it show
   through around the card (the sidebar is sticky and would peek out on tall windows) */
body.auth-open .sidebar,
body.auth-open main,
body.auth-open .notes-fab { display: none; }

.auth-card {
  width: 100%; max-width: 380px;
  display: flex; flex-direction: column; gap: 18px;
  padding: 32px 30px 28px;
  background: var(--chrome-bg); border: 1px solid var(--chrome-brd); border-radius: 18px;
}
.auth-brand { display: flex; align-items: center; gap: 13px; justify-content: center; }
.auth-emblem { width: 54px; height: 54px; object-fit: contain; }
.auth-brand-text { display: flex; flex-direction: column; line-height: 1.1; }
.auth-brand-text strong {
  font-size: 17px; font-weight: 800; color: var(--text); letter-spacing: .2px; line-height: 1.12;
}
.auth-brand-text span {
  font-size: 10px; color: var(--text-soft); letter-spacing: 1.6px;
  text-transform: uppercase; font-weight: 700; margin-top: 4px;
}

/* same segmented control as the rest of the app, stretched to the card width */
.auth-seg {
  display: flex; border: 1px solid var(--glass-brd); border-radius: 12px; overflow: hidden;
  padding: 3px; gap: 3px; background: var(--surface);
}
.auth-seg .seg-btn { flex: 1; text-align: center; }

.auth-form { display: flex; flex-direction: column; gap: 13px; }
.auth-form .hint { margin-top: -4px; }
.auth-submit { width: 100%; justify-content: center; margin-top: 3px; }
.auth-submit:disabled { opacity: .6; pointer-events: none; }

.auth-link {
  border: none; background: none; color: var(--text-soft); cursor: pointer;
  font-size: 12px; font-weight: 500; padding: 2px; text-align: center;
}
.auth-link:hover { color: var(--accent-deep); text-decoration: underline; }

/* message line: reserves no space until it has something to say */
.auth-msg {
  display: none; font-size: 12.5px; font-weight: 500; line-height: 1.45;
  text-align: center; color: var(--text-soft);
}
.auth-msg.show { display: block; }
.auth-msg.err { color: var(--danger); }

/* username screen — same card, but it is a form, so the copy sits left-aligned above it */
.auth-name-h { margin: 0; font-size: 17px; font-weight: 800; color: var(--text); }
.auth-name-p { margin: -6px 0 2px; font-size: 13px; color: var(--text-soft); line-height: 1.5; }
.auth-pending { display: flex; flex-direction: column; align-items: center; gap: 9px; text-align: center; }
.auth-pending-ico {
  display: inline-flex; align-items: center; justify-content: center;
  width: 58px; height: 58px; border-radius: 50%; margin-bottom: 3px;
  color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
}
.auth-pending h2 { margin: 0; font-size: 17px; font-weight: 800; color: var(--text); }
.auth-pending p { margin: 0; font-size: 13px; color: var(--text-soft); line-height: 1.5; }
.auth-pending-who { font-weight: 700; color: var(--text) !important; }

/* ===== Sync failure banner =====
   The one message in this app that must not be missable: Utils.toast is a
   no-op, so a rejected write would otherwise leave the screen showing data the
   database never accepted. Sits above everything and stays until a push
   succeeds. */
.sync-err {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 600; max-width: min(560px, calc(100vw - 32px));
  display: flex; flex-direction: column; gap: 3px;
  padding: 12px 18px; border-radius: 12px;
  background: color-mix(in srgb, var(--danger) 16%, var(--surface));
  border: 1px solid var(--danger); color: var(--text);
  box-shadow: 0 0 22px 2px color-mix(in srgb, var(--danger) 45%, transparent);
}
.sync-err strong { font-size: 13.5px; font-weight: 700; color: var(--danger); }
.sync-err span { font-size: 12px; color: var(--text-soft); word-break: break-word; }

/* ===== Role-driven UI =====
   body.no-write  = viewer (may read, may not change anything)
   body.no-delete = anyone but the owner

   This only stops the UI from OFFERING actions that would be rejected anyway —
   the real boundary is row-level security in Postgres (supabase/03_data.sql).
   Hiding is done in CSS rather than in each render function so it survives every
   re-render on its own; behaviour that CSS cannot reach (dragging, click-to-edit,
   the inline history notes) is blocked in JS as well. */
body.no-write #addPursuitBtn,
body.no-write #addCardBtn,
body.no-write #switchBtn,
body.no-write #rehireBtn,
body.no-write #scanAdd,
body.no-write #d-edit,
body.no-write #dd-edit,
body.no-write .notes-fab,
body.no-write .photo-add,
body.no-write .photo-rm,
body.no-write .set-cat[data-cat="backup"],
body.no-write .set-cat[data-cat="placeholders"],
body.no-write .set-panel[data-panel="backup"],
body.no-write .set-panel[data-panel="placeholders"],
body.no-delete .set-cat[data-cat="danger"],
body.no-delete .set-panel[data-panel="danger"] { display: none !important; }

/* the inline "Add Notes" field on a history row reads as an empty box when it is
   not editable, so drop it entirely rather than leave a dead placeholder */
body.no-write .hist-memo { visibility: hidden; pointer-events: none; }
/* a scanner card is clickable only because clicking opens the edit dialog */
body.no-write .scanf { cursor: default; }

/* A picture whose file is gone from storage. The record still holds the path,
   so the thumbnail slot stays — with a stand-in instead of a broken image, and
   its remove button still beside it so the dead entry can be cleared. */
.photo-thumb.photo-missing {
  display: flex; align-items: center; justify-content: center;
  color: var(--text-soft); opacity: .55;
  background: var(--surface-2);
  border: 1px dashed color-mix(in srgb, var(--text-soft) 45%, transparent);
  cursor: default;
}

/* ===== Deletion requests ===== */
.req-ask-lead { margin: 0 0 14px; font-size: 13px; color: var(--text-soft); line-height: 1.5; }
.req-ask .field + .field { margin-top: 12px; }
.req-ask-actions { display: flex; justify-content: flex-end; margin-top: 16px; }

.req-list { display: flex; flex-direction: column; gap: 9px; margin-top: 10px; }
.req-row {
  display: flex; align-items: flex-start; gap: 12px; padding: 12px 14px;
  background: var(--surface-2); border: 1px solid var(--glass-brd); border-radius: 12px;
}
.req-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.req-label { font-size: 13.5px; font-weight: 700; color: var(--text); }
.req-meta { font-size: 11.5px; color: var(--text-soft); }
/* The reason is the whole case for saying yes — it was set in the same faded
   grey as the timestamps beside it and read as one more piece of bookkeeping.
   Full text colour in the same sunken block the record's own fields sit in
   (.req-detail), but outlined in the danger colour: of everything in the row it
   is the only part a person wrote, and it argues for the delete. */
.req-reason {
  font-size: 13px; color: var(--text); margin-top: 6px; white-space: pre-wrap;
  padding: 9px 11px; border-radius: 9px;
  background: var(--surface); border: 1px solid var(--danger);
  /* A reason is free text an admin typed, so it can be one word longer than the
     row is wide. Without these it pushed the field — and the Administration
     window with it — out past its own edge instead of wrapping. */
  min-width: 0; overflow-wrap: anywhere; word-break: break-word;
}
/* Names the field, the way .req-dl names each of the record's own — but inline,
   so a short reason sits beside the word and only a long one wraps under it. */
.req-reason-lbl {
  font-size: 11.5px; font-weight: 600; color: var(--danger);
  margin-right: 7px;
}
.req-actions { display: flex; gap: 7px; flex-shrink: 0; }

/* viewers never reach Settings' admin categories; this one is admin+ like the
   others, and the queue itself is empty for them anyway (no read policy) */
body.no-write .set-cat[data-cat="requests"],
body.no-write .set-panel[data-panel="requests"] { display: none !important; }

/* ===== Change history =====
   Shares the deletion-request row shell (.req-row / .req-check / .req-detail)
   so both lists read as one thing; only the operation badge is its own. */
.aud-head { display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }
.aud-op {
  flex-shrink: 0; padding: 3px 9px; border-radius: 999px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
}
.aud-insert { color: var(--success); background: color-mix(in srgb, var(--success) 15%, transparent); }
/* orange, so a change is distinct from a create (green) and a delete (red) */
.aud-update { color: #c2691a; background: color-mix(in srgb, #c2691a 16%, transparent); }
html[data-theme="dark"] .aud-update { color: #e0913f; background: color-mix(in srgb, #e0913f 15%, transparent); }
.aud-delete { color: var(--danger); background: color-mix(in srgb, var(--danger) 15%, transparent); }
/* keeps rows without an Undo aligned with the ones that have a checkbox */
.aud-nopick { width: 18px; flex-shrink: 0; }

/* the two dates under an account read as answers, their labels as questions */
.usr-row .req-meta b { color: var(--text); font-weight: 700; }

/* "this is getting long" — a notice, not a warning: nothing is wrong, there is
   just more of it than is useful. Amber, and it names the cost of clearing. */
.aud-full {
  display: flex; align-items: flex-start; gap: 8px; margin-top: 12px;
  padding: 9px 12px; border-radius: 11px; font-size: 12.5px; line-height: 1.45;
  color: var(--admin-glow);
  background: color-mix(in srgb, var(--admin-glow) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--admin-glow) 35%, transparent);
}
.aud-full svg { flex-shrink: 0; margin-top: 1px; }
.aud-full b { font-weight: 800; }

/* --- the one bar above the list ---
   Owner: search + op filters on the left, Undo selected and Clear on the right,
   all inside the same panel the bulk buttons used to have to themselves.
   Admin: `boxed` is never set, the bulk half is never built, and what is left is
   a bare filter row — no empty panel around two controls. */
.aud-bar {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  margin: 12px 0 10px;
}
.aud-bar.boxed {
  padding: 9px 13px; background: var(--surface-2);
  border: 1px solid var(--glass-brd); border-radius: 11px;
}
.aud-filter { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
.aud-bulk-actions { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.aud-ops { display: flex; gap: 6px; flex-wrap: wrap; }
/* The buttons ARE the badge from the rows below — same .aud-op shell, same
   colour class — so what you press and what you are looking for read as one
   thing. Off, it is dimmed and outlined; on, it fills in, exactly like the badge
   on a row. */
/* An ACTIVE button is the badge, exactly — same tint, same text colour, nothing
   swapped in and nothing added on top. What marks it as chosen is that it is at
   full strength with a ring in its own colour; the other two are faded back.
   The fill itself comes from .aud-insert / .aud-update / .aud-delete above, so
   the two can never drift apart. */
.aud-op-btn {
  border: 1px solid transparent; cursor: pointer; font-family: inherit;
  opacity: .42;
  transition: opacity .14s ease, border-color .14s ease;
}
.aud-op-btn:hover { opacity: .78; }
.aud-op-btn.active { opacity: 1; border-color: currentColor; }

/* --- an entry that has been put back --- */
/* Dimmed rather than hidden: the change still happened, and the record of it is
   the point of this window. The badge is what says it no longer stands. */
.aud-undone .req-label, .aud-undone .req-detail { opacity: .6; }
.aud-restored {
  display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0;
  padding: 3px 9px; border-radius: 999px;
  font-size: 10.5px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
  color: var(--accent); background: var(--accent-soft);
}
.aud-restored svg { width: 12px; height: 12px; }
.aud-restored-meta { color: var(--accent); opacity: .85; }

/* bulk selection in the deletion-request queue */
.req-bulk {
  display: flex; align-items: center; gap: 14px; margin-top: 12px;
  padding: 9px 13px; background: var(--surface-2);
  border: 1px solid var(--glass-brd); border-radius: 11px;
}
.req-check { display: flex; align-items: center; gap: 7px; cursor: pointer; font-size: 12.5px; color: var(--text-soft); }
/* the input itself is sized and hidden further down, where the custom .req-box
   replaces it — nothing to style here */
.req-bulk .btn { margin-left: auto; }

/* viewers never see the Administration button; its panels are role-picked anyway */
body.no-write #adminBtn { display: none !important; }

/* ===== Deletion requests: custom checkbox + record preview ===== */
/* the native box ignores border-radius and accent sizing, so it is hidden and a
   styled square takes its place — the input stays for keyboard and a11y */
.req-check { position: relative; user-select: none; }
.req-check input { position: absolute; opacity: 0; width: 0; height: 0; }
.req-box {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; flex-shrink: 0; border-radius: 6px;
  border: 1.5px solid color-mix(in srgb, var(--text-soft) 55%, transparent);
  background: var(--surface); color: transparent;
  transition: background .13s, border-color .13s, color .13s;
}
.req-check:hover .req-box { border-color: var(--accent); }
.req-check input:checked + .req-box {
  background: var(--accent); border-color: var(--accent); color: var(--accent-ink);
}
.req-check input:focus-visible + .req-box {
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 35%, transparent);
}
.req-check-lbl { margin-left: 1px; }
/* Administration → Pages: one collapsible group per page, each holding a switch
   per role. Bigger type than the selection checkboxes these borrow their look
   from — this is a setting you read, not a row you tick off a list. */
.pg-roles { display: flex; flex-direction: column; gap: 10px; margin: 4px 0 10px; }
.pg-roles .req-check { font-size: 13.5px; color: var(--text); }
/* the page's own icon in front of its name, the way the open rail shows it,
   with the drag handle ahead of both */
.pg-name { display: inline-flex; align-items: center; gap: 9px; }
.pg-name svg { opacity: .85; flex-shrink: 0; }
/* …and the name stays PUT beside the grip. `.set-grp > summary span { flex: 1 }`
   above is a descendant selector, so it caught the grip too and blew it up to
   fill the row — which pushed the icon and the page's name all the way over to
   the chevron on the right. Only .pg-name is allowed to take the leftover width. */
.pg-name > .pg-grip { flex: 0 0 auto; }
/* Held down to start a drag, a phone would otherwise select the page's name and
   raise the copy/paste bubble over the row being dragged. */
.pg-row {
  cursor: grab;
  user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
}
.pg-row:active { cursor: grabbing; }
.pg-row.pg-dragging { opacity: .45; }
/* The grip is the handle, and on a phone it is the ONLY way in: `touch-action:
   none` hands its gestures to the drag code instead of scrolling the panel,
   which is why pressing anywhere else on the row still just opens it. */
.pg-grip {
  display: inline-flex; align-items: center; opacity: .45;
  touch-action: none; padding: 4px; margin: -4px 0 -4px -4px;
}
.pg-grip svg { display: block; }

/* what the record actually holds — read live, so it reflects edits since filing */
.req-detail {
  display: flex; flex-direction: column; gap: 2px; margin-top: 8px;
  padding: 9px 11px; border-radius: 9px;
  background: var(--surface); border: 1px solid var(--glass-brd);
}
.req-dl { display: flex; gap: 9px; align-items: baseline; font-size: 11.5px; }
.req-dl span { color: var(--text-soft); min-width: 108px; flex-shrink: 0; }
.req-dl b { color: var(--text); font-weight: 600; word-break: break-word; }
.req-gone { color: var(--danger); font-size: 12px; }
/* a picture request shows the picture — nothing else describes one */
.req-pic { align-items: flex-start; }
.req-pic img { max-width: 190px; max-height: 130px; border-radius: 8px; display: block; cursor: zoom-in; }

/* number box beside a slider (Placeholders) */
/* the generic field styling further up wins on specificity, so this one names
   its row as well — otherwise the box keeps the 11px padding of a normal input */
.troopmax-field .set-slider-num { height: 30px; padding: 0 6px; font-size: 13px; }
.set-slider-num {
  width: 62px; flex-shrink: 0; text-align: center;
  padding: 3px 6px; border-radius: 8px; font-size: 12.5px; font-weight: 700; line-height: 1.5;
  background: var(--surface-2); color: var(--text);
  border: 1px solid var(--glass-brd);
}
/* Clicking it selects what is in there (see App.buildSettings), so it has to
   LOOK like a field you type over: the accent ring and the selection colour say
   "this whole value is about to be replaced". */
.set-slider-num:focus {
  outline: none;
  border-color: color-mix(in srgb, var(--accent) 65%, var(--border));
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
  background: var(--surface);
}
.set-slider-num::selection { background: color-mix(in srgb, var(--accent) 40%, transparent); color: var(--text); }

/* Number box beside a slider. No spinner at all: the native one is tiny,
   hover-only and different in every browser, and custom arrow buttons were
   tried and dropped — the slider already covers stepping. */
.set-slider-num { -moz-appearance: textfield; appearance: textfield; }
.set-slider-num::-webkit-outer-spin-button,
.set-slider-num::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

/* ===== Accounts =====
   Same row shell again (.req-row / .aud-head / .aud-op); only the role badge
   colours and the one-line "what this role may do" belong to this list. */
/* One colour per rung of the ladder: red at the top, orange for the admins,
   green for read-only. An account still waiting is blue and blinks — it is the
   only one of the four that is asking for something. */
.usr-owner  { color: var(--danger); background: color-mix(in srgb, var(--danger) 15%, transparent); }
.usr-admin  { color: #c2691a; background: color-mix(in srgb, #c2691a 16%, transparent); }
html[data-theme="dark"] .usr-admin { color: #e0913f; background: color-mix(in srgb, #e0913f 15%, transparent); }
.usr-viewer { color: var(--success); background: color-mix(in srgb, var(--success) 15%, transparent); }
.usr-pending {
  color: #a07407; background: color-mix(in srgb, #d9a520 20%, transparent);
  animation: usrWait 2.6s ease-in-out infinite;
}
html[data-theme="dark"] .usr-pending { color: #f0c94a; background: color-mix(in srgb, #f0c94a 15%, transparent); }
@keyframes usrWait { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
/* someone who reads with motion turned down gets the colour, not the blink */
@media (prefers-reduced-motion: reduce) { .usr-pending { animation: none; } }
/* Who is on the site right now (Supabase Realtime presence, js/presence.js).
   The last column of the row, and its own line in the account's window — it is
   in the flow either way now, not pinned to a corner. */
.usr-row { position: relative; }
.usr-live {
  display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0;
  font-size: 10px; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
}
.usr-live.on { color: var(--success); }
.usr-live.off { color: var(--text-soft); }
.usr-dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
.usr-live.on .usr-dot { box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent); }
/* One line per person: badge, name, presence — everything else is in the window
   the row opens. Three columns on one baseline, so a long list stays readable
   as a list rather than as a stack of cards. */
.usr-row {
  align-items: center; gap: 11px; padding: 9px 12px;
  cursor: pointer; transition: border-color .14s, background .14s;
}
.usr-row:hover { background: var(--surface); }
.usr-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the badge is a fixed column so the names line up down the list */
.usr-row .aud-op { flex-shrink: 0; min-width: 74px; text-align: center; }
/* the count and the role filter share the line above the list */
.usr-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin: 0 2px 8px;
}
/* how many of them are here right now — the question the list is usually opened for */
.usr-count { font-size: 12px; font-weight: 700; color: var(--text-soft); }
.usr-count b { color: var(--success); font-size: 13.5px; }
/* Each chip lights in its own role's colour when on, so the filter reads as the
   same four colours the badges below it wear. None on = everyone, which is why
   there is no "All" chip to get out of step with the others. */
.usr-filter { display: flex; gap: 5px; flex-wrap: wrap; }
.usr-filter-btn {
  border: 1px solid var(--glass-brd); background: var(--surface); color: var(--text-soft);
  cursor: pointer; padding: 4px 11px; border-radius: 999px;
  font-family: inherit; font-size: 11px; font-weight: 700; letter-spacing: .3px;
  transition: background .14s, color .14s, border-color .14s;
}
.usr-filter-btn:hover { color: var(--text); border-color: var(--border); }
/* viewer/admin take their fill from the role switch's own rules further down;
   only the border has to be named here, since those two never needed one */
.usr-filter-btn.usr-seg-viewer.active { border-color: var(--success); }
.usr-filter-btn.usr-seg-admin.active { border-color: #c2691a; }
html[data-theme="dark"] .usr-filter-btn.usr-seg-admin.active { border-color: #e0913f; }
.usr-filter-btn.usr-seg-owner.active { background: var(--danger); border-color: var(--danger); color: #fff; }
.usr-filter-btn.usr-seg-pending.active { background: #a07407; border-color: #a07407; color: #fff; }
html[data-theme="dark"] .usr-filter-btn.usr-seg-pending.active { background: #f0c94a; border-color: #f0c94a; color: #241c00; }
/* a long list scrolls inside the panel instead of pushing everything below it away */
.usr-scroll { max-height: 340px; overflow-y: auto; padding-right: 2px; }

/* A row is outlined in its own role's colour, so the list reads as four groups
   at a glance rather than four rows with different badges. Each border is the
   exact colour of that row's badge — see .usr-owner / .usr-admin / … above. */
.usr-row.role-owner { border-color: var(--danger); }
.usr-row.role-admin { border-color: #c2691a; }
html[data-theme="dark"] .usr-row.role-admin { border-color: #e0913f; }
.usr-row.role-viewer { border-color: var(--success); }
.usr-row.role-pending { border-color: #a07407; }
html[data-theme="dark"] .usr-row.role-pending { border-color: #f0c94a; }

/* ---- one account's window (the row opens it) ---- */
.usr-panel { display: flex; flex-direction: column; gap: 10px; min-width: 300px; }
/* badge left, online state right — the two things you check before deciding */
.usr-panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.usr-panel-name { font-size: 17px; font-weight: 800; color: var(--text); line-height: 1.2; }
.usr-panel .req-detail { margin-top: 2px; }
/* the rights sentence wraps rather than stretching the window */
.usr-panel .req-dl b { white-space: normal; }
/* The two rungs and the padlock stand side by side, matched in height AND
   width — the switch says where the account is, the padlock takes it off the
   ladder, and neither is the more important half.
   A grid of two equal tracks, not flex: with flex the words inside still get a
   say through min-width, and "Viewer Admin" against "Lock" came out 16px apart.
   1fr 1fr is the same width whatever is written in them. */
.usr-panel-row {
  display: grid; grid-template-columns: 1fr 1fr; align-items: stretch;
  gap: 8px; margin-top: 2px;
}
.usr-panel-row .usr-seg { min-width: 0; }
.usr-panel-row .usr-seg-btn { flex: 1 1 0; min-width: 0; padding-left: 0; padding-right: 0; }
.usr-panel-lock {
  min-width: 0;
  justify-content: center; gap: 7px; font-weight: 700;
  padding: 0 12px; border-radius: 11px;      /* height comes from the row */
}
.usr-panel-approve {
  justify-content: center; gap: 7px; font-weight: 700;
  margin-top: 2px; padding-top: 9px; padding-bottom: 9px;
}
.usr-panel-lock svg, .usr-panel-approve svg { opacity: .9; }

/* Both rungs on screen with the current one lit, rather than a dropdown that
   had to be opened to reveal there were only ever two answers. Each side lights
   in its OWN role's colour — the same green and amber the badges wear — so the
   switch says which rung it is on without reading it. */
.usr-seg {
  display: inline-flex; gap: 3px; padding: 4px; border-radius: 12px;
  background: var(--surface); border: 1px solid var(--glass-brd);
}
.usr-seg-btn {
  border: none; background: transparent; color: var(--text-soft); cursor: pointer;
  padding: 8px 18px; border-radius: 9px;
  font-family: inherit; font-size: 13px; font-weight: 700;
  transition: background .14s, color .14s;
}
.usr-seg-btn:hover { background: var(--surface-2); color: var(--text); }
.usr-seg-viewer.active { background: var(--success); color: #fff; }
.usr-seg-admin.active { background: #c2691a; color: #fff; }
html[data-theme="dark"] .usr-seg-admin.active { background: #e0913f; color: #201400; }

/* ===== Backup: collapsible groups ===== */
.set-grp { border-bottom: 1px solid var(--glass-brd); }
.set-grp:last-of-type { border-bottom: 0; }
.set-grp > summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
  padding: 12px 2px; list-style: none;
  font-size: 11px; font-weight: 800; letter-spacing: .7px; text-transform: uppercase;
  color: var(--text-soft);
}
.set-grp > summary::-webkit-details-marker { display: none; }
.set-grp > summary span { flex: 1; }
.set-grp > summary svg { transition: transform .18s; opacity: .7; }
.set-grp[open] > summary { color: var(--text); }
.set-grp[open] > summary svg { transform: rotate(180deg); }
/* …except in Pages, where each row already carries the page's own icon on the
   left: a second one spinning on the right made the row look busier than the
   one setting in it warrants */
[data-panel="pages"] .set-grp[open] > summary svg { transform: none; }
.set-grp > summary:hover { color: var(--text); }
.set-grp > .field:last-child, .set-grp > div:last-child { margin-bottom: 12px; }
/* a group that does NOT fold (Backup's two): the heading reads the same as a
   summary does, it just has nothing to click */
.set-grp-head {
  padding: 12px 2px; font-size: 11px; font-weight: 800; letter-spacing: .7px;
  text-transform: uppercase; color: var(--text);
}
.bk-actions { margin: 10px 0; }
.bk-list { margin-top: 0; }
/* Overwrite is amber: it is not destructive the way Delete is, but it does
   throw away the copy the backup was holding. */
.bk-over { color: #a04a07; border-color: color-mix(in srgb, #a04a07 45%, transparent); }
.bk-over:hover { background: color-mix(in srgb, #a04a07 12%, transparent); border-color: #a04a07; }
html[data-theme="dark"] .bk-over { color: #e0913f; border-color: color-mix(in srgb, #e0913f 45%, transparent); }
html[data-theme="dark"] .bk-over:hover { background: color-mix(in srgb, #e0913f 14%, transparent); border-color: #e0913f; }

/* ===== Change history: fixed height, paged =====
   A year of changes must not stretch the window past the bottom of the screen —
   the list scrolls inside itself and the page controls stay put below it. */
.aud-scroll {
  max-height: 46vh; overflow-y: auto; overscroll-behavior: contain;
  padding-right: 4px; margin-right: -4px;   /* keeps the scrollbar off the cards */
}
.aud-pager {
  display: flex; align-items: center; justify-content: center; gap: 12px;
  margin-top: 12px;
}
.aud-pageno { font-size: 12px; color: var(--text-soft); font-weight: 600; }
/* The two bulk buttons sit together at the right end of the bar. The shared
   `.req-bulk .btn { margin-left: auto }` would otherwise apply to BOTH and
   split the free space between them. */
#aud-bulk { gap: 8px; justify-content: flex-end; }
#aud-bulk .btn { margin-left: 0; }

/* Decision buttons on top, "Show" in the bottom-right corner — it is not part
   of the decision, it is the way to go and look before making one. */
.req-actions-col {
  flex-direction: column; align-items: flex-end; justify-content: space-between;
  gap: 10px; align-self: stretch;
}
.req-decide { display: flex; gap: 7px; }
.aud-goto { white-space: nowrap; margin-top: auto; }

/* ===== Waiting-for-a-decision counts =====
   On the Administration button in the sidebar footer, and on the category the
   count belongs to. */
/* Only the button that carries a badge becomes a positioning context. A blanket
   `.icon-btn { position: relative }` also caught .wt-pop in the Watch window and
   knocked it out of its corner — icon buttons elsewhere position themselves. */
#adminBtn { position: relative; }

/* A round count. One digit is a circle; two grow it into a pill instead of
   squashing the number. Bright, slightly raised, and it never touches the icon. */
.btn-badge, .cat-badge {
  display: inline-flex; align-items: center; justify-content: center;
  box-sizing: border-box; min-width: 20px; height: 20px; padding: 0 6px;
  border-radius: 999px;
  /* a fixed, saturated red rather than --danger: the theme's red is tuned for
     text and buttons, and it goes muddy at this size */
  background: #f11d2e;
  color: #fff; font-size: 11.5px; font-weight: 900; line-height: 1;
  letter-spacing: 0; font-variant-numeric: tabular-nums;
  text-shadow: 0 1px 1px rgba(0, 0, 0, .35);   /* holds the digit against the red */
  box-shadow: 0 2px 7px rgba(241, 29, 46, .55);
}
/* on the round button it rides the corner with a cut-out, so it reads as a
   badge ON the button rather than as part of the icon */
.btn-badge {
  position: absolute; top: -4px; right: -4px;
  min-width: 19px; height: 19px; font-size: 11px;
  border: 2px solid var(--side-bg, var(--surface));
}
.cat-badge { margin-left: auto; }

/* Mapbox token: masked field + the eye that reveals it */
.tok-row { gap: 8px; }
.tok-row input { flex: 1; min-width: 0; }
.tok-actions { margin-top: 10px; }

/* 🗑 .ok-flash (the green tick) went with its only caller — Utils.flashOk, removed 2026-09-13.
   Rick had already taken the last call out: the panel it landed on says what happened itself. */

/* ===== Note fields that show real links =====
   The twin <div> Utils.linkArea puts beside the textarea. It carries the SAME
   classes as the field, so it inherits the box it is standing in for; what is
   added here is only what a div does not do on its own. */
.linkarea-view {
  white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto;
  cursor: text; line-height: 1.5;
  /* the twin stands in for a textarea, so it needs its box: the type selectors
     that draw one (.field textarea, …) cannot reach a div */
  padding: 11px 13px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text); font-size: 14px; font-family: inherit;
  min-height: 60px;
}
/* a linked phrase is the accent colour and nothing else — no underline under the
   words somebody wrote */
.linkarea-view a { color: var(--accent); text-decoration: none; word-break: break-word; cursor: pointer; }
.linkarea-view a:hover { color: color-mix(in srgb, var(--accent) 80%, var(--text)); }
/* the twin is TYPED IN now (contenteditable), so it takes the focus ring the
   field it replaces had, and shows the placeholder while it is empty */
.rich-note:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
/* a bare address in a note is a CHIP — a chain and the site it points at. The
   address itself is stored and opened, never read off the screen. */
/* a.url-chip, not .url-chip: `.linkarea-view a` above sets the colour and would
   otherwise win on specificity, painting every chip the plain link colour */
a.url-chip {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: baseline;
  padding: 1px 8px 1px 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 15%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  color: var(--accent); font-size: .92em; font-weight: 600; text-decoration: none;
  white-space: nowrap;
}
/* A bare address is the chain ALONE. Square padding so it comes out round rather
   than as an empty pill, and wide enough that a thumb can still hit it — 24px
   across, against the 11px the icon would be on its own. */
a.url-chip.url-chip-bare { gap: 0; padding: 1px 3px; }
.url-chip svg { flex-shrink: 0; opacity: .85; }
a.url-chip:hover { background: color-mix(in srgb, var(--accent) 26%, transparent); color: var(--accent); }
/* ⚠ ABSOLUTE, and that is the whole point. Emptying the field left the caret
   sitting AFTER the placeholder — an in-flow ::before is real content the caret
   comes after, so the next character was typed behind "Write a note…". Taken out
   of the flow, the box is genuinely empty and the caret starts where the text
   will. */
.rich-note { position: relative; }
.rich-note.is-empty::before {
  content: attr(data-ph); color: var(--text-soft); opacity: .7; pointer-events: none;
  position: absolute; left: 0; top: 0; padding: inherit; border: inherit;
  border-color: transparent; width: 100%; box-sizing: border-box;
}
/* 250px on the New Note screen (Rick, 2026-09-12). ⚠ It has to be said HERE as well: the twin that
   shows the links carries the field's own classes, and `.linkarea-view { min-height: 60px }` above
   stands later in this file than `.notes-text` does — so the box you actually look at kept the
   small height while the hidden field behind it was 250. */
.notes-text.linkarea-view { height: 250px; min-height: 250px; }
/* the opened note's own field is a bigger, quieter box — the twin follows it */
.note-open-text.linkarea-view {
  background: var(--surface); border-color: var(--panel-div); border-radius: 12px;
  padding: 13px 15px; font-size: 14.5px; line-height: 1.6; min-height: 180px;
}

/* ===== A trooper card whose Details carry a note ===== */
/* Its own token, not --warn: that one is dark brown-orange in light and yellow
   in dark, and this corner has to read yellow in both. */
:root { --note-corner: #d9a213; }
html[data-theme="dark"] { --note-corner: #e0b341; }
/*
   The corner, not a badge: it says "there is something written here" without
   taking a place in the card's layout or being another thing to read. The card
   is `overflow: hidden`, so the triangle keeps the rounded corner. */
.tcard.has-note::after {
  content: ''; position: absolute; top: 0; right: 0; width: 19px; height: 19px;
  background: linear-gradient(225deg, var(--note-corner) 0 52%, transparent 52%);
  pointer-events: none;
}

/* a backup's name turning into a field, in place, in its own row. `.sm-input`
   only sets padding and size — the box itself comes from `.field input`, which
   cannot reach an input standing on its own, so it came out browser-white. */
.bk-name-inp {
  width: 100%; max-width: 260px;
  background: var(--surface-2); color: var(--text);
  border: 1px solid color-mix(in srgb, var(--accent) 45%, var(--border));
  border-radius: 9px; font-family: inherit; font-weight: 700;
}
.bk-name-inp:focus { outline: none; border-color: var(--accent); }

/* A field masked without being a password field — see the Mapbox token in
   js/app.js for why that matters. WebKit/Blink honour -webkit-text-security,
   which is what Rick's browsers are; anywhere else the token simply reads as
   text, which is no worse than what the page already hands out. */
.masked {
  -webkit-text-security: disc;
  text-security: disc;
  font-family: inherit; letter-spacing: .08em;
}

/* ===== A Last-seen row's kind (and the link it carries) =====
   A label, not a form field: a + until something is chosen, the kind after
   that. Pressing it brings the real combo forward (that is what holds the
   options, and what a new kind is typed into) and drops it again once a choice
   is made. The pencil beside it opens the link window and puts the row in edit
   mode — where the label chooses a kind instead of following the link. */
/* the name does NOT stretch here — the kind sits behind it rather than being
   pushed to the far edge */
.ls-meta { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; margin-left: 20px; }
/* as wide as what is in it — a + is a small label, "Traffic Stop" a wider one —
   and never wider than the row can carry */
/* one width for every label, handed down as --ls-kind-w by whichever window draws the rows
   (the longest entry in the kinds list, floor "Traffic Stop", ceiling 200px) */
.ls-kind { position: relative; width: var(--ls-kind-w, 108px); min-width: var(--ls-kind-w, 108px); max-width: var(--ls-kind-w, 108px); flex: 0 0 auto; }
/* The combo lies under the label at the same size, so the option list it opens
   lands in the right place. It is absolutely positioned, which puts it OVER the
   label whatever the markup order, so it has to let clicks through or the label
   cannot be pressed at all — its list is the one part that must still take them
   (it does anyway once placeList has moved it into the window's scrolling box;
   this covers the case where it stays put).
   And it is the FIELD that hides under the label, never the whole combo: the
   list starts life as a child of the combo, and hiding that hid the list too. */
.ls-kind .combo { position: absolute; inset: 0; pointer-events: none; }
.ls-kind .combo-list { pointer-events: auto; }
.ls-kind.picking .ls-label { visibility: hidden; }
.ls-kind .combo-input {
  opacity: 0; pointer-events: none;
  padding: 6px 10px !important; font-size: 13px !important;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text); height: 100%;
}
.ls-kind.picking .combo-input { opacity: 1; pointer-events: auto; }
.ls-kind .combo-input:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.ls-kind .combo-caret { display: none; }
/* the label itself: the Date field's box, the name's type, its content centred.
   Positioned, so it stays above the combo lying over it. */
.ls-label {
  position: relative; z-index: 1;
  display: flex; align-items: center; justify-content: center;
  width: 100%; min-height: 30px;
  padding: 5px 12px; border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text);
  font-family: inherit; font-size: 13px; text-decoration: none;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  /* it always leads somewhere: to the options when it has no link, to the video
     when it has one — so it points either way */
  cursor: pointer;
}
.ls-label:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); }
a.ls-label:hover { text-decoration: none; }
/* A pursuit's own label: there is no list to open, so it does not offer one —
   unless it is a link to that pursuit's video, which does point. It has no
   .ls-kind box around it either, so it takes the shared width straight, or it
   would be the one ragged label in the column. */
.ls-meta-fixed .ls-label { cursor: default; width: var(--ls-kind-w, 108px); }
.ls-meta-fixed a.ls-label { cursor: pointer; }
/* the red label is a button when the link is still missing — pressing it is how one gets added */
.ls-meta-fixed button.ls-label { cursor: pointer; }
.ls-label.as-text { color: var(--text-soft); }
/* a sighting with nowhere to look it up: the label is all there is, so it says
   so itself — the chain button that used to carry this is gone */
.ls-label.nolink { border-color: color-mix(in srgb, var(--danger) 60%, transparent); }
/* nothing chosen yet: a + in the middle and nothing else to read */
.ls-label.empty { color: var(--text-soft); }
.ls-label.empty:hover { color: var(--accent); }
/* the pencil: a switch. Lit RED while the row is in edit mode — the label is
   doing something different from usual while that is on, and red is the colour
   this app uses for "this changes something". */
.ls-editbtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; padding: 0; flex: 0 0 auto;
  border: 1px solid transparent; border-radius: 9px;
  background: none; color: var(--text-soft); cursor: pointer;
}
/* nothing linked yet → the chain says so in red, before it is even hovered */
.ls-editbtn.nolink { color: var(--danger); }
.ls-editbtn:hover { color: var(--danger); background: rgba(217, 52, 43, .12); }
.ls-editbtn.on {
  color: var(--danger); background: rgba(217, 52, 43, .16);
  border-color: rgba(217, 52, 43, .45);
}
/* a row whose date came from a pursuit has no ✕ — but the line still starts
   where every other line starts */
.h-del-slot { flex-shrink: 0; width: 14px; margin-right: 2px; }
/* the small window's field: .sm-input only sets size, so on its own it came out
   browser-white — the box has to be drawn here, like everywhere else */
.prompt-box { text-align: left; }
.prompt-box .confirm-title { margin-bottom: 10px; }
.prompt-input {
  width: 100%; box-sizing: border-box;
  padding: 10px 13px !important; font-size: 13.5px !important;
  border: 1px solid var(--border); border-radius: 12px;
  background: var(--surface-2); color: var(--text); font-family: inherit;
}
.prompt-input:focus { outline: none; border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); }
.prompt-input::placeholder { color: var(--text-soft); opacity: .6; }
.prompt-box .confirm-actions { margin-top: 14px; }
.prompt-open { text-decoration: none; display: inline-flex; align-items: center; justify-content: center; }

/* A dropdown list that had to leave the flow to be seen at all (Combo.placeList):
   fixed to the screen at the field's rectangle. Above the window it belongs to,
   because the box it came out of is usually inside one. */
.combo-list.combo-list-fixed {
  position: fixed; right: auto; z-index: 420;
  max-height: 240px; box-shadow: 0 18px 40px -18px rgba(0, 0, 0, .55);
}
/* opened upwards: the shadow goes the other way too */
.combo-list.combo-list-up { box-shadow: 0 -18px 40px -18px rgba(0, 0, 0, .55); }

/* A dropdown as a sheet in the lower right of a phone screen (Combo.placeList):
   the field it belongs to is often at the far end of a row that scrolls
   sideways, and a list pinned to that field ended up half off-screen. Down here
   it is always in the same place and big enough to hit with a thumb.
   It is ABSOLUTE, not fixed, and placeList moves it into the window's scrolling
   box and sets `top` there — so it travels with the content instead of standing
   still over it. Fixed, it also came out painted twice on Rick's phone while
   the window scrolled behind it. `left: auto` beats the base rule's left: 0,
   and pointer-events because the list no longer sits inside .ls-kind's combo,
   which is where that used to come from. */
.combo-list.combo-list-sheet {
  position: absolute; left: auto; right: 16px; bottom: auto;
  width: max-content !important; min-width: 150px; max-width: min(78vw, 320px);
  /* six entries and then it scrolls: 6 x 48px plus the box's 8px of padding
     either side and its 1px border. A list of user-named entries can grow to
     any length, and a box the height of the window is a wall, not a menu. */
  max-height: 306px;
  border-radius: 24px; padding: 8px; z-index: 420; pointer-events: auto;
  box-shadow: 0 -20px 50px -18px rgba(0, 0, 0, .6);
}
/* standing beside its field rather than at the window's right edge (sideways and
   on a wide screen): placeList sets `left`, so `right` has to let go */
.combo-list.combo-list-sheet.combo-list-beside { right: auto; }
/* …but on a phone the room, not the count, decides — six of them are 306px of a
   390px-tall landscape screen. (This has to live after the rule above rather
   than in the phone block further up: same specificity, and the later one wins.) */
@media (max-width: 700px),
       (max-height: 500px) and (max-width: 950px) and (orientation: landscape) {
  .combo-list.combo-list-sheet { max-height: 45vh; }
}
.combo-list-sheet .combo-opt { padding: 12px 14px; font-size: 14px; border-radius: 16px; }
.combo-list-sheet .combo-del, .combo-list-sheet .combo-edit { opacity: .6; padding: 6px 8px; }
