/* =============================================================================
   FIELD DATA COLLECTOR — design system
   Palette: originally a rugged, sunlight-readable "field-tool" dark theme
   (Asphalt/Panel/Steel/Paper/Safety Yellow). Now reskinned to match the
   Tasks and Form Designer tabs' light, blue-accented look (see tasks.css's
   own header comment for that palette's origin) — the person building
   this asked for one consistent visual language across the whole app
   instead of the dark field-tool look on some screens and the light
   task-theme look on others. This is a values-only reskin: every color
   in this file already routed through the custom properties below (no
   hardcoded hex anywhere else in this stylesheet), so redefining them
   here is what actually re-themes the whole app — Map, Capture, Admin,
   Viewer, Login — without restructuring a single component, matching
   the existing task-theme's actual hex values 1:1 so the two stylesheets
   produce genuinely identical colors, not just visually similar ones.
   The custom property NAMES below (--asphalt, --paper, --yellow, etc.)
   are kept as-is rather than renamed to match their new meaning — every
   rule in this file (and several inline styles in the JS view files)
   references them by name, and renaming would have meant touching every
   one of those call sites for a purely cosmetic gain. Read --yellow as
   "the app's primary accent color" and --asphalt/--panel as "app
   background / card surface" going forward.
   Type:    Barlow Condensed (labels/headers, tracked caps) · IBM Plex Sans
            (UI/body) · IBM Plex Mono (coordinates, timestamps, IDs).
   ========================================================================== */

@import url('https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');

:root {
  /* Values below match tasks.css's --task-* tokens exactly (see that
     file) — --asphalt = --task-bg, --panel = --task-surface,
     --steel = --task-border, --paper = --task-text,
     --paper-dim = --task-text-dim, --yellow = --task-accent. */
  --asphalt: #f4f6fb;
  --panel: #ffffff;
  --panel-raised: #f7f8fc;
  --steel: #aab3c8;
  --paper: #16213a;
  --paper-dim: #6b7690;
  --yellow: #2f6fed;
  --orange: #e8622c;
  --green: #4ca771;
  --red: #d8483a;

  /* Topbar/tabbar "chrome" theme — deliberately separate from --paper
     (also a dark navy, but used as the app's body TEXT color against a
     light background) since these two are used in opposite roles: here
     it's a background color that text sits ON TOP of, not text itself.
     Keeping them as distinct tokens means a future re-theme of the body
     text color can't accidentally also re-theme the chrome bars.
     --chrome-bg is sampled directly from the two topbar logo PNGs' own
     background (assets/logo-*.png — both were generated on the same
     navy) rather than picked independently, so the images blend into
     the bar with no visible seam/rectangle around them. */
  --chrome-bg: #01162e;
  /* The tabbar's own background — split out from --chrome-bg (Admin >
     Branding, see theme.js) so the two bars can be recolored
     independently. Defaults to the same value so nothing changes for
     anyone who hasn't touched Branding; only JS ever sets this
     directly (as an inline style on :root), never a value hardcoded
     into a rule here. */
  --chrome-bg-bottom: var(--chrome-bg);
  --chrome-cyan: #2fc7e3;
  /* Default (non-narrow) topbar logo display height — overridden at
     narrower breakpoints further down (search "--topbar-logo-h") so the
     two wide logo lockups shrink instead of clipping on small phones. */
  --topbar-logo-h: 30px;

  --font-display: 'Barlow Condensed', system-ui, sans-serif;
  --font-body: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
  --font-mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, monospace;

  --radius: 10px;
  --tap-min: 48px;

  color-scheme: light;
}

/* Dark mode (Settings menu \u2192 Dark/Light, see theme.js's
   applyColorScheme()) — every rule in this file already routes through
   the custom properties above (see this file's own header comment: "no
   hardcoded hex anywhere else in this stylesheet"), so redefining just
   those here is what re-themes the entire app. Chrome bars
   (--chrome-bg/--chrome-bg-bottom) are deliberately NOT overridden —
   they're already a dark navy in both themes, chosen to match the
   brand logo artwork's own background (see that token's own comment),
   not something that needs to change with light/dark. */
:root[data-theme="dark"] {
  --asphalt: #10151f;
  --panel: #181f2e;
  --panel-raised: #1e2637;
  --steel: #2b3448;
  --paper: #e7ebf5;
  --paper-dim: #97a1ba;
  --yellow: #5b9bff;
  --orange: #e8622c;
  --green: #4ca771;
  --red: #e15646;
  color-scheme: dark;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--asphalt);
  color: var(--paper);
  font-family: var(--font-body);
  -webkit-tap-highlight-color: transparent;
}

body {
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
  overflow: hidden;
}

h1, h2, h3, .label {
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin: 0;
}

h1 { font-size: 22px; }
h2 { font-size: 17px; color: var(--paper-dim); }

.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }

button, input, select, textarea {
  font-family: inherit;
  font-size: 16px; /* prevents iOS auto-zoom on focus */
  color: var(--paper);
}

/* Date inputs — a centered calendar icon (drawn ourselves, since native
   rendering of the icon varies wildly by browser/OS — Chrome puts a
   small one on the right, iOS Safari often shows none at all) plus a
   full-box invisible click target so tapping ANYWHERE in the input
   opens the picker, not just the small native icon area. Firefox's date
   input already opens on a click anywhere natively, so it's unaffected
   by (and doesn't need) the picker-indicator override below — only
   Chrome/Safari (WebKit/Blink) restrict the native click target to the
   icon itself. */
/* Date inputs — a single centered gray calendar icon (drawn ourselves,
   since native rendering of the icon varies wildly by browser/OS —
   Chrome puts a small one on the right, iOS Safari often shows none at
   all, Firefox draws its own that would otherwise show up ALONGSIDE
   ours) plus a full-box invisible click target so tapping ANYWHERE in
   the input opens the picker, not just the small native icon area.
   -moz-appearance strips Firefox's own native icon so only this one
   shows; Firefox already opens its picker on a click anywhere natively,
   so it doesn't need (and WebKit's ::-webkit-calendar-picker-indicator
   selector doesn't exist for it anyway) the indicator-overlay trick
   below — only Chrome/Safari (WebKit/Blink) restrict the native click
   target to the icon itself. The icon hides while the field is focused
   (about to be filled in / already showing a value being edited) AND
   permanently once a date is actually picked (the .has-value class,
   toggled by app.js's own global date-input listener — CSS alone can't
   reliably tell "has a value" apart from "empty" for a date input
   across browsers), so it doesn't sit awkwardly on top of the value.

   The four background-* properties are !important on purpose: this
   rule already lost to a same-specificity `background: ...` SHORTHAND
   rule twice — once from this file's own generic `input[type="text"],
   input[type="number"], input[type="date"], textarea, select {...}`
   further down, and again from tasks.css's `.task-theme input, select,
   textarea {...}` (tasks.css loads after this file, so on an equal-
   specificity tie it wins) — and a shorthand `background:` always
   resets every background-* sub-property that isn't re-stated,
   silently wiping the icon out both times even though neither rule
   was trying to touch it. Rather than keep chasing every current and
   future place a background shorthand might get applied to a date
   input, !important on just these four properties settles it — the
   background-COLOR those other rules set is untouched and still
   applies normally, only the icon-drawing properties are protected. */
input[type="date"] {
  position: relative;
  -moz-appearance: textfield;
  background-repeat: no-repeat !important;
  background-position: center center !important;
  background-size: 19px 19px !important;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Crect x='3' y='5' width='18' height='16' rx='2' stroke='%239ca3af' stroke-width='1.6'/%3E%3Cpath d='M8 3v4M16 3v4' stroke='%239ca3af' stroke-width='1.6' stroke-linecap='round'/%3E%3Cpath d='M3 10h18' stroke='%239ca3af' stroke-width='1.6'/%3E%3Crect x='6' y='13' width='2.4' height='2.4' fill='%239ca3af'/%3E%3Crect x='10.8' y='13' width='2.4' height='2.4' fill='%239ca3af'/%3E%3Crect x='15.6' y='13' width='2.4' height='2.4' fill='%239ca3af'/%3E%3Crect x='6' y='17' width='2.4' height='2.4' fill='%239ca3af'/%3E%3Crect x='10.8' y='17' width='2.4' height='2.4' fill='%239ca3af'/%3E%3C/svg%3E") !important;
}
input[type="date"]:focus, input[type="date"].has-value {
  background-image: none !important;
}
input[type="date"]::-webkit-calendar-picker-indicator {
  position: absolute;
  left: 0; top: 0;
  width: 100%; height: 100%;
  margin: 0; padding: 0;
  opacity: 0; /* our own icon is the background-image above; this is purely the click target */
  cursor: pointer;
}

/* ---------- App shell ------------------------------------------------- */
#app {
  height: 100%;
  display: flex;
  flex-direction: column;
}

#view-root {
  flex: 1;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  position: relative;
}
/* Screen transition (§ direct request — "windows and screens
   open/close") — a quick fade+rise played every time router.js swaps
   this element's content for a different view. CSS animations don't
   replay just because a child's content changed underneath them, so
   router.js resets this via the standard "set to none, force a
   reflow, clear it" trick rather than toggling a class — see its own
   comment at the navigate() call site. Kept short and subtle
   (0.18s, 4px of motion) since this fires on EVERY navigation,
   including rapid back-and-forth tab switching, where anything more
   pronounced would start to feel like it's fighting the person rather
   than helping. See the consolidated prefers-reduced-motion block
   near the end of this file's own animation additions for how this
   (and every other animation added this session) gets turned off for
   anyone who's asked their OS for that. */
@keyframes view-fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
#view-root { animation: view-fade-in 0.18s ease both; }

.topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 8px 16px;
  border-bottom: 1px solid var(--steel);
  background: var(--chrome-bg);
}
/* Text/icon colors made explicit here (not by redefining --paper/--yellow
   for the whole .topbar scope) because .user-menu-dropdown and
   .notif-dropdown are DOM descendants of .topbar but are their own
   floating panels with their own light background (--panel-raised) —
   redefining the shared custom properties at .topbar level would leak
   into those dropdowns too and make their own text illegible against
   their own light background. Targeting only the always-visible bar
   elements (never the dropdown panels, which are siblings of the
   buttons that open them, not their children) keeps this scoped
   correctly. */
.topbar .user-menu-btn .user-menu-name { color: #ffffff; }
.topbar .user-menu-btn .chevron { color: #ffffff; }
.topbar .notif-bell-btn { color: #ffffff; }

/* Flex, evenly distributed, logo NOT centered — simpler than the grid
   layout this used to be. That grid version reserved the *logo* its own
   column but still needed the username's own shrink/truncate chain to
   be bulletproof for the two SIDE columns not to collide with it or
   overflow, and that chain kept having one more missing min-width:0
   than the last fix caught (documented at length further down this
   file). Switching the outer layout to flex doesn't replace needing
   that chain to be correct — it still needs to be — but it does mean
   all three sections (brand / logo / topbar-right) are ordinary flex
   items that shrink by the exact same rule instead of two of them
   being pinned to fixed-width grid columns and one having to do all the
   flexing, which is simpler to reason about and leaves less room for
   exactly the kind of one-level-too-shallow fix that kept happening. */
.topbar .brand { flex: 0 1 auto; min-width: 0; display: flex; align-items: center; gap: 8px; overflow: hidden; }

.topbar-divider { flex: none; width: 1px; height: 26px; background: rgba(255, 255, 255, 0.25); margin: 0 1px; }

/* Both logos (the app's own "Field Data Collector" mark and the org's
   "Imaginary Engineering" mark — see app.js's BRAND_LOGO_SRC/
   ORG_LOGO_DEFAULT_SRC, real cropped PNGs now, not hand-built SVG) are
   flex: 0 0 auto — NEVER flex-shrunk. A shrunk raster logo doesn't
   shrink cleanly the way text does; it either distorts (if width and
   height both got squeezed unevenly) or gets clipped by the row's own
   overflow. Since object-fit:contain plus a fixed height already keeps
   each image's own aspect ratio correct at any size, the actual
   narrow-screen size control lives in the @media block below (an
   explicit smaller `--topbar-logo-h` at each breakpoint) rather than
   flexbox — so on a small phone the logos get deliberately smaller,
   never cropped or squashed. The username span is what actually absorbs
   the space pressure (it already has its own min-width:0/ellipsis
   chain, see .user-menu-btn .user-menu-name's own comment) — that's
   normal, expected truncation for an arbitrarily long name, not a sign
   this layout is fighting for room. */
.topbar-logo {
  flex: 0 0 auto;
  height: var(--topbar-logo-h);
  width: auto;
  object-fit: contain;
  pointer-events: none;
}

/* The org logo specifically (the admin-configurable one — see
   renderHeader()'s ORG_LOGO_DEFAULT_SRC/state.headerLogo, and the
   Branding screen's size slider in admin.js) additionally scales by
   --topbar-logo-scale, a percentage set inline per-render from
   state.headerLogoSize (default 100 = unscaled). The app's own fixed
   "Field Data Collector" mark stays plain .topbar-logo, unscaled — it
   isn't admin-configurable, so it has no size to apply here. Layered
   on TOP of --topbar-logo-h's own responsive breakpoints (below), not
   instead of them, so a larger admin-chosen size still shrinks
   proportionally on a small phone rather than ignoring the mobile-fit
   work that variable exists for. */
.topbar-logo-org {
  height: calc(var(--topbar-logo-h) * var(--topbar-logo-scale, 1));
}

.topbar-right { flex: 1 1 auto; min-width: 0; display: flex; align-items: center; justify-content: flex-end; gap: 10px; }

/* Narrow phones: shrink the logos (proportionally, via height only —
   see .topbar-logo's own comment) and tighten the row's padding/gaps so
   the two wide logo lockups, the username, and the bell all fit without
   any of them being clipped. Two steps rather than one: 480px covers
   most phones in portrait; 360px covers the still-common smaller/older
   handsets (e.g. iPhone SE) where even the 480px sizing leaves too
   little room for a normal-length name once both logos are present. */
@media (max-width: 480px) {
  .topbar { padding: 8px 10px; gap: 6px; }
  :root { --topbar-logo-h: 24px; }
  .topbar .brand { gap: 6px; }
  .topbar-divider { height: 20px; }
  .topbar-right { gap: 8px; }
  .user-menu-btn .user-menu-name { max-width: 110px; }
}
@media (max-width: 360px) {
  :root { --topbar-logo-h: 20px; }
  .user-menu-btn .user-menu-name { max-width: 80px; }
}
@media (max-width: 320px) {
  .topbar { padding: 6px 8px; gap: 4px; }
  :root { --topbar-logo-h: 17px; }
  .topbar .brand { gap: 4px; }
  .topbar-right { gap: 5px; }
  .user-menu-btn .user-menu-name { max-width: 46px; }
  .user-menu-btn { padding: 8px 2px; gap: 3px; }
}

/* ---------- User menu dropdown ------------------------------------------ */
.user-menu { position: relative; min-width: 0; }
.user-menu-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  background: none;
  border: none;
  color: var(--paper);
  font-family: inherit;
  font-size: 14px;
  padding: 8px 4px;
  cursor: pointer;
}
.user-menu-btn .user-menu-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 200px; /* just a sane ceiling for a very long name on a WIDE screen — the actual narrow-screen truncation now comes from the flex chain itself shrinking this element down to whatever space is really left, not from this number */
  min-width: 0; /* THIRD instance of the same flex gotcha in this one header (see .topbar-right and .user-menu's own comments) — as a flex item of .user-menu-btn, this span's default min-width:auto resolves to its full nowrap text width, which can legitimately override max-width per the CSS spec's min/max resolution rule (min-width wins when the two conflict) — meaning without this, a long enough name simply ignored the cap and rendered at full width regardless, still colliding with the bell. */
}
.user-menu-btn .chevron {
  font-size: 10px;
  color: var(--paper-dim);
  transition: transform 0.15s ease;
}
.user-menu.open .user-menu-btn .chevron { transform: rotate(180deg); }
.user-menu-dropdown {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 180px;
  max-width: min(280px, calc(100vw - 24px));
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  box-shadow: 0 6px 18px rgba(0,0,0,0.45);
  padding: 6px;
  z-index: 50;
  display: none;
}
.user-menu.open .user-menu-dropdown { display: block; }
.user-menu-dropdown button, .user-menu-dropdown a {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: none;
  color: var(--paper);
  font-family: inherit;
  font-size: 14px;
  padding: 10px 12px;
  border-radius: 6px;
  cursor: pointer;
  text-decoration: none;
  box-sizing: border-box;
}
.user-menu-dropdown button:hover, .user-menu-dropdown a:hover { background: var(--panel); }
.user-menu-dropdown button.danger { color: var(--red); }
.user-menu-dropdown .divider { height: 1px; background: var(--steel); margin: 6px 4px; }

/* Global Project Selector (§18) — lives inside the existing user-menu
   dropdown rather than as its own topbar element; see app.js's
   renderHeader() for the full reasoning. Styled as a small labeled
   group at the top of the dropdown, above the existing "All Projects"/
   Dashboard/User Guide items, so it reads as "context" rather than
   another menu action. */
.user-menu-project-picker { padding: 6px 12px 10px; }
.user-menu-project-picker label {
  display: block;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--paper-dim);
  margin-bottom: 4px;
}
.user-menu-project-picker select { width: 100%; }

/* ---------- Notification bell (topbar) — deliberately kept in the global
   dark theme, not the task module's light theme, since the topbar is
   shared chrome outside any one view (same reasoning the tabbar and
   user-menu already follow). .topbar-right itself is defined up with
   the rest of the topbar's grid layout. ------------------------------- */
.notif-bell-wrap { position: relative; flex: none; }
.notif-bell-btn {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  width: 36px; height: 36px;
  background: none; border: none; color: var(--paper); cursor: pointer;
}
.notif-bell-btn svg { width: 20px; height: 20px; }
.notif-badge {
  position: absolute; top: 3px; right: 3px;
  min-width: 15px; height: 15px; border-radius: 999px;
  background: var(--red); color: #fff; font-size: 10px; font-weight: 700;
  display: flex; align-items: center; justify-content: center; padding: 0 3px;
  font-family: var(--font-body);
}
.notif-dropdown {
  /* right: 0, not a negative offset — this bell is now the rightmost
     element in the topbar (see .topbar-right's reordering), sitting
     close to the topbar's own edge padding; a negative right offset
     pushed the dropdown a few pixels past the bell's own right edge,
     which was enough to clip it off the right side of the viewport on
     narrow (iOS) screens. max-width is capped against the actual
     viewport width minus the topbar's 16px+16px padding, not just a
     flat vh-based guess, so it can never overflow regardless of screen
     width. */
  position: absolute; top: calc(100% + 6px); right: 0;
  width: 300px; max-width: min(82vw, calc(100vw - 32px)); max-height: 70vh; overflow-y: auto;
  background: var(--panel-raised); border: 1px solid var(--steel); border-radius: var(--radius);
  box-shadow: 0 6px 18px rgba(0,0,0,0.45); padding: 6px; z-index: 50; display: none;
}
.notif-bell-wrap.open .notif-dropdown { display: block; }
.notif-dropdown-header {
  display: flex; align-items: center; justify-content: space-between;
  padding: 8px 8px 6px; font-family: var(--font-display); font-weight: 700;
  text-transform: uppercase; font-size: 12px; letter-spacing: 0.05em; color: var(--paper-dim);
}
.notif-dropdown-header button {
  background: none; border: none; color: var(--yellow); font-size: 11px; cursor: pointer; text-transform: none; letter-spacing: normal;
}
.notif-item {
  display: block; width: 100%; text-align: left; background: none; border: none;
  color: var(--paper); font-family: inherit; padding: 8px; border-radius: 6px; cursor: pointer;
}
.notif-item:hover { background: var(--panel); }
.notif-item.unread { background: rgba(47, 111, 237, 0.08); }
.notif-item-title { font-size: 13px; font-weight: 600; }
.notif-item-body { font-size: 12px; color: var(--paper-dim); margin-top: 2px; }
.notif-item-time { font-size: 11px; color: var(--paper-dim); margin-top: 4px; }
.notif-empty { padding: 16px 8px; text-align: center; color: var(--paper-dim); font-size: 13px; }

/* ---------- Generic modal ------------------------------------------------ */
@keyframes modal-overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-box-in { from { opacity: 0; transform: scale(0.96) translateY(8px); } to { opacity: 1; transform: scale(1) translateY(0); } }
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px;
  overflow-y: auto; /* lets the backdrop itself scroll when the box is taller than the viewport, so nothing in it becomes unreachable */
  animation: modal-overlay-in 0.15s ease both;
}
.modal-box {
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  max-width: 720px;
  width: 100%;
  max-height: 85vh;
  max-height: 85dvh; /* dvh accounts for mobile browser chrome (URL bar) resizing the visible viewport — plain vh is fixed to the larger "chrome hidden" height, so on a shorter actual viewport the box could extend past what's really reachable even though overflow-y:auto is set correctly below. Browsers without dvh support just keep the vh value above. */
  display: flex;
  flex-direction: column;
  box-shadow: 0 12px 32px rgba(0,0,0,0.5);
  overflow: hidden; /* clips to max-height so only .modal-body scrolls, not the whole box */
  margin: auto; /* keeps it centered even once .modal-overlay itself needs to scroll */
  animation: modal-box-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}
.modal-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px;
  border-bottom: 1px solid var(--steel);
  flex-shrink: 0;
}
.modal-header h2 { margin: 0; }

/* ---------- Session Playback (fieldSessions.js) — full screen -------
   Overrides just enough of the generic .modal-overlay/.modal-box rules
   above to go edge-to-edge instead of the normal centered/capped-width
   box every other modal in this app uses — a scrub-through-time map
   view wants the whole viewport, not a 1200px column. */
.pb-overlay {
  padding: 0; /* the generic rule's 20px backdrop margin — full screen has no backdrop left to show around the box */
  align-items: stretch;
  justify-content: stretch;
}
.pb-fullscreen {
  max-width: none;
  width: 100vw;
  height: 100vh;
  height: 100dvh; /* see .modal-box's own dvh comment above — same mobile-chrome reasoning applies here */
  max-height: none;
  border-radius: 0;
  margin: 0;
}
/* Title + close X + every playback control (buttons, speed, clock,
   scrub bar) in one sticky row above the map — reachable regardless of
   how far the events list below the map is scrolled. flex-wrap lets it
   fall onto more than one visual line on a narrow phone screen without
   becoming a second, separate row in the markup; it's still one flex
   container. */
.pb-toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  padding: 10px 52px 10px 14px; /* extra right padding keeps controls clear of the fixed top-right close button (see .pb-close-btn) so it never overlaps the scrub bar or clock */
  border-bottom: 1px solid var(--steel);
  flex-shrink: 0;
  position: sticky;
  top: 0;
  z-index: 1;
  background: var(--panel);
}
.pb-title { margin: 0; flex-shrink: 0; }
.pb-toolbar-controls {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 8px;
  flex: 1 1 auto;
  min-width: 0;
}
.pb-clock { white-space: nowrap; }
/* Overrides the app-wide `select { width: 100% }` rule (see that rule's
   own comment further up this file) — that rule is meant for form
   fields that should fill their container, but inside this toolbar's
   flex row it stretched the speed dropdown to fill all remaining row
   width, forcing everything after it (the clock, the scrub bar) onto
   its own wrapped line instead of staying in the same row as the
   playback buttons. `width: auto` lets it size to its own content
   (e.g. "5×") like a normal <select>. */
.pb-speed-select { width: auto; min-width: 0; }
/* Icon-only Play/Pause (a later request) — fixed square instead of the
   variable-width text+icon button this used to be, so the row's layout
   doesn't shift width every time Play/Pause toggles. */
.pb-play-btn { width: 40px; min-width: 40px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 15px; }
/* flex-basis 100% pushes the scrub bar onto its own line once the
   buttons/speed/clock ahead of it in the same row have claimed the
   available width, rather than squeezing it into a sliver — still the
   same single .pb-toolbar-controls flex row, just wrapping like every
   other control here does on a narrow viewport. */
.pb-scrub { flex: 1 1 220px; min-width: 140px; }
/* Fixed to the screen's own top-right corner (not the toolbar row) so
   it stays in the same spot regardless of how the toolbar's own
   controls wrap on a narrow viewport — "top right of the screen", not
   "end of a row that might not be at the top anymore once it wraps".
   Sits above .pb-toolbar's own sticky z-index so it's never covered by
   it. */
.pb-close-btn {
  position: fixed;
  top: 10px;
  right: 10px;
  z-index: 2;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid var(--steel);
  background: var(--panel-raised);
  color: var(--paper);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pb-close-btn:active { background: var(--steel); }
/* Map + events list fill the rest of the fullscreen box below the
   sticky toolbar — map takes the majority share (its own scrub/replay
   is the primary content), events list gets the remainder and scrolls
   independently rather than growing the whole screen. */
.pb-content {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.pb-map { flex: 1 1 60%; min-height: 200px; }
.pb-events-list {
  flex: 1 1 40%;
  min-height: 120px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  border-top: 1px solid var(--steel);
  padding: 8px 14px;
}
/* Item 3 (a later request) — Playback's discrete-event map markers
   (see fieldSessions.js's own EVENT_MARKER_STYLES/eventMarker()). A
   plain <span>, not a full circle-drawing SVG, since L.divIcon just
   drops raw HTML onto the map — the circle, centering, and glyph
   sizing all come from this one rule rather than being redrawn per
   marker in JS. */
.pb-event-marker span {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 1px 3px rgba(0,0,0,0.4);
  color: #fff;
  font-size: 11px;
  line-height: 1;
}

/* ---------- Settings modal (settings.js) --------------------------- */
.settings-row {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 4px 0;
}
.settings-row-label { font-weight: 600; color: var(--paper); }
.settings-row-help { font-size: 13px; color: var(--paper-dim); margin-top: 2px; }
/* A real <button role="switch"> (not a checkbox styled to look like a
   switch) — matches this app's existing preference for genuine
   interactive semantics over faked ones elsewhere (see .task-view-toggle
   in tasks.css for the same "buttons, not radio inputs, for a segmented
   control" choice). track/thumb are two nested spans purely for the
   sliding-thumb visual; aria-checked (kept in sync by settings.js) is
   what actually communicates state to assistive tech, not their
   presence/position. */
.theme-toggle {
  display: flex; align-items: center; gap: 10px; background: none; border: none; padding: 4px; cursor: pointer;
  font-family: var(--font-body); font-size: 13px; font-weight: 600; color: var(--paper-dim);
}
.theme-toggle-track {
  position: relative; display: inline-block; width: 44px; height: 24px; border-radius: 999px;
  background: var(--steel); transition: background 0.15s ease; flex-shrink: 0;
}
.theme-toggle-thumb {
  position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%;
  background: #ffffff; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); transition: transform 0.15s ease;
}
.theme-toggle.is-dark .theme-toggle-track { background: var(--yellow); }
.theme-toggle.is-dark .theme-toggle-thumb { transform: translateX(20px); }
.theme-toggle.is-dark .theme-toggle-text { color: var(--paper); }
.modal-body {
  padding: 20px;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  flex: 1 1 auto; /* constrains this to the box's remaining space so its own overflow-y:auto actually clips content instead of the flex item just growing to fit it */
  min-height: 0; /* required alongside flex:1 for overflow to work inside a column flex container — without it a flex item won't shrink below its content's natural size */
}
.modal-body h3 {
  font-family: var(--font-display);
  color: var(--yellow);
  margin: 24px 0 8px;
}
.modal-body h3:first-child { margin-top: 0; }
.modal-body h4 { margin: 16px 0 6px; }
.modal-body p, .modal-body li { color: var(--paper); line-height: 1.55; }
.modal-body ul, .modal-body ol { padding-left: 22px; margin: 8px 0; }
.modal-body li { margin-bottom: 4px; }
.modal-body code {
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: 4px;
  padding: 1px 5px;
  font-family: var(--font-mono);
  font-size: 0.9em;
}
.modal-toc {
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 12px 16px;
  margin-bottom: 20px;
}
.modal-toc div { font-family: var(--font-display); color: var(--paper-dim); font-size: 12px; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
.modal-toc a { color: var(--yellow); text-decoration: none; display: block; padding: 3px 0; font-size: 14px; }
.modal-toc a:hover { text-decoration: underline; }

/* ---------- Bottom tab nav --------------------------------------------- */
.tabbar {
  display: flex;
  border-top: 1px solid var(--steel);
  background: var(--chrome-bg-bottom);
  padding-bottom: env(safe-area-inset-bottom);
}

.tabbar button {
  flex: 1;
  background: none;
  border: none;
  padding: 10px 4px calc(8px + env(safe-area-inset-bottom, 0px));
  min-height: var(--tap-min);
  color: rgba(255, 255, 255, 0.8);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  border-bottom: 2px solid transparent;
  transition: color 0.15s ease, border-bottom-color 0.15s ease;
}

.tabbar button.active {
  color: var(--chrome-cyan);
  border-bottom-color: var(--chrome-cyan);
}
.tabbar button:active .icon { transform: scale(0.88); }
.tabbar .icon { transition: transform 0.1s ease; }

.tabbar .icon {
  font-size: 18px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
}
.tabbar .icon svg { width: 100%; height: 100%; }

/* ---------- Buttons ------------------------------------------------------ */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap-min);
  padding: 0 20px;
  border-radius: var(--radius);
  border: 1px solid var(--steel);
  background: var(--panel-raised);
  color: var(--paper);
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.08s ease, background 0.15s ease;
}
.btn:active { transform: scale(0.97); }

/* Explicit press feedback triggered from JS on click — :active alone is
   easy to miss on a fast tap, especially on touch devices where it doesn't
   always render reliably. */
@keyframes btn-press-flash {
  0% { transform: scale(1); }
  40% { transform: scale(0.92); }
  100% { transform: scale(1); }
}
.btn-press-flash { animation: btn-press-flash 180ms ease; }

/* One-time highlight so a newly-added field is easy to spot after the
   list scrolls to it, without leaving any lasting visual change. */
@keyframes field-flash-once {
  0%, 100% { border-color: var(--paper); }
  30% { border-color: var(--yellow); }
}
.field-flash-once { animation: field-flash-once 650ms ease; }

.btn-primary {
  background: var(--yellow);
  border-color: var(--yellow);
  color: var(--asphalt);
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

.btn-danger { background: var(--red); border-color: var(--red); color: #fff; }
.btn-block { width: 100%; }
.btn-ghost { background: transparent; }
.btn:disabled { opacity: 0.4; pointer-events: none; }

/* ---------- Fields / forms ------------------------------------------------ */
.field { margin-bottom: 18px; }

/* A bit more breathing room between fields specifically in the actual
   form being filled out — not a global change, since .field is also used
   for login/admin/designer inputs elsewhere that don't need it. */
#fields-container .field { margin-bottom: 24px; }

.field-label {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-family: var(--font-display);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  font-size: 14px;
  margin-bottom: 6px;
  color: var(--paper);
}
.field-label .required { color: var(--red); }
.field-help { font-size: 13px; color: var(--paper-dim); margin-top: 4px; }

input[type="text"], input[type="number"], input[type="date"],
textarea, select {
  width: 100%;
  max-width: 100%;
  min-width: 0; /* same flex/grid "don't shrink below content" gotcha as the header (see that section's own long comment) can otherwise let a date input's internal mm/dd/yyyy segments force it wider than its container inside any flex/grid form layout, pushing it past the screen edge */
  min-height: var(--tap-min);
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 12px 14px;
  color: var(--paper);
}
textarea { min-height: 96px; resize: vertical; }
input:focus, textarea:focus, select:focus {
  outline: 2px solid var(--yellow);
  outline-offset: 1px;
}

/* Class Breaks/Graduated Colors/Graduated Symbols' own class-row
   number fields (min/max range, and Graduated Symbols' own per-class
   size + null-size) are already narrow by design — just wide enough
   for the handful of digits they're meant to hold. Chrome/Safari/Edge
   draw a native up/down spinner INSIDE a number input's own box by
   default, which on a field this narrow reads as a separate little
   button glued to the right edge, eating into the space actually
   available for the digits themselves (worst on the size fields,
   whose 1\u201360 range can need 2 digits) rather than sitting outside it
   the way it would on a normal full-width field. Every other number
   input in this app is either wide enough that this was never
   noticeable, or (like the date-adjacent ones above) already has its
   own more involved custom styling — this is the first place it's
   actually caused a usability problem worth fixing globally for these
   specific fields rather than leaving to browser defaults. */
.cls-min, .cls-max, .cls-size {
  -moz-appearance: textfield;
}
.cls-min::-webkit-outer-spin-button, .cls-min::-webkit-inner-spin-button,
.cls-max::-webkit-outer-spin-button, .cls-max::-webkit-inner-spin-button,
.cls-size::-webkit-outer-spin-button, .cls-size::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* Checkboxes/radios were left entirely to browser/OS defaults until
   now — on plenty of devices that's a plain black-bordered square,
   which is what "not black, modern and nice looking" is about.
   accent-color is a single, well-supported (all current major
   browsers) property that recolors the native control's checked state
   without needing to hand-roll a fully custom checkbox with extra
   markup/JS — the native control already looks reasonably modern
   (rounded corners, smooth check) once it isn't just black. Explicit
   width/height gives it a consistent size across browsers too, though
   any call site that already sets its own inline width (several do, to
   escape the general width:100% input rule above) keeps that instead,
   since inline styles win — accent-color/cursor still apply regardless. */
/* Fully custom-drawn (appearance: none) rather than relying on
   accent-color alone — accent-color only recolors the browser's OWN
   native widget, and evidently that wasn't landing reliably enough
   (still rendering as a plain black box on whatever this was tested
   on), so this draws the whole thing by hand instead: a bordered box
   (rounded for checkbox, circular for radio), filled yellow with a
   hand-drawn checkmark/dot when checked. This is guaranteed to look
   the same everywhere regardless of what any given browser/OS would
   otherwise have drawn natively. */
input[type="checkbox"], input[type="radio"] {
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  width: 16px;
  height: 16px;
  min-width: 16px;
  min-height: 16px;
  flex: none;
  margin: 0;
  padding: 0;
  border: 1.5px solid var(--steel);
  background: var(--panel-raised);
  cursor: pointer;
  position: relative;
  display: inline-block;
  vertical-align: middle;
  transition: background-color 0.12s ease, border-color 0.12s ease;
}
input[type="checkbox"] { border-radius: 4px; }
input[type="radio"] { border-radius: 50%; }
input[type="checkbox"]:checked, input[type="radio"]:checked {
  background: var(--yellow);
  border-color: var(--yellow);
}
input[type="checkbox"]:checked::after {
  content: '';
  position: absolute;
  left: 4.5px; top: 1.5px;
  width: 4px; height: 8px;
  border: solid var(--asphalt);
  border-width: 0 1.5px 1.5px 0;
  transform: rotate(45deg);
}
input[type="radio"]:checked::after {
  content: '';
  position: absolute;
  top: 50%; left: 50%;
  width: 6px; height: 6px;
  background: var(--asphalt);
  border-radius: 50%;
  transform: translate(-50%, -50%);
}
input[type="checkbox"]:disabled, input[type="radio"]:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}
input[type="checkbox"]:focus-visible, input[type="radio"]:focus-visible {
  outline: 2px solid var(--yellow);
  outline-offset: 2px;
}

/* Without this reset, iOS Safari renders its own circular swatch-with-ring
   for type="color", which distorts badly whenever a custom width/padding
   is applied (as every color picker in the Designer does) — this forces a
   clean, consistent rounded-rectangle swatch on every platform instead. */
input[type="color"] {
  -webkit-appearance: none;
  appearance: none;
  border: 1px solid var(--steel);
  border-radius: 6px;
  background: none;
  cursor: pointer;
}
input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; border-radius: 5px; }
input[type="color"]::-webkit-color-swatch { border: none; border-radius: 5px; }
input[type="color"]::-moz-color-swatch { border: none; border-radius: 5px; }

.field-error {
  border-color: var(--red) !important;
}
.field-error-msg { color: var(--red); font-size: 13px; margin-top: 6px; }

.choice-list { display: flex; flex-direction: column; gap: 8px; }
.choice-row {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--tap-min);
  padding: 10px 14px;
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
}
.choice-row input { width: 20px; height: 20px; accent-color: var(--yellow); }

.yesno-toggle { display: flex; gap: 8px; }
.yesno-toggle button {
  flex: 1; min-height: var(--tap-min);
  border-radius: var(--radius); border: 1px solid var(--steel);
  background: var(--panel-raised); color: var(--paper); font-weight: 600;
}
.yesno-toggle button.selected-yes { background: var(--green); border-color: var(--green); color: #08130d; }
.yesno-toggle button.selected-no { background: var(--red); border-color: var(--red); color: #fff; }

/* ---------- Field tag / status badge (signature element) ----------------
   Notched like a physical equipment tag; color communicates sync state. */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 12px;
  font-family: var(--font-display);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  clip-path: polygon(8px 0, 100% 0, 100% 100%, 8px 100%, 0 50%);
  background: var(--steel);
  color: var(--paper);
}
.tag-draft { background: var(--panel-raised); color: var(--paper-dim); border: 1px solid var(--steel); }
.tag-pending { background: var(--orange); color: #2b1206; }
.tag-synced { background: var(--green); color: #06170e; }
.tag-error { background: var(--red); color: #fff; }
.tag-published { background: var(--green); color: #06170e; }
.tag-draftform { background: var(--panel-raised); color: var(--paper-dim); border: 1px solid var(--steel); }
.tag-archived { background: var(--steel); color: var(--paper-dim); }

/* ---------- GPS accuracy reticle (signature element) ---------------------
   A rangefinder-style ring rather than a plain number. */
/* Photo field capture button (Capture > photo fields) — replaces the
   browser's own unstyled file-input "Choose File" button with something
   matching the rest of the app. The underlying <input type="file"> is
   still there and still functionally identical, just visually hidden
   behind this label (a well-established, accessible pattern — clicking
   or tapping the label activates the input exactly as if it were
   visible). */
.photo-capture-btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px;
  min-height: 44px;
  border-radius: var(--radius);
  border: 2px solid var(--steel);
  background: var(--panel-raised);
  color: var(--paper);
  cursor: pointer;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 13px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.photo-capture-btn:active { border-color: var(--yellow); background: rgba(47, 111, 237, 0.12); }
.photo-capture-btn svg { width: 20px; height: 20px; flex-shrink: 0; color: var(--yellow); }

/* Signature field (Capture > signature fields) — white background with
   dark ink is deliberate: a signature is expected to look like ink on
   paper, not blend into the surrounding UI, regardless of what that
   surrounding UI's own background happens to be. */
.signature-pad-wrap { position: relative; }
.signature-canvas {
  display: block;
  width: 100%;
  height: 160px;
  background: #ffffff;
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  /* Without this, a finger trying to sign on a touch device would also
     scroll/pan the page underneath it — the single most important line
     in this whole block. */
  touch-action: none;
  cursor: crosshair;
}
.signature-clear-btn {
  position: absolute;
  top: 8px;
  right: 10px;
  color: var(--paper-dim);
}
.answer-signature-img {
  max-width: 260px;
  max-height: 100px;
  background: #ffffff;
  border: 1px solid var(--steel);
  border-radius: 6px;
  padding: 4px;
}

/* ---------- Phase 3 field types: derived value display (calculated/
   lookup/gps_location) + repeating_section rows ------------------------ */
.derived-field-box {
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 10px 12px;
  min-height: 42px;
  display: flex;
  align-items: center;
  color: var(--paper);
}
.derived-field-box .card-meta { margin: 0; }
.repeating-rows { display: flex; flex-direction: column; gap: 10px; }
.repeating-row {
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 10px 12px;
}
.repeating-row-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 8px;
  padding-bottom: 6px;
  border-bottom: 1px solid var(--steel);
}
.repeating-row-fields .field:last-child { margin-bottom: 0; }

.gps-badge {
  display: flex; align-items: center; gap: 10px;
  padding: 8px 12px;
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  /* Resets needed now that this is a real <button>, for accessibility,
     rather than a plain div — buttons don't inherit font/cursor/alignment
     the way divs do by default. */
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  width: 100%;
}
.gps-chevron {
  margin-left: auto;
  color: var(--paper-dim);
  font-size: 11px;
  transition: transform 0.15s ease;
  flex-shrink: 0;
}
/* Collapsed: hide the ring and caption label, keep just the value — the
   whole point is reclaiming vertical space, and the ring (34px) plus the
   two-line text block is most of this badge's height. */
.gps-badge.collapsed {
  padding: 6px 12px;
}
.gps-badge.collapsed .gps-ring,
.gps-badge.collapsed .gps-caption {
  display: none;
}
.gps-badge.collapsed .gps-chevron {
  transform: rotate(-90deg);
}
.gps-ring {
  width: 34px; height: 34px; border-radius: 50%;
  position: relative;
  border: 2px solid var(--steel);
  display: flex; align-items: center; justify-content: center;
  flex-shrink: 0;
}
.gps-ring::before, .gps-ring::after {
  content: '';
  position: absolute;
  background: currentColor;
}
.gps-ring::before { width: 100%; height: 1px; }
.gps-ring::after { width: 1px; height: 100%; }
.gps-ring .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; z-index: 1; }
.gps-good { color: var(--green); border-color: var(--green); }
.gps-fair { color: var(--orange); border-color: var(--orange); }
.gps-poor { color: var(--red); border-color: var(--red); }
.gps-text { font-family: var(--font-mono); font-size: 13px; }
.gps-text .gps-caption { display: block; font-family: var(--font-display); font-size: 11px; letter-spacing: 0.06em; color: var(--paper-dim); text-transform: uppercase; }

/* ---------- Cards / lists -------------------------------------------------- */
.card {
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 10px;
}
.card-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }

/* ---------- Feature Layer detail screen restyle (item 6) -------------
   Header card + icon-badged accordion sections replacing the previous
   plain h1/<details> markup — purely visual, none of the actual
   section content/functionality changed (see admin.js's own
   layerSectionHtml() docblock). */
.layer-detail-header {
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 18px 20px;
  margin-bottom: 16px;
}
.layer-detail-title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 26px;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin: 0 0 4px;
}
.layer-detail-subtitle { margin: 0 0 12px; }
.layer-detail-badges { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.layer-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border-radius: 20px;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.02em;
}
.layer-badge-geom { background: rgba(47, 111, 237, 0.12); color: var(--yellow); text-transform: uppercase; }
/* No backend "layer status" concept exists in this app (a layer row
   only exists here once it's already fully connected) — "Ready" is
   therefore a fixed, always-true label rather than something read off
   the layer object, not a state machine this is silently pretending
   to track. */
.layer-badge-ready { background: rgba(76, 167, 113, 0.12); color: var(--green); }
.layer-badge-ready::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: var(--green); display: inline-block; flex-shrink: 0; }

.layer-section {
  position: relative;
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  margin-bottom: 12px;
  overflow: hidden;
}
.layer-section > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 14px 16px;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 16px;
  color: var(--paper);
}
.layer-section > summary::-webkit-details-marker { display: none; } /* Safari/Chrome's own default triangle marker — replaced by .layer-section-chevron below */
.layer-section > summary::marker { content: ''; } /* Firefox's equivalent */
.layer-section-icon {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--yellow);
}
.layer-section-icon.icon-danger { color: var(--red); }
.layer-section-label { flex: 1; }
.layer-section-danger > summary { color: var(--red); }
.layer-section-chevron {
  flex-shrink: 0;
  display: flex;
  color: var(--paper-dim);
  transition: transform 0.15s ease;
}
.layer-section[open] > summary .layer-section-chevron { transform: rotate(180deg); }
.layer-section[open] > summary {
  background: rgba(47, 111, 237, 0.06);
  border-bottom: 1px solid var(--steel);
}
.layer-section-danger[open] > summary { background: rgba(216, 72, 58, 0.06); }
.layer-section-body { padding: 12px 16px 18px; }
/* Sits directly over the summary row's own right side (item 8's own
   "+ Add Field" header action) — a sibling of <summary>, not nested
   inside it, so clicking it doesn't also toggle the section open/
   closed the way a click anywhere inside <summary> would. <details>'s
   own native behavior already hides every child but <summary> while
   closed, so this needs no [open]-specific rule of its own to stay
   hidden alongside the rest of the body when collapsed. */
.layer-section-header-action {
  position: absolute;
  top: 10px;
  right: 44px; /* clears the chevron sitting at the row's far right */
}

/* Style-editor sub-sections (item 2: Symbology / Scale Range / Labels,
   inside Map Creator's Configure Layer panel's own "Style" section —
   see featureLayerStyleEditor.js's mountFeatureLayerStyleEditor and
   mapStylePicker.js's renderScaleAndLabelPicker). A lighter-weight
   sibling of .layer-section above (no icon slot, no danger variant —
   not needed here) using the same "bordered box, chevron rotates on
   open" language so nested collapsibles read as clearly related to the
   outer Style/Filters/Popup Fields sections without being visually
   confused for them. Collapsed by default: callers simply omit the
   `open` attribute. */
.fls-subsection {
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  margin-bottom: 10px;
  overflow: hidden;
  background: var(--panel-raised, var(--panel));
}
.fls-subsection-summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 14px;
  font-weight: 600;
  font-size: 13px;
  color: var(--paper);
}
.fls-subsection-summary::-webkit-details-marker { display: none; }
.fls-subsection-summary::marker { content: ''; }
.fls-subsection-summary::after {
  content: '';
  margin-left: auto;
  width: 8px;
  height: 8px;
  flex-shrink: 0;
  border-right: 2px solid var(--paper-dim);
  border-bottom: 2px solid var(--paper-dim);
  transform: rotate(45deg);
  transition: transform 0.15s ease;
}
.fls-subsection[open] > .fls-subsection-summary::after { transform: rotate(-135deg); }
.fls-subsection[open] > .fls-subsection-summary {
  border-bottom: 1px solid var(--steel);
  background: rgba(47, 111, 237, 0.06);
}
.fls-subsection-body { padding: 12px; }

/* Non-collapsible variant (§ direct request — Map Creator's own
   Symbology/Scale Range/Labels/Popups/Filters/Preview left-nav
   already IS the "which section am I looking at" affordance now (see
   architecture.md §123) — an ADDITIONAL collapse/expand toggle on the
   one section a person specifically navigated to see was a leftover
   from when these all had to share one scrolling accordion panel
   together, and just reads as a confusing, pointless second click
   now that each section gets its own whole screen). Same header
   row/icon/title/desc markup, just <div>s instead of <details>/
   <summary> (so there's nothing to click-to-toggle), no chevron, and
   permanently styled the same as `[open]` above rather than plain,
   since it's never anything BUT open. `overflow: visible` — unlike
   the collapsible variant, nothing here needs to clip a collapse
   animation, and this codebase has a specific prior bug where
   `overflow: hidden` on a container silently clipped a dropdown/
   context menu rendered inside it (see architecture.md's own "Key
   learnings" note) — no reason to reintroduce that risk for a
   container that no longer collapses at all. */
.fls-subsection-static { overflow: visible; }
.fls-subsection-summary-static {
  cursor: default;
  border-bottom: 1px solid var(--steel);
  background: rgba(47, 111, 237, 0.06);
}
.fls-subsection-summary-static::after { display: none; }

/* Save Style sticky footer (§ direct request) — pinned to the bottom
   of whatever scrolling ancestor it's mounted inside (Map Creator's
   own .map-builder-config-panel-scroll on desktop, tasks.css's
   .task-modal bottom sheet on mobile — see the mobile-specific rule
   below) via position:sticky, which needs no DOM restructuring the way
   the Cancel/Apply footer's plain-flex-sibling approach does — this
   component (featureLayerStyleEditor.js) doesn't own either shell, so
   sticky is the only option available without reaching into a caller's
   own layout.
   ROOT CAUSE of the still-visible gap (confirmed with a Playwright
   repro against this exact CSS before landing this fix, not just
   reasoned about — see architecture.md): the previous version tried to
   cancel the scroll container's own bottom padding with a negative
   `margin-bottom` on this element, on the theory that `bottom: 0`
   sticks to the padding edge and a matching negative margin would
   stretch the box the rest of the way to the container's true (border)
   edge. That theory is right for the LEFT/RIGHT margins (negative
   side margins genuinely do widen a block box, confirmed empirically
   too — those still work and are unchanged below) but WRONG for
   margin-bottom specifically: this element is the last child with
   nothing after it, and margin-bottom only ever affects the gap to
   whatever follows an element, never the element's OWN position or
   size — a Playwright measurement confirmed the gap stayed EXACTLY
   the container's bottom padding (16px) no matter what margin-bottom
   was set to, including 0. The actual fix is a NEGATIVE `bottom` sticky
   offset instead (`bottom: -16px`, not `bottom: 0`) — that genuinely
   does shift the stuck box past the padding edge to the container's
   true bottom, confirmed to close the gap to exactly 0px. */
.fls-sticky-save-footer {
  position: sticky;
  bottom: -16px;
  z-index: 2;
  background: var(--panel);
  border-top: 1px solid var(--steel);
  padding: 12px 16px 16px;
  margin: 4px -16px 0; /* left/right still cancel the container's own 16px side padding (this part always worked); no bottom margin — see this rule's own comment for why that was never doing anything */
  display: flex;
  align-items: center;
}
/* Same footer, mounted inside the mobile "Layer Config" bottom sheet
   instead (mapBuilder.js's own #lc-style/#rl-style — openModal() from
   taskCreateModal.js, not the desktop .map-builder-config-panel-scroll
   panel the base rule above was tuned for). .task-modal's own padding
   is 16px 14px 24px (tasks.css) — different side/bottom insets than
   the base rule's 16px/16px, so it needs its own matching offset/
   margin, same "negative `bottom`, not `margin-bottom`" fix as above. */
.task-modal .fls-sticky-save-footer {
  bottom: -24px;
  margin: 4px -14px 0;
  padding: 12px 14px 24px;
}

/* Auto-update checkbox (§ direct request to restore this — see
   mountFeatureLayerStyleEditor's own comment for the history) — same
   .designer-check row every other inline checkbox+label pairing in
   this panel already uses, just with more breathing room since it
   sits alone at the top of the section rather than beside a color
   swatch. */
.fls-auto-update-check {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--paper-dim);
}

/* Icon + stacked title/description — the row content INSIDE
   .fls-subsection-summary (see mapBuilder.js's own flsRowHtml() helper,
   and the equivalent inline markup in featureLayerStyleEditor.js's
   Symbology summary / mapStylePicker.js's Scale Range & Labels
   summaries). A bold title with a smaller gray description line
   beneath it, and this element's own chevron (still drawn by
   .fls-subsection-summary::after above) at the far right. No badge
   background behind the icon itself — bare currentColor glyph only, by
   direct request. `.fls-row-desc` is simply omitted by the caller for a
   section with no description (the top-level "Style" row itself, in
   the reference screenshot) — nothing here needs to conditionally hide
   it. */
.fls-row-icon {
  flex-shrink: 0;
  width: 22px;
  height: 22px;
  color: #2f6fed;
  display: flex;
  align-items: center;
  justify-content: center;
}
.fls-row-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}
.fls-row-title { font-weight: 700; font-size: 14px; color: var(--paper); }
.fls-row-desc { font-weight: 400; font-size: 12px; color: var(--paper-dim); line-height: 1.35; }

/* Save buttons inside the Style sub-sections (Symbology's "Save Style",
   Scale Range & Labels' shared "Save") — icon + label, matching the
   reference screenshot's own "Save Style" button treatment. */
.fls-save-btn { display: inline-flex; align-items: center; gap: 8px; }


/* ---------- Manage Fields table (items 8/9) --------------------------
   Field Name / Type / Alias / Domain / Actions columns via CSS grid,
   shared between the header row and every field row so they line up.
   Leading blank column matches the visual drag-handle glyph in front
   of each row's field name. 1fr for Alias/Domain (the two variable-
   width text columns) with a fixed max-width on the domain <select>
   inside an expanded row (see that select's own max-width in
   admin.js) is what item 9 actually asks for: it must NOT stretch to
   the full grid column width the way a bare `select { width:100% }`
   naturally would on a wide desktop viewport. */
.fields-table-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.fields-table-search {
  position: relative;
  flex: 1 1 240px;
  min-width: 180px;
  max-width: 360px;
}
.fields-table-search svg { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--paper-dim); pointer-events: none; }
/* input[type="search"] specifically (not just `input`) so this beats
   the desktop-narrow-inputs override further down this file
   (`input[type="search"] { padding: 8px 12px; }` inside its own
   @media (min-width:1024px) block) on specificity rather than losing
   to it on source order — that override was resetting padding-left
   back to a plain 12px on desktop, right on top of the icon, which is
   exactly the reported "icon covers the placeholder text" bug. Equal
   specificity between two rules is decided by source order, and that
   override happens to load later in this file; adding [type="search"]
   here makes this rule strictly more specific so it wins regardless of
   where either rule sits. */
.fields-table-search input[type="search"] { padding-left: 36px; }
.fields-table-system-toggle { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.fields-table-header, .fields-table-row-summary {
  display: grid;
  /* Actions is a FIXED width (not auto) on purpose — each row's
     .fields-table-row-summary is its own separate grid container (not
     one shared grid spanning the header + every row), so CSS grid's
     `auto` track sizing was being computed independently per row based
     on THAT row's own content: some rows render two icon buttons
     (edit+delete), some render one, some render none (system fields),
     so the Actions column's width silently varied row to row and never
     matched the header's own "Actions" text width either — the actual
     cause of the reported column misalignment. A fixed width is
     identical in the header and every row regardless of how many
     buttons that particular row happens to have. 76px comfortably
     fits two 30px icon buttons plus their 4px gap (see
     .fields-table-col-actions) with a little breathing room. */
  grid-template-columns: 24px 1.2fr 0.8fr 1.8fr 76px;
  gap: 10px;
  align-items: center;
}
.fields-table-col-grip { display: flex; align-items: center; color: var(--steel); cursor: default; }
.fields-table-header {
  padding: 8px 10px;
  border-bottom: 1px solid var(--steel);
  color: var(--paper-dim);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.fields-table-header-actions { text-align: right; }
.fields-table-row { border-bottom: 1px solid var(--steel); }
.fields-table-row:last-child { border-bottom: none; }
.fields-table-row-summary { padding: 10px; }
.fields-table-col-actions { display: flex; justify-content: flex-end; gap: 4px; }
.fields-type-badge { background: rgba(47, 111, 237, 0.1); color: var(--yellow); text-transform: uppercase; font-size: 11px; }
.fields-type-badge.fields-type-date, .fields-type-badge.fields-type-timestamp, .fields-type-badge.fields-type-timestamptz { background: rgba(76, 167, 113, 0.12); color: var(--green); }
/* "Display Name / Domain" combined column — a small tag naming what's
   special about a field (see admin.js's own displayDomainCellHtml
   comment) stacked above the actual value text, or a plain em dash for
   a field that's neither. */
.fields-table-col-display { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; }
.fields-tag {
  display: inline-block;
  padding: 1px 8px;
  border-radius: 10px;
  font-size: 10px;
  font-weight: 700;
  text-transform: none;
  letter-spacing: 0.02em;
}
.fields-tag-display { background: var(--panel-raised); color: var(--paper-dim); border: 1px solid var(--steel); }
.fields-tag-domain { background: rgba(138, 111, 214, 0.12); color: #8a6fd6; }
.fields-tag-value { font-size: 14px; color: var(--paper); }
.fields-row-icon-btn {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--paper-dim);
  cursor: pointer;
}
.fields-row-icon-btn:hover { background: var(--panel-raised); color: var(--yellow); }
.fields-row-icon-btn-danger:hover { color: var(--red); }
/* Below the grid's own breakpoint, the row STAYS one row (item 5) —
   Field Name / Type / Display-Domain / Actions all stay side by side
   instead of stacking into a single column. The grip is still hidden
   (purely decorative desktop-density affordance, no room for it on a
   phone either way). Field Name and the Display/Domain value both get
   `min-width: 0` + ellipsis truncation — grid items, like flex items,
   default to a min-width equal to their own content's intrinsic width
   (same reasoning architecture.md's flex-layout-discipline rule calls
   out for nested flex chains), so without this a long column/value
   name would silently push the row wider than the screen instead of
   truncating; the Type badge and Actions icons stay their natural
   (auto) width since they're short and self-contained either way. */
@media (max-width: 720px) {
  .fields-table-header { display: none; }
  .fields-table-row-summary {
    grid-template-columns: 1fr auto 1fr auto;
    column-gap: 8px;
    row-gap: 0;
  }
  .fields-table-col-grip { display: none; }
  .fields-table-col-name,
  .fields-table-col-display {
    min-width: 0;
    overflow: hidden;
  }
  .fields-table-col-name {
    white-space: nowrap;
    text-overflow: ellipsis;
  }
  .fields-table-col-display .fields-tag-value {
    display: block;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .fields-table-col-actions { justify-content: flex-end; margin-top: 0; }
}

/* ---------- Map Style "Symbol Type" custom dropdown (item 2) --------
   Custom button+popover, not a native <select> — no browser supports
   rendering an icon inside an <option>, and this needs one next to
   every option's text (see admin.js's own rendererTypePickerHtml()
   docblock). */
.renderer-type-picker { position: relative; }
.renderer-type-btn {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  color: var(--paper);
  font-size: 14px;
  cursor: pointer;
  min-height: var(--tap-min);
}
.renderer-type-btn-label { flex: 1; text-align: left; }
.renderer-type-chevron { flex-shrink: 0; color: var(--paper-dim); display: flex; }
.renderer-type-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 20px; height: 20px; }
.renderer-type-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 5;
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0,0,0,0.18);
  padding: 4px;
  max-height: 320px;
  overflow-y: auto;
}
.renderer-type-option {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 9px 10px;
  background: transparent;
  border: none;
  border-radius: 8px;
  color: var(--paper);
  font-size: 14px;
  text-align: left;
  cursor: pointer;
}
.renderer-type-option:hover:not(:disabled) { background: var(--panel-raised); }
.renderer-type-option-label { flex: 1; }
.renderer-type-option-disabled, .renderer-type-option:disabled {
  color: var(--paper-dim);
  cursor: not-allowed;
  opacity: 0.6;
}
.renderer-type-soon {
  flex-shrink: 0;
  font-size: 10px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  padding: 2px 6px;
  border-radius: 10px;
  background: var(--steel);
  color: var(--panel);
}

/* ---------- Shared restyled Admin list screens (Domains, Teams,
   Manage Fields — items 4/7/8) ---------------------------------------- */
.admin-header-row {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 14px;
  flex-wrap: wrap;
}
.admin-header-action { flex-shrink: 0; white-space: nowrap; }
.admin-list-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 16px 18px;
  margin-bottom: 10px;
  cursor: pointer;
  min-height: var(--tap-min);
}
.admin-list-row-title { font-family: var(--font-display); font-weight: 700; font-size: 16px; }
.admin-list-row-chevron { flex-shrink: 0; color: var(--paper-dim); display: flex; }
.admin-dashed-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: var(--tap-min);
  padding: 12px;
  margin-top: 6px;
  background: rgba(47, 111, 237, 0.04);
  border: 1.5px dashed var(--yellow);
  border-radius: var(--radius);
  color: var(--yellow);
  font-weight: 700;
  cursor: pointer;
}
.admin-info-note {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  margin-top: 16px;
  padding: 12px 14px;
  background: rgba(47, 111, 237, 0.06);
  border: 1px solid rgba(47, 111, 237, 0.25);
  border-radius: var(--radius);
  color: var(--paper-dim);
  font-size: 13px;
}
.admin-info-note-icon { flex-shrink: 0; color: var(--yellow); margin-top: 1px; }
.admin-icon-btn { display: inline-flex; align-items: center; gap: 8px; }
.admin-icon-btn svg { flex-shrink: 0; }

/* Simple segmented tab control (Users/Roles — item 6; reused wherever
   else a restyled admin screen needs a two-way toggle). */
.admin-tabs { display: flex; gap: 4px; background: var(--panel-raised); border: 1px solid var(--steel); border-radius: var(--radius); padding: 4px; width: fit-content; }
.admin-tab {
  padding: 8px 18px;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--paper-dim);
  font-weight: 700;
  font-size: 14px;
  cursor: pointer;
}
.admin-tab.active { background: var(--panel); color: var(--yellow); box-shadow: 0 1px 3px rgba(0,0,0,0.08); }

/* ---------- Users list row (item 6) ---------------------------------- */
.user-row-identity { display: flex; align-items: center; gap: 12px; min-width: 0; }
.user-avatar {
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #fff;
  font-weight: 700;
  font-size: 13px;
  font-family: var(--font-display);
}
.user-row-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.user-role-badge { background: rgba(47, 111, 237, 0.1); color: var(--yellow); text-transform: none; }
.user-badge-disabled { background: rgba(216, 72, 58, 0.1); color: var(--red); }
.user-badge-disabled::before { content: none; }
/* Mirrors .tag-pending's own orange "not finished yet" treatment —
   shown on a Users row whose password was set/reset by an admin and
   hasn't been replaced with one the person chose themselves yet. See
   AuthMiddleware::requireAuth() on the backend, which is what actually
   enforces this, not just what this badge is reporting. */
.user-badge-pending { background: rgba(232, 98, 44, 0.12); color: var(--orange); }
.user-badge-pending::before { content: none; }
.user-row-menu-wrap { position: relative; }
.user-row-menu-btn {
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--paper-dim);
  cursor: pointer;
}
.user-row-menu-btn:hover { background: var(--panel-raised); }
.user-row-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 5;
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  box-shadow: 0 6px 20px rgba(0,0,0,0.18);
  padding: 4px;
  min-width: 130px;
}
.user-row-menu-item {
  width: 100%;
  text-align: left;
  padding: 8px 10px;
  background: transparent;
  border: none;
  border-radius: 6px;
  color: var(--paper);
  font-size: 14px;
  cursor: pointer;
}
.user-row-menu-item:hover:not(:disabled) { background: var(--panel-raised); }
.user-row-menu-item-danger { color: var(--red); }
.user-row-menu-item:disabled { color: var(--paper-dim); cursor: not-allowed; opacity: 0.6; }

/* ---------- Domain detail screen (item 5) ----------------------------- */
.btn-outline {
  background: transparent;
  border: 1.5px solid var(--yellow);
  color: var(--yellow);
}
.btn-outline:active { background: rgba(47, 111, 237, 0.08); }
.btn-outline-danger {
  background: transparent;
  border: 1.5px solid var(--red);
  color: var(--red);
  flex-shrink: 0;
}
.btn-outline-danger:active { background: rgba(216, 72, 58, 0.08); }
.domain-values-header, .domain-values-row {
  display: grid;
  /* Fixed Actions width, not auto — same reasoning as Manage Fields'
     own .fields-table-header/.fields-table-row-summary fix: each row
     is its own separate grid container, so an `auto` column's width is
     computed independently per row based on THAT row's own button
     count (existing rows show Save+Delete, the trailing "new value"
     row shows only "+ Add value"), which silently misaligned the
     Actions column against the header and between rows. */
  grid-template-columns: 64px 1fr 1fr 140px;
  gap: 10px;
  align-items: center;
}
.domain-values-header {
  padding: 6px 4px;
  border-bottom: 1px solid var(--steel);
  color: var(--paper-dim);
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.domain-values-row { padding: 8px 4px; border-bottom: 1px solid var(--steel); }
.domain-values-row-new { border-bottom: none; padding-top: 12px; }
.domain-order-buttons { display: flex; flex-direction: column; gap: 2px; }
.domain-order-btn {
  width: 22px;
  height: 18px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: none;
  color: var(--paper-dim);
  font-size: 9px;
  cursor: pointer;
  padding: 0;
}
.domain-order-btn:hover:not(:disabled) { color: var(--yellow); }
.domain-order-btn:disabled { opacity: 0.3; cursor: not-allowed; }
.domain-values-actions { display: flex; gap: 6px; justify-content: flex-end; }
@media (max-width: 720px) {
  .domain-values-header { display: none; }
  .domain-values-row, .domain-values-row-new { grid-template-columns: 1fr; row-gap: 6px; }
  .domain-values-actions { justify-content: flex-start; }
}
.admin-danger-box {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-top: 16px;
  padding: 16px 18px;
  background: rgba(216, 72, 58, 0.05);
  border: 1.5px solid rgba(216, 72, 58, 0.4);
  border-radius: var(--radius);
}
.admin-danger-box-icon {
  flex-shrink: 0;
  width: 38px;
  height: 38px;
  border-radius: 50%;
  background: rgba(216, 72, 58, 0.12);
  color: var(--red);
  display: flex;
  align-items: center;
  justify-content: center;
}
.admin-danger-box-text { flex: 1; min-width: 0; }
.admin-danger-box-title { font-weight: 700; color: var(--red); margin-bottom: 2px; }
.admin-icon-btn { display: inline-flex; align-items: center; gap: 8px; }
.admin-icon-btn svg { flex-shrink: 0; }

/* ---------- Branding screen theme color fields (item 5) --------------
   Wraps a real `<input type="color">` so it renders as a small square
   swatch instead of the browser's own larger default color-input
   chrome, with a synced hex text field and (where the EyeDropper API
   exists) an eyedropper button alongside it — see admin.js's own
   themeColorFieldHtml()/wireThemeColorControls() for the JS side. */
.theme-color-field {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--panel-raised);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 6px;
}
.theme-color-swatch-wrap {
  flex-shrink: 0;
  width: 36px;
  height: 36px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--steel);
}
.theme-color-swatch {
  width: 48px;
  height: 48px;
  margin: -6px;
  padding: 0;
  border: none;
  cursor: pointer;
  background: none;
}
.theme-color-hex {
  flex: 1;
  min-width: 0;
  width: auto;
  min-height: 34px;
  background: transparent;
  border: none;
  padding: 0 4px;
  text-transform: uppercase;
}
.theme-color-hex:focus { outline: none; }
.theme-color-eyedrop-btn {
  flex-shrink: 0;
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: 8px;
  color: var(--paper-dim);
  cursor: pointer;
}
.theme-color-eyedrop-btn:active { background: var(--steel); }

/* Answers card(s) on a record's detail page — each field gets uniform
   padding instead of the global .field margin, with a hairline border
   between fields (not around the whole card, which .card already
   provides). A class rather than an ID because a form with sections
   splits its answers into multiple separate card segments — one per
   run of consecutive real-answer fields, broken up by section headings/
   dividers/text blocks in between (see buildAnswersHtml in viewer.js).
   Scoped so this doesn't affect .field anywhere else in the app (login,
   admin, Designer, Capture all rely on its normal margin-only spacing). */
.answers-card-segment .field {
  margin-bottom: 0;
  padding: 14px 0;
}
.answers-card-segment .field:first-child { padding-top: 0; }
.answers-card-segment .field:last-child { padding-bottom: 0; }
.answers-card-segment .field + .field { border-top: 1px solid rgba(0, 0, 0, 0.08); }

/* Bordered section on a record's detail page / PDF export — the actual
   border-color is set inline per-section (each section's own color from
   the Designer, see buildAnswersHtml in viewer.js); this just defines
   the shape. The heading sits inside the border with a bolder,
   uppercase, larger treatment and its own matching-colored underline —
   deliberately much louder than a regular field label, so it reads
   unmistakably as a section header, not just another field. */
.answers-section {
  border: 2px solid var(--yellow);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin: 18px 0;
}
.answers-section-heading {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 17px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding-bottom: 8px;
  margin-bottom: 12px;
  border-bottom: 2px solid var(--yellow);
}
.card-title { font-weight: 600; margin-bottom: 4px; }
.card-meta { font-size: 13px; color: var(--paper-dim); }

.list-page { padding: 12px 14px 24px; }

/* Keeps Add Field / Add Separator reachable without scrolling back up
   through a long field list. Negative side margins cancel out .list-page's
   own padding so the panel spans full width edge-to-edge, with matching
   padding re-added so its content still lines up with everything else. */
.fields-sticky-header {
  position: sticky;
  top: 0;
  z-index: 5;
  background: var(--asphalt);
  margin: 0 -14px 10px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--steel);
}

/* ---------- Bottom sheet --------------------------------------------------- */
.sheet-backdrop {
  position: fixed; inset: 0; background: rgba(0,0,0,0.55);
  z-index: 40;
}
.sheet {
  position: fixed; left: 0; right: 0; bottom: 0;
  max-height: 92vh;
  background: var(--panel);
  border-top: 1px solid var(--steel);
  border-radius: 16px 16px 0 0;
  z-index: 41;
  display: flex;
  flex-direction: column;
  padding-bottom: env(safe-area-inset-bottom);
}
.sheet-handle {
  width: 42px; height: 5px; border-radius: 3px;
  background: var(--steel);
  margin: 10px auto 4px;
}
.sheet-header {
  padding: 6px 18px 14px;
  border-bottom: 1px solid var(--steel);
}
.sheet-body { padding: 16px 18px; overflow-y: auto; }

/* Two-column form layout (opt-in per form, see Designer > Formatting).
   Single column below 480px regardless — no cramming fields on a phone. */
.fields-two-col {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0 16px;
  align-content: start;
}
@media (min-width: 480px) {
  .fields-two-col { grid-template-columns: 1fr 1fr; }
}

/* Collapsible section headings (Designer > field > Section heading >
   Collapsible). The heading itself gets cursor:pointer + flex layout
   inline (capture.js), this just styles the chevron and gives the
   collapsing body a bit of breathing room. */
.section-chevron {
  display: inline-block;
  font-size: 14px;
  color: var(--yellow);
  width: 1em;
  flex-shrink: 0;
}
/* .section-group is the section's own bordered/backgrounded box — the
   heading (label) and its fields now share this one box (see
   renderFieldsGrouped in capture.js) rather than the heading sitting
   above it, outside the border. .section-body is purely the inner fields
   container now: no border/radius of its own, just enough padding so
   fields don't crowd the heading or the box's own edges. */
.section-group {
  margin: 4px 0 24px;
  padding: 12px 14px 4px;
  border-radius: var(--radius);
}
.section-body {
  padding: 0;
}

.sheet-footer {
  padding: 12px 18px;
  border-top: 1px solid var(--steel);
  display: flex; gap: 10px;
}

/* =====================================================================
   Form Layout customization (Designer > Layout tab)
   =====================================================================
   Every rule below is scoped under `.form-render-root` — the class
   capture.js's `.sheet`, the Designer's `.designer-preview-frame`, and
   the legacy full-screen `showFormPreview` overlay's `.sheet` all carry
   (see fieldStyle.js's layoutStyleCss()/layoutDataAttrs()) — so none of
   this leaks into the app's other uses of `.field`/`.btn`/`input` etc.
   (login, admin, the Designer's own canvas rows). CSS custom properties
   are set inline per-form via layoutStyleCss(); every rule here reads
   them with a fallback matching the PRE-EXISTING hardcoded value, so a
   form with none of these settings configured (i.e. every form that
   existed before this feature shipped) renders pixel-identical to
   before — `resolveLayoutConfig`'s defaults exist for the JS/validation
   side of that guarantee, these fallbacks are the CSS side of it. */

.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}

/* Establishes a CSS containment context sized to THIS element's own
   width, not the browser viewport — what makes the mobile-fallback rules
   below (Left-of-Field auto-reverting to Above, sticky nav, larger
   photos) respond correctly inside the Designer's Preview device frame,
   which is a fixed-width box on a normal-width desktop browser and would
   never trigger a plain `@media (max-width: ...)` query even when its
   "Mobile" tab is selected. Real Capture on an actual phone gets the
   same rules from BOTH the @container queries below and the @media ones
   already used elsewhere in this file — redundant there, but harmless,
   since they set identical properties. */
.form-render-root { container-type: inline-size; }

/* ---- Density (field/section/nav spacing) ---- */
.form-render-root #fields-container .field,
.form-render-root #preview-fields .field,
.form-render-root #preview-fields-container .field {
  margin-bottom: var(--form-field-gap, 24px);
}
.form-render-root .field-label { margin-bottom: var(--form-label-gap, 6px); }
.form-render-root .section-group {
  padding: var(--form-section-pad, 12px 14px 4px);
  margin: 4px 0 var(--form-section-gap, 24px);
}
.form-render-root .sheet-footer { padding: var(--form-nav-pad, 12px 18px); }

/* ---- Field size + border radius ---- */
.form-render-root input[type="text"], .form-render-root input[type="email"],
.form-render-root input[type="tel"], .form-render-root input[type="url"],
.form-render-root input[type="number"], .form-render-root input[type="date"],
.form-render-root textarea, .form-render-root select {
  min-height: var(--form-field-height, var(--tap-min));
  padding: var(--form-field-pad-v, 12px) var(--form-field-pad-h, 14px);
  border-radius: var(--form-radius, var(--radius));
}
.form-render-root textarea { min-height: calc(var(--form-field-height, var(--tap-min)) * 1.6); }
.form-render-root .btn {
  min-height: var(--form-field-height, var(--tap-min));
  border-radius: var(--form-radius, var(--radius));
}
.form-render-root .choice-row,
.form-render-root .yesno-toggle button,
.form-render-root .photo-capture-btn,
.form-render-root .signature-pad-wrap,
.form-render-root .derived-field-box {
  border-radius: var(--form-radius, var(--radius));
}
.form-render-root .choice-row { min-height: var(--form-field-height, var(--tap-min)); }
.form-render-root .yesno-toggle button { min-height: var(--form-field-height, var(--tap-min)); }

/* Theme colors — primary drives the submit/primary button, success/
   warning/danger drive the matching semantic UI states. accentColor
   keeps controlling --yellow (focus rings, required-marker, links)
   exactly as before; these are the three genuinely NEW semantic colors
   the spec asked for, layered on without touching what --yellow already
   does. */
.form-render-root .btn-primary {
  background: var(--form-primary, var(--yellow));
  border-color: var(--form-primary, var(--yellow));
  color: var(--form-primary-fg, var(--asphalt));
}
.form-render-root .yesno-toggle button.selected-yes {
  background: var(--form-success, var(--green));
  border-color: var(--form-success, var(--green));
  color: var(--form-success-fg, #08130d);
}
.form-render-root .yesno-toggle button.selected-no {
  background: var(--form-danger, var(--red));
  border-color: var(--form-danger, var(--red));
  color: var(--form-danger-fg, #fff);
}
.form-render-root .field-error { border-color: var(--form-danger, var(--red)) !important; }
.form-render-root .field-error-msg { color: var(--form-danger, var(--red)); }

/* ---- Section style ---- */
/* Flat (default) intentionally adds nothing here — a section's own
   border (sectionBorderCss(), always inline, unchanged by this feature)
   is exactly what every form already looked like, so "flat" IS the
   pre-existing appearance. Card/Outlined/Filled Header layer additional
   CSS-only treatment on top of .section-group without touching that
   inline border or the section's semantic structure (still one heading
   + one body div, just sharing a single bordered wrapper now instead of
   the heading sitting above the border). */
.form-render-root[data-section-style="card"] .section-group {
  border-radius: var(--form-radius, 10px);
  box-shadow: 0 4px 14px rgba(0,0,0,0.35);
  background: var(--panel-raised);
}
.form-render-root[data-section-style="outlined"] .section-group {
  box-shadow: none;
  background: transparent;
  border-radius: var(--form-radius, 4px);
}
/* Filled Header: the heading bleeds edge-to-edge at the very top of the
   group (negative margin equal to the group's own horizontal padding),
   so it reads as a filled bar spanning the section's full width rather
   than a smaller label floating inside the padded area. overflow:hidden
   on the group clips its corners to match the group's own border-radius
   even if the two radii aren't pixel-identical. */
.form-render-root[data-section-style="filled_header"] .section-group {
  padding-top: 0;
  overflow: hidden;
}
.form-render-root[data-section-style="filled_header"] .field[data-field-type="section"] {
  margin: 0 calc(-1 * var(--form-section-pad-h, 14px)) 10px;
  padding: 10px var(--form-section-pad-h, 14px);
  background: var(--panel-raised);
  border-left: 4px solid var(--yellow);
}
.form-render-root[data-section-style="filled_header"] .field[data-field-type="section"] h2 {
  margin: 0;
}

/* ---- Label position ---- */
/* Left of Field: label and control share a row. Only applied per-field
   via resolveLabelMode()'s data-label-mode (set in capture.js/renderField)
   — a field type Left doesn't suit (choice lists, photo, signature,
   repeating, etc.) always keeps data-label-mode="above" regardless of
   this setting, so this selector simply never matches those. */
.form-render-root .field[data-label-mode="left"] {
  display: flex;
  align-items: flex-start;
  gap: 12px;
}
.form-render-root .field[data-label-mode="left"] > .field-label {
  flex: 0 0 var(--form-label-width, 130px);
  margin-bottom: 0;
  padding-top: calc(var(--form-field-pad-v, 12px) + 2px);
}
.form-render-root .field[data-label-mode="left"] > input,
.form-render-root .field[data-label-mode="left"] > select,
.form-render-root .field[data-label-mode="left"] > .yesno-toggle {
  flex: 1 1 auto;
  min-width: 0;
}
/* Small screens: Left of Field would cramp both the label and the
   control, so it automatically falls back to Above Field — no JS resize
   listener needed, this just responds live to viewport width. */
@media (max-width: 640px) {
  .form-render-root .field[data-label-mode="left"] {
    flex-direction: column;
    gap: var(--form-label-gap, 6px);
  }
  .form-render-root .field[data-label-mode="left"] > .field-label {
    flex-basis: auto;
    padding-top: 0;
  }
}
@container (max-width: 640px) {
  .form-render-root .field[data-label-mode="left"] {
    flex-direction: column;
    gap: var(--form-label-gap, 6px);
  }
  .form-render-root .field[data-label-mode="left"] > .field-label {
    flex-basis: auto;
    padding-top: 0;
  }
}

/* Floating Label: control renders BEFORE its label in the DOM (see
   capture.js's wrapControl()) so this sibling selector can reach forward
   from the input's :focus/:not(:placeholder-shown) state to style the
   label that visually sits on top of it. */
.form-render-root .field[data-label-mode="floating"] .field-control {
  position: relative;
}
.form-render-root .field[data-label-mode="floating"] .field-control input,
.form-render-root .field[data-label-mode="floating"] .field-control textarea {
  padding-top: calc(var(--form-field-pad-v, 12px) + 12px);
  padding-bottom: calc(var(--form-field-pad-v, 12px) - 6px);
}
.form-render-root .field[data-label-mode="floating"] .field-control .field-label {
  position: absolute;
  left: var(--form-field-pad-h, 14px);
  top: 50%;
  transform: translateY(-50%);
  margin: 0;
  padding: 0 4px;
  font-size: 14px;
  text-transform: none;
  letter-spacing: normal;
  color: var(--paper-dim);
  background: transparent;
  pointer-events: none;
  transition: top 0.12s ease, font-size 0.12s ease, transform 0.12s ease, color 0.12s ease;
}
.form-render-root .field[data-label-mode="floating"] .field-control input:focus ~ .field-label,
.form-render-root .field[data-label-mode="floating"] .field-control input:not(:placeholder-shown) ~ .field-label,
.form-render-root .field[data-label-mode="floating"] .field-control textarea:focus ~ .field-label,
.form-render-root .field[data-label-mode="floating"] .field-control textarea:not(:placeholder-shown) ~ .field-label {
  top: 12px;
  transform: translateY(0);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  background: var(--panel-raised);
  color: var(--paper);
}
.form-render-root .field[data-label-mode="floating"] .field-control input:focus ~ .field-label {
  color: var(--yellow);
}

/* ---- Progress indicator ---- */
.form-progress { margin-top: 10px; }
.progress-bar-track {
  width: 100%; height: 8px; border-radius: 999px;
  background: var(--panel-raised); border: 1px solid var(--steel);
  overflow: hidden;
}
.progress-bar-fill {
  height: 100%;
  background: var(--form-primary, var(--yellow));
  transition: width 0.2s ease;
}
@media (prefers-reduced-motion: reduce) { .progress-bar-fill { transition: none; } }

.form-stepper {
  display: flex; align-items: flex-start; gap: 0;
  overflow-x: auto; -webkit-overflow-scrolling: touch;
}
.stepper-step { display: flex; flex-direction: column; align-items: center; gap: 4px; flex-shrink: 0; min-width: 32px; }
.stepper-dot {
  display: flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 50%;
  font-size: 12px; font-weight: 700;
  background: var(--panel-raised); border: 2px solid var(--steel); color: var(--paper-dim);
}
.stepper-step[data-state="current"] .stepper-dot {
  border-color: var(--form-primary, var(--yellow)); color: var(--form-primary, var(--yellow));
}
.stepper-step[data-state="done"] .stepper-dot {
  background: var(--form-success, var(--green)); border-color: var(--form-success, var(--green)); color: var(--form-success-fg, #08130d);
}
.stepper-name { font-size: 10px; color: var(--paper-dim); max-width: 72px; text-align: center; white-space: normal; }
.stepper-step[data-state="current"] .stepper-name { color: var(--paper); font-weight: 600; }
.stepper-connector { flex: 1 1 16px; height: 2px; background: var(--steel); margin-top: 12px; min-width: 12px; }
/* Compact mobile fallback — numbered dots only, no names, per "On mobile,
   automatically switch to a compact representation if page names would
   overflow." */
@media (max-width: 480px) {
  .stepper-name { display: none; }
}
@container (max-width: 480px) {
  .stepper-name { display: none; }
}

/* ---- Mobile optimization ---- */
/* Sticky nav — data-mobile-touch/stickyNavigation both come from the same
   form-wide Mobile Optimization group, applied together since a sticky
   footer without larger touch targets underneath it would be an easy
   mis-tap on a phone. Safe-area padding matches .sheet's own existing
   env(safe-area-inset-bottom) handling. Written once as a mixin-like
   comment below, then repeated verbatim inside both @media (real narrow
   viewports — actual Capture on a phone) and @container (the Designer's
   Preview device frame, which is never actually viewport-narrow — see
   .form-render-root's own container-type comment above) since neither
   at-rule can nest inside the other in plain CSS. */
@media (max-width: 640px) {
  .sheet-footer-sticky {
    position: sticky; bottom: 0;
    background: var(--panel);
    padding-bottom: calc(var(--form-nav-pad, 12px) + env(safe-area-inset-bottom));
    box-shadow: 0 -4px 12px rgba(0,0,0,0.3);
  }
  .form-render-root[data-mobile-touch="true"] input,
  .form-render-root[data-mobile-touch="true"] select,
  .form-render-root[data-mobile-touch="true"] textarea,
  .form-render-root[data-mobile-touch="true"] .btn,
  .form-render-root[data-mobile-touch="true"] .choice-row,
  .form-render-root[data-mobile-touch="true"] .yesno-toggle button {
    min-height: max(var(--form-field-height, var(--tap-min)), 48px);
  }
  .form-render-root[data-mobile-touch="true"] input[type="checkbox"],
  .form-render-root[data-mobile-touch="true"] input[type="radio"] {
    width: 22px; height: 22px; min-width: 22px; min-height: 22px;
  }
  /* Two-column forms always collapse to one column below this breakpoint
     already (see .fields-two-col's own 480px rule) — Auto Single Column
     (on by default) is that existing behavior; this project treats
     "responsive" as the recommended default rather than a separate
     opt-in, per the product brief (a form designed on desktop is
     routinely filled out on a phone). Turning it off is intentionally
     NOT wired to bypass the collapse — an unreadable, overflowing
     two-column phone layout isn't a real product option worth building. */
}
@container (max-width: 640px) {
  .sheet-footer-sticky {
    position: sticky; bottom: 0;
    background: var(--panel);
    padding-bottom: calc(var(--form-nav-pad, 12px) + env(safe-area-inset-bottom));
    box-shadow: 0 -4px 12px rgba(0,0,0,0.3);
  }
  .form-render-root[data-mobile-touch="true"] input,
  .form-render-root[data-mobile-touch="true"] select,
  .form-render-root[data-mobile-touch="true"] textarea,
  .form-render-root[data-mobile-touch="true"] .btn,
  .form-render-root[data-mobile-touch="true"] .choice-row,
  .form-render-root[data-mobile-touch="true"] .yesno-toggle button {
    min-height: max(var(--form-field-height, var(--tap-min)), 48px);
  }
  .form-render-root[data-mobile-touch="true"] input[type="checkbox"],
  .form-render-root[data-mobile-touch="true"] input[type="radio"] {
    width: 22px; height: 22px; min-width: 22px; min-height: 22px;
  }
}

/* ---- Media layout: photos ---- */
.photo-preview { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.photo-thumb { width: 110px; }
.photo-thumb-img-box { position: relative; width: 110px; height: 110px; }
.photo-thumb-img { width: 110px; height: 110px; object-fit: cover; border-radius: 8px; border: 1px solid var(--steel); display: block; }
.photo-thumb-remove {
  position: absolute; top: -6px; right: -6px; width: 22px; height: 22px; border-radius: 50%;
  background: var(--form-danger, var(--red)); color: #fff; border: 2px solid var(--panel);
  font-size: 12px; line-height: 1; padding: 0; cursor: pointer;
}
.photo-thumb-desc { margin-top: 6px; min-height: 36px; padding: 6px 8px; font-size: 13px; width: 100%; }

.photo-preview[data-layout="list"] { flex-direction: column; }
.photo-preview[data-layout="list"] .photo-thumb { width: 100%; display: flex; align-items: center; gap: 14px; }
.photo-preview[data-layout="list"] .photo-thumb-img-box { width: 84px; height: 84px; flex-shrink: 0; }
.photo-preview[data-layout="list"] .photo-thumb-img { width: 84px; height: 84px; }
.photo-preview[data-layout="list"] .photo-thumb-desc { margin-top: 0; }

.photo-carousel-nav { display: flex; align-items: center; justify-content: space-between; margin-top: 8px; gap: 10px; }
.photo-preview[data-layout="carousel"] { flex-wrap: nowrap; overflow: hidden; }
.photo-preview[data-layout="carousel"] .photo-thumb { display: none; width: 100%; }
.photo-preview[data-layout="carousel"] .photo-thumb.photo-carousel-active { display: block; }
.photo-preview[data-layout="carousel"] .photo-thumb-img-box { width: 100%; height: 220px; }
.photo-preview[data-layout="carousel"] .photo-thumb-img { width: 100%; height: 220px; }

/* Larger Photos (Mobile Optimization) — bigger thumbnails specifically on
   touch/mobile widths, without enlarging desktop UI. */
@media (max-width: 640px) {
  .form-render-root[data-mobile-photos="true"] .photo-thumb,
  .form-render-root[data-mobile-photos="true"] .photo-thumb-img-box,
  .form-render-root[data-mobile-photos="true"] .photo-thumb-img {
    width: 140px; height: 140px;
  }
  .form-render-root[data-mobile-photos="true"] .photo-preview[data-layout="carousel"] .photo-thumb-img-box,
  .form-render-root[data-mobile-photos="true"] .photo-preview[data-layout="carousel"] .photo-thumb-img {
    width: 100%; height: 260px;
  }
}
@container (max-width: 640px) {
  .form-render-root[data-mobile-photos="true"] .photo-thumb,
  .form-render-root[data-mobile-photos="true"] .photo-thumb-img-box,
  .form-render-root[data-mobile-photos="true"] .photo-thumb-img {
    width: 140px; height: 140px;
  }
  .form-render-root[data-mobile-photos="true"] .photo-preview[data-layout="carousel"] .photo-thumb-img-box,
  .form-render-root[data-mobile-photos="true"] .photo-preview[data-layout="carousel"] .photo-thumb-img {
    width: 100%; height: 260px;
  }
}

/* ---- Media layout: signature ---- */
.form-render-root [data-signature-layout="full_width"] .signature-pad-wrap { width: 100%; }
.form-render-root [data-signature-layout="full_width"] .signature-canvas { width: 100%; }

/* ---------- Map ------------------------------------------------------------ */
/* z-index:0 (not "auto") is what actually matters here — position:absolute
   alone doesn't create a stacking context; it needs an explicit z-index
   too. Without one, Leaflet's own internal panes/controls (which it
   assigns z-index values from 200 up to 1000) aren't contained within
   #map at all — they leak out and compete directly with sibling elements
   like .editor-toolbar, which is how a z-index:15 button could end up
   rendering behind the map despite looking like it should obviously be
   on top. */
#map { position: absolute; inset: 0; z-index: 0; }
.map-view-wrap { position: relative; height: 100%; }

/* Saved Map banner (map.js's renderSavedMapBanner) — a thin bar pinned
   to the top of the map, only shown while navigate('map', {savedMapId})
   has this view showing a Saved Map's curated layer set instead of its
   normal "every layer I have access to" one. z-index 10: above #map's
   own 0 (see that rule's own comment on why an explicit z-index is
   required at all here — Leaflet's internal panes leak up to 1000
   otherwise) but below every floating tool toggle (15+), so opening a
   tool panel still layers correctly over this bar. */
.saved-map-banner {
  position: absolute; top: 0; left: 0; right: 0; z-index: 10;
  display: flex; align-items: center; gap: 10px; padding: 8px 14px;
  background: var(--panel); border-bottom: 1px solid var(--steel);
  box-shadow: 0 2px 8px rgba(0,0,0,0.25);
  font-family: var(--font-body); font-size: 13px; color: var(--paper);
}
.saved-map-banner-name { font-weight: 700; font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.02em; }

/* GIS Data Import wizard's Coordinate System step — small solid dot for
   each sample Point/MultiPoint feature drawn over the extent preview
   (importWizard.js's renderCrsStep). A plain L.divIcon with no content
   of its own is otherwise invisible (an empty div renders nothing) —
   this is what actually draws the dot inside it. Matches
   featureStyle's own 'var(--yellow)' (this app's actual brand blue,
   despite the name — see this file's own :root comment) used for the
   Line/Polygon sample shapes in that same preview, so every sample
   feature (whatever its geometry type) reads as "part of the same
   preview" at a glance. */
.import-preview-marker {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--yellow);
  border: 1.5px solid #fff;
  box-shadow: 0 0 0 1px var(--yellow);
}
.saved-map-banner-warning { color: var(--yellow); font-size: 12px; }
.saved-map-banner-spacer { flex: 1; }

/* Map editor toolbar (editorToolbar.js) — collapsed to a small floating
   tab by default (bottom-right, clear of Leaflet's own zoom control at
   top-left), expanding to a full panel that grows upward from this
   bottom anchor as its content gets taller. */
.editor-toolbar { position: absolute; right: 14px; bottom: 18px; z-index: 15; font-family: var(--font-body); }
.editor-toolbar-tab {
  display: none; width: 44px; height: 44px; border-radius: 50%;
  background: var(--yellow); color: var(--asphalt); border: none;
  font-size: 26px; box-shadow: 0 3px 10px rgba(0,0,0,0.4); cursor: pointer;
}
.editor-toolbar.collapsed .editor-toolbar-tab { display: block; }
.editor-toolbar.collapsed .editor-toolbar-panel { display: none; }
.editor-toolbar-panel {
  width: 260px; max-height: 80vh; max-height: 80dvh; overflow-y: auto;
  -webkit-overflow-scrolling: touch; touch-action: pan-y;
  background: var(--panel); border: 1px solid var(--steel);
  border-radius: var(--radius); padding: 14px; box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.editor-toolbar-header {
  display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px;
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 13px; color: var(--paper);
}
.editor-toolbar-panel select,
.editor-toolbar-panel input[type="text"],
.editor-toolbar-panel input[type="number"],
.editor-toolbar-panel input[type="date"] {
  width: 100%; background: var(--panel-raised); border: 1px solid var(--steel); border-radius: 6px;
  padding: 8px 10px; color: var(--paper); font-size: 13px; font-family: var(--font-body);
}

/* Select-features tool (featureSelect.js) — a small floating toggle,
   stacked just above the editor toolbar's own tab so the two never
   overlap, plus the sliding bottom attribute-table panel it opens once
   at least one feature is selected. Available to any authenticated
   user (unlike the editor toolbar), since feature reads are open to
   everyone — see architecture.md §6. */
.feature-select-toggle {
  position: absolute; right: 14px; bottom: 70px; z-index: 15;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--panel-raised); color: var(--paper); border: 1px solid var(--steel);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px rgba(0,0,0,0.4); cursor: pointer;
}
.feature-select-toggle.active { background: var(--yellow); color: var(--asphalt); border-color: var(--yellow); }

/* Suppresses iOS's long-press text-selection magnifier/"Copy/Look Up"
   callout bubble from popping up over the map while the Select tool's
   own long-press-to-rectangle-select gesture (featureSelect.js) is
   armed — same -webkit-touch-callout/-webkit-user-select fix this
   codebase already uses for Form Designer's drag handles (see
   designer.css's .drag-handle), just toggled on the map container only
   while Select mode is actually on (setToggleActive), rather than left
   on permanently — a long-press means something different on this map
   when Select mode is off (e.g. Editor Toolbar's own long-press-to-
   finish-a-shape gesture), so this shouldn't suppress the callout there
   too without reason to. */
.leaflet-container.feature-select-touch-active {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}

/* Custom cursor for Select mode — a plain arrow (white outline so it
   stays visible over both the light and dark basemaps) with a small
   dashed box near its tip, so it reads at a glance as "click to
   select," distinct from the map's normal grab/pointer cursor. Colors
   are keyword names (black/white), not hex, specifically so nothing in
   this data URI needs percent-encoding for a literal '#'. The hotspot
   (the two numbers after the url()) is pinned to the arrow's own tip at
   (3,2) in the SVG's own coordinate space, matching where a real arrow
   cursor's hotspot sits, so clicks register exactly where the tip
   visually points rather than off to one side of it. */
.leaflet-container.feature-select-active {
  cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M3 2 L3 18 L7 14 L10 20 L12.5 19 L9.5 13 L15 13 Z' fill='black' stroke='white' stroke-width='1'/%3E%3Crect x='13' y='13' width='8' height='8' rx='1' fill='none' stroke='black' stroke-width='1.4' stroke-dasharray='2 2'/%3E%3C/svg%3E") 3 2, crosshair;
}

/* CARTO's dark_all tiles (basemaps.js's dark-mode Streets basemap) read
   as very dark/near-black as shipped — brightened here via filter
   rather than switching tile providers (see basemaps.js's own comment
   on the .streets-tile className this targets, and why setUrl()'s
   light/dark swap can't just conditionally add/remove this class
   itself). Only affects Streets specifically, not Aerial imagery,
   which has no equivalent "too dark" complaint and shouldn't be
   touched by a dark-mode filter that's really about the Streets style. */
:root[data-theme="dark"] .streets-tile {
  filter: brightness(1.35) contrast(0.92) saturate(0.9);
}

.attr-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 17;
  background: var(--panel); border-top: 1px solid var(--steel);
  border-radius: 14px 14px 0 0; box-shadow: 0 -4px 16px rgba(0,0,0,0.5);
  display: flex; flex-direction: column; overflow: hidden;
  font-family: var(--font-body);
  transition: height 0.18s ease;
}
.attr-panel.hidden { display: none; }
.attr-panel.state-mid { height: 50%; }
.attr-panel.state-full { height: 100%; border-radius: 0; }
/* "Full collapse" — only the drag handle stays visible/clickable, tall
   enough to stay an easy tap target; header and body are hidden rather
   than just shrunk, since there's no useful room to show either at
   this height. */
.attr-panel.state-collapsed { height: 26px; }
.attr-panel.state-collapsed .attr-panel-header,
.attr-panel.state-collapsed .attr-panel-body { display: none; }
/* While the handle is actively being dragged, height is set directly
   (inline style, in px, on every pointermove) rather than jumping
   between the three preset states below — the eased transition meant
   for those discrete jumps would otherwise fight the drag and make it
   feel laggy/rubber-banded instead of following the pointer 1:1. */
.attr-panel.dragging { transition: none; }

.attr-panel-handle {
  width: 100%; padding: 8px 0 6px; background: none; border: none; cursor: pointer; flex-shrink: 0;
  touch-action: none; /* otherwise the browser's own touch-scroll handling intercepts the drag before pointermove ever sees it */
}

/* ---------- Walk to Map (walkCapture.js) -----------------------------
   A full-viewport takeover. z-index:30 is deliberately BELOW
   .task-modal-backdrop/.task-modal (40/41) and the generic .modal-
   overlay (100): a feature-version conflict modal (openFeatureConflict
   Modal) can legitimately open while a Walk Replacement save is in
   flight, and it must stack on top of this overlay, not behind it.

   The map fills the ENTIRE overlay (not a flex sibling that shrinks to
   share space with the controls) — .walk-capture-panel and
   attachGeometryReshapeTool's own floating panel both float OVER it as
   absolutely-positioned overlays instead, same pattern the rest of the
   app already uses for #map + .attr-panel/.editor-toolbar. This is
   also what makes the map's own on-screen size CONSTANT across every
   walk-capture screen (walking/review/editing) regardless of how tall
   that screen's own panel content is — Leaflet isn't told about
   container resizes automatically (no ResizeObserver wired up, just
   one invalidateSize() at creation), so a map-wrap that changed size
   per screen would drift out of sync with what Leaflet thinks its own
   pixel bounds are. */
.walk-capture-overlay {
  position: fixed; inset: 0; z-index: 30;
  background: var(--asphalt);
}
.walk-capture-map-wrap { position: absolute; inset: 0; }
/* Explicit z-index:0 (not just position:absolute) for the same reason
   #map's own rule has one, earlier in this file (see the "z-index:0
   (not 'auto') is what actually matters here" comment above) — Leaflet's
   internal panes assign themselves z-index values from 200 up past
   1000, and without a stacking context to contain them at the #wc-map
   level, panning/redrawing the map can repaint those panes ABOVE
   .walk-capture-panel/.editor-toolbar despite both having their own
   much higher-looking z-index numbers, since those numbers only
   compare directly against unconstrained Leaflet internals otherwise. */
.walk-capture-map-wrap #wc-map { position: absolute; inset: 0; z-index: 0; }
/* Wins over .editor-toolbar's own base z-index:15 (see editorToolbar.js's
   "Edit Geometry"/Walk Replacement panel) so it always floats above
   .walk-capture-panel (16) when both are present — in practice
   walkCapture.js empties/hides its own panel while the reshape tool's
   floating panel is active, but this keeps the stacking order correct
   even for a brief transition frame either way. */
.walk-capture-map-wrap .editor-toolbar { z-index: 20; }
.walk-capture-panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 16;
  max-height: 52vh; max-height: 52dvh;
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--panel);
  border-top: 1px solid var(--steel);
  border-radius: 16px 16px 0 0;
  box-shadow: 0 -4px 16px rgba(0,0,0,0.5);
  padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
  font-family: var(--font-body);
}
.walk-capture-panel.hidden { display: none; }
.walk-capture-header {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 10px;
  margin-bottom: 8px;
}
.walk-capture-title {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 14px; color: var(--yellow);
}
/* Compact three-stat row (elapsed/distance/points) — small enough that
   the whole walking screen (stats + Mark Corner/Pause/Finish/Cancel)
   fits comfortably within .walk-capture-panel's max-height without
   crowding out the live map above it. */
.walk-capture-stats {
  display: flex; gap: 6px; margin-bottom: 8px;
}
.walk-capture-stat {
  flex: 1; display: flex; flex-direction: column; align-items: center;
  background: var(--panel-raised); border: 1px solid var(--steel); border-radius: 8px;
  padding: 6px 4px;
}
.walk-capture-stat-value {
  font-family: var(--font-display); font-size: 15px; font-weight: 700; color: var(--paper);
}
.walk-capture-stat-label {
  font-size: 9px; text-transform: uppercase; letter-spacing: 0.04em; color: var(--paper-dim); margin-top: 1px;
}
/* Pause is a compact square icon button (not a full-width text button)
   specifically so all four walking controls — Mark Corner, Pause,
   Finish, Cancel — fit on one row (per request). Mark Corner uses a
   text label instead of a symbol since there's no well-established
   glyph for it, sized down so it stays single-line and matches the
   height of the icon/text buttons beside it. */
.walk-capture-controls { gap: 6px; align-items: stretch; }
.walk-capture-controls .btn { flex: 1; padding: 10px 6px; font-size: 13px; }
.walk-capture-icon-btn {
  flex: 0 0 auto; width: 42px; padding: 0; font-size: 18px;
  display: flex; align-items: center; justify-content: center;
}
/* Mark Corner reads as a text label (not the icon set above) but must stay
   the same height as Pause/Finish/Cancel; a smaller, non-wrapping font
   keeps it single-line so align-items:stretch doesn't grow the row. */
.walk-capture-mark-corner-btn {
  font-size: 11px; white-space: nowrap; padding: 10px 4px;
}

/* Capture-method radios (editorToolbar.js's "Draw on Map" / "Walk to
   Map", and this file's own cleanup-preset picker) — plain native
   radios, not a styled segmented control, since there's no drag/press
   interaction here worth the extra chrome .theme-toggle-style controls
   get elsewhere in this app. */
.capture-method-option {
  display: flex; align-items: center; gap: 8px;
  font-size: 13px; color: var(--paper); padding: 4px 0; cursor: pointer;
}
.capture-method-option input { margin: 0; }
.attr-panel-handle::after {
  content: ''; display: block; width: 42px; height: 5px; border-radius: 3px; background: var(--steel); margin: 0 auto;
}
.attr-panel-header {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 0 14px 10px; border-bottom: 1px solid var(--steel); flex-shrink: 0;
}
.attr-panel-title { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.attr-panel-title strong {
  font-family: var(--font-display); text-transform: uppercase; letter-spacing: 0.03em; font-size: 14px;
  color: var(--paper); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.attr-panel-title-area { flex: 1; min-width: 0; }
/* Shown instead of .attr-panel-title once features from more than one
   feature layer are selected at once — a tab per layer (name + record
   count); clicking a tab switches which layer's attribute table the
   panel body shows. Horizontally scrollable rather than wrapping, so a
   handful of selected layers still fits the header's one row. */
.attr-panel-tabs { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 2px; }
.attr-panel-tab {
  flex-shrink: 0; background: var(--panel-raised); border: 1px solid var(--steel); border-radius: 6px;
  padding: 5px 10px; color: var(--paper-dim); font-family: var(--font-body); font-size: 12.5px;
  cursor: pointer; white-space: nowrap;
}
.attr-panel-tab.active { background: var(--yellow); border-color: var(--yellow); color: var(--asphalt); font-weight: 600; }
.attr-panel-tab-count { opacity: 0.75; }
.attr-panel-tab.active .attr-panel-tab-count { opacity: 0.7; }
.attr-panel-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
/* Zoom to / Edit are icon-only in the actions row — same .btn treatment
   (border, background, disabled state) as everywhere else, just square
   instead of text-width, with the icon sized to sit comfortably inside.
   Deliberately smaller than the app's usual --tap-min (48px): this row
   sits in a tight header next to the tab strip and close button, and at
   full tap-min size the two buttons visually dominated the header. The
   icon itself stays 20px either way — only the surrounding button
   shrinks — so the tap target loses padding, not visual weight. */
.attr-panel-actions .btn-icon-only { width: 34px; min-height: 34px; height: 34px; padding: 0; gap: 0; }
.attr-panel-actions .btn-icon-only svg { width: 20px; height: 20px; }
.attr-panel-actions .btn-icon-only.active { background: var(--yellow); color: var(--asphalt); border-color: var(--yellow); }
.attr-panel-close-btn { font-size: 18px; padding: 4px 6px; }
/* .task-table-draggable-th's own .dragging (tasks.css) works here
   unmodified — cursor/touch-action/opacity are color-agnostic. The drop
   LINE itself (tableColumns.js's showDropIndicator) resolves its color
   from whichever CSS variable name the caller passes as accentVar —
   featureSelect.js passes '--yellow' here instead of the default
   '--task-accent', since .attr-table isn't inside .task-theme and that
   token wouldn't resolve. */
.attr-panel-body { flex: 1; overflow: auto; }
/* Same "don't recompute :hover onto whatever a re-render happened to
   leave under a stationary pointer" fix as .task-table's own
   .suppress-hover (tasks.css) — see tableColumns.js's
   suppressHoverUntilMouseMove(), used here after every sort/reorder/
   filter redraw of this table too. */
.attr-table.suppress-hover tbody tr:hover { background: transparent; }

/* Search-features tool (featureSearch.js) — floating toggle stacked
   directly above the select tool's own toggle (same right-side stack as
   the editor toolbar tab below it), rather than off in its own corner,
   so the map's feature tools read as one deliberate group: search on
   top, select in the middle, draw/edit at the bottom. */
.feature-search-toggle {
  position: absolute; right: 14px; bottom: 122px; z-index: 15;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--panel-raised); color: var(--paper); border: 1px solid var(--steel);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px rgba(0,0,0,0.4); cursor: pointer;
}
.feature-search-toggle.active { background: var(--yellow); color: var(--asphalt); border-color: var(--yellow); }

.search-panel {
  position: absolute; right: 14px; bottom: 174px; z-index: 16; width: 260px; max-height: 60vh;
  background: var(--panel); border: 1px solid var(--steel); border-radius: var(--radius);
  box-shadow: 0 4px 16px rgba(0,0,0,0.5); display: flex; flex-direction: column; overflow: hidden;
  font-family: var(--font-body);
}
.search-panel.hidden { display: none; }
.search-panel-header {
  display: flex; align-items: center; justify-content: space-between; padding: 10px 8px 10px 14px;
  border-bottom: 1px solid var(--steel);
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 13px; color: var(--paper);
}
.search-panel-close-btn { font-size: 16px; padding: 4px 8px; }
.search-panel-body { padding: 10px; display: flex; flex-direction: column; gap: 8px; overflow: hidden; min-height: 0; }
.search-panel-body select,
.search-panel-body input[type="text"] {
  width: 100%; background: var(--panel-raised); border: 1px solid var(--steel); border-radius: 6px;
  padding: 8px 10px; color: var(--paper); font-size: 13px; font-family: var(--font-body);
}
.search-panel-results { overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
.search-result-item {
  display: flex; flex-direction: column; align-items: flex-start; gap: 1px;
  width: 100%; text-align: left; background: none; border: none; border-radius: 6px;
  padding: 7px 8px; cursor: pointer; color: var(--paper);
}
.search-result-item:hover { background: var(--panel-raised); }
.search-result-name { font-size: 13px; font-weight: 600; }
.search-result-layer { font-size: 11px; color: var(--paper-dim); }
.search-result-truncated { padding: 4px 2px 0; }

/* ArcGIS Online reference-layers tool (referenceLayers.js) — floating
   "+" toggle stacked directly above the search toggle, same right-side
   stack (search on top... now this above it, select in the middle,
   draw/edit at the bottom), open to any authenticated user (adding a
   PRIVATE layer needs no special role; adding a PUBLIC one is gated
   server-side the same as feature_layers writes — see
   reference_layers/create.php). */
.arcgis-tool-toggle {
  position: absolute; right: 14px; bottom: 174px; z-index: 15;
  width: 44px; height: 44px; border-radius: 50%;
  background: var(--panel-raised); color: var(--paper); border: 1px solid var(--steel);
  display: flex; align-items: center; justify-content: center;
  box-shadow: 0 3px 10px rgba(0,0,0,0.4); cursor: pointer;
}
.arcgis-tool-toggle.active { background: var(--yellow); color: var(--asphalt); border-color: var(--yellow); }

.arcgis-panel {
  position: absolute; right: 14px; bottom: 226px; z-index: 16; width: 340px; max-height: 70vh;
  background: var(--panel); border: 1px solid var(--steel); border-radius: 16px;
  box-shadow: 0 8px 28px rgba(15,23,42,0.18); display: flex; flex-direction: column; overflow: hidden;
  font-family: var(--font-body);
}
.arcgis-panel.hidden { display: none; }
.arcgis-panel-header {
  display: flex; align-items: center; justify-content: space-between; padding: 18px 20px;
  border-bottom: 1px solid var(--steel);
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.03em; font-size: 15px; color: var(--paper);
}
/* Reads as chrome (dismiss control), not a text action — deliberately
   NOT .link-btn's blue/bold-text treatment, which is reserved for
   actions like "Sign in"/"Back". Matches the header's own --paper
   (near-black) text color instead of the accent blue. */
.arcgis-panel-close-btn {
  display: flex; align-items: center; justify-content: center;
  background: none; border: none; padding: 6px; border-radius: 8px; color: var(--paper); cursor: pointer;
}
.arcgis-panel-close-btn:hover { background: var(--panel-raised); }
.arcgis-panel-body { padding: 20px; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; min-height: 0; }
.arcgis-panel-section { display: flex; flex-direction: column; gap: 10px; }
/* #arcgis-manual-add and #arcgis-browse-view are each a single plain
   wrapper div (not their own flex container) around several stacked
   form/list elements — without their own gap, those inner elements
   pack flush against each other regardless of .arcgis-panel-section's
   own gap above, which only spaces its own direct children (the
   section title, the Browse button, and this wrapper div itself) apart
   from EACH OTHER, not anything inside it. */
#arcgis-manual-add, #arcgis-browse-view { display: flex; flex-direction: column; gap: 12px; }
/* #arcgis-add-layer-mount wraps renderArcGisAddLayerPanel's own two
   sections (Account, Add a layer) — needs the same flex/gap treatment
   .arcgis-panel-body gives ITS direct children, since this mount div
   is now the thing sitting between them instead of those two sections
   being direct children of .arcgis-panel-body themselves. */
#arcgis-add-layer-mount { display: flex; flex-direction: column; gap: 20px; }
/* Visual break before "Layers on map" specifically — Account and Add a
   layer read as one continuous "getting set up" flow with just
   whitespace between them, but the saved-layers list below is a
   distinct, separately-scrolling concern worth a firmer line. */
.arcgis-panel-section-divided { border-top: 1px solid var(--steel); padding-top: 20px; }
.arcgis-panel-section-title {
  font-family: var(--font-display); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.04em; font-size: 12px; color: var(--paper-dim);
}
.arcgis-panel-body input[type="text"],
.arcgis-panel-body input[type="url"] {
  width: 100%; background: var(--panel); border: 1px solid var(--steel); border-radius: 12px;
  padding: 13px 14px; color: var(--paper); font-size: 14.5px; font-family: var(--font-body);
}
.arcgis-panel-body input[type="text"]::placeholder,
.arcgis-panel-body input[type="url"]::placeholder { color: var(--paper-dim); }

.arcgis-account-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--panel-raised); border: 1px solid var(--steel); border-radius: 12px; padding: 14px 16px;
}
.arcgis-account-status { font-size: 14px; color: var(--paper-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.arcgis-account-status strong { color: var(--paper); font-weight: 600; }

/* Secondary action (Sign in/Sign out) — an outlined pill rather than a
   filled button, so it doesn't visually compete with "Add to map"
   below, the panel's one solid/filled action. */
.arcgis-btn-outline {
  display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; white-space: nowrap;
  background: var(--panel); border: 1px solid var(--yellow); color: var(--yellow);
  font-weight: 600; font-size: 14px; border-radius: 10px; padding: 10px 16px; cursor: pointer;
}
.arcgis-btn-outline:active { transform: scale(0.97); }

/* "Browse my ArcGIS layers…" — a full-width outlined card rather than a
   plain button, with its icon+label left-aligned like a menu entry,
   since it opens a whole picker rather than performing an action
   in place. */
.arcgis-browse-btn {
  display: flex; align-items: center; gap: 10px; width: 100%; text-align: left;
  background: var(--panel); border: 1px solid var(--steel); border-radius: 12px;
  padding: 15px 16px; color: var(--yellow); font-weight: 600; font-size: 14px; cursor: pointer;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.arcgis-browse-btn:active { transform: scale(0.99); }
.arcgis-browse-btn svg { flex-shrink: 0; }

/* The panel's one filled/solid action. Deliberately its own class
   rather than the generic .btn-primary — that one is styled for the
   app's uppercase/condensed-font buttons elsewhere, which would clash
   with this panel's plainer, denser look. */
.arcgis-btn-primary {
  display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%;
  background: var(--yellow); border: none; color: #fff; font-weight: 700; font-size: 14px;
  border-radius: 10px; padding: 11px 16px; cursor: pointer; font-family: var(--font-body);
}
.arcgis-btn-primary:active { transform: scale(0.98); }
.arcgis-btn-primary:disabled { opacity: 0.6; pointer-events: none; }

.arcgis-private-check { display: flex; align-items: center; gap: 10px; font-size: 14.5px; color: var(--paper); }
.arcgis-private-check input[type="checkbox"] { width: 22px; height: 22px; accent-color: var(--yellow); flex-shrink: 0; cursor: pointer; }
.arcgis-panel-error { font-size: 13px; color: var(--red); }
.arcgis-panel-hint { font-size: 12.5px; color: var(--paper-dim); line-height: 1.5; }
.arcgis-layers-list { display: flex; flex-direction: column; gap: 6px; }
.arcgis-layer-row {
  display: flex; align-items: center; gap: 10px; padding: 10px 8px; border-radius: 10px;
}
.arcgis-layer-row:hover { background: var(--panel-raised); }
.arcgis-layer-visibility { width: 16px; height: 16px; accent-color: var(--yellow); flex-shrink: 0; cursor: pointer; }

/* Full-screen fallback whenever the floating card genuinely doesn't
   fit, on ANY device — not just phones. Two independent triggers:
   - narrow width (phones): the fixed-width card runs off the left/right
     edges, or (short/landscape) pushes its own top above y=0.
   - short height (a non-maximized or small desktop browser window,
     a laptop display, etc.): the panel's content has grown enough
     across recent redesigns (more sections, more padding, a populated
     "Layers on map" list) that `bottom: 226px` anchoring can push the
     top of the panel above the visible viewport even at a perfectly
     normal desktop WIDTH — width alone was never the actual constraint
     there, available vertical room was.
   Either way the fix is the same: cover the full map view (fixed, full
   inset) instead of floating as a small anchored card — same idea as
   .attr-panel's own state-full (full-height bottom sheet), just
   applied unconditionally here rather than as one of several drag
   states, since this panel has no drag handle of its own. */
@media (max-width: 640px), (max-height: 800px) {
  .arcgis-panel {
    position: fixed; inset: 0; width: auto; max-height: none;
    border-radius: 0; z-index: 500;
  }
}
.arcgis-layer-swatch { width: 12px; height: 12px; border-radius: 3px; flex-shrink: 0; }
.arcgis-layer-title { flex: 1; min-width: 0; font-size: 13.5px; color: var(--paper); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.arcgis-layer-badge {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.03em; color: var(--paper-dim);
  border: 1px solid var(--steel); border-radius: 5px; padding: 2px 6px; flex-shrink: 0;
}
.arcgis-layer-remove-btn { font-size: 14px; padding: 2px 6px; flex-shrink: 0; }
.arcgis-layer-style-btn {
  background: none; border: none; padding: 4px; flex-shrink: 0; cursor: pointer;
  color: var(--paper-dim); border-radius: 6px; display: flex; align-items: center;
}
.arcgis-layer-style-btn:hover { color: var(--paper); background: var(--panel); }

/* Web Map group heading (Layers on map) — "Add all layers" tags every
   layer it creates with the source web map's id/title (migrations/055);
   renderList() groups those rows under one of these instead of listing
   them as unrelated siblings. Sits directly above its group's own
   indented .arcgis-layer-row-indented rows, same sibling-div structure
   .arcgis-layer-style-panel already uses rather than true DOM nesting. */
.arcgis-webmap-group-header {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 8px 8px 4px; margin-top: 4px;
}
.arcgis-webmap-group-title {
  display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.03em; color: var(--paper-dim);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
}
.arcgis-webmap-group-title svg { flex-shrink: 0; }
.arcgis-webmap-remove-all-btn { font-size: 12px; flex-shrink: 0; white-space: nowrap; }
/* Reads as "belonging to" the group heading above it — same indent
   convention the map legend already uses for symbolize-by-field rows
   nested under their own layer (.map-legend-row-indented). */
.arcgis-layer-row-indented { padding-left: 22px; }

/* Collapsed style editor for one saved layer (Layers on map > the
   sliders icon on that row) — reuses renderLineOrPolygonStylePicker's/
   this file's own renderPointStylePicker's plain .field/input styling
   (already global, nothing special needed here), this block is just
   the expand/collapse chrome around it. Sits directly under its own
   .arcgis-layer-row (see renderList()'s markup — one sibling div per
   row, not nested inside it) so toggling it doesn't reflow the row's
   own flex layout. */
.arcgis-layer-style-panel {
  padding: 10px 8px 12px; margin: -4px 0 4px; border-radius: 10px;
  background: var(--panel-raised); border: 1px solid var(--steel);
}
.arcgis-layer-style-panel.hidden { display: none; }

/* border-collapse: collapse + a sticky header row inside a scrollable
   container is a well-documented WebKit rendering bug — Safari fails
   to repaint the sticky cell correctly, so it visually doubles right
   after a row is added and then snaps/jumbles into place on scroll.
   border-collapse: separate (with border-spacing: 0, so adjacent cell
   borders still sit flush against each other) avoids the bug entirely
   without changing how the table looks. (Only the header row freezes
   now — see architecture.md: an earlier sticky first-COLUMN behavior
   was removed because its header cell, also sticky, shared the same
   z-index as the other sticky header cells and got visually painted
   over by them as the table scrolled horizontally, so the frozen
   column's own field-name label would disappear while the column's
   data cells stayed pinned — confusing enough that plain unfrozen
   horizontal scrolling for all columns is the more predictable
   behavior here.) */
.attr-table { width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; }
.attr-table th, .attr-table td {
  padding: 8px 12px; text-align: left; white-space: nowrap; border-bottom: 1px solid var(--steel);
}
.attr-table th {
  position: sticky; top: 0; background: var(--panel); color: var(--paper-dim); font-weight: 600; z-index: 1;
}
.attr-table tbody tr { cursor: pointer; color: var(--paper); }
.attr-table tbody tr:hover { background: var(--panel-raised); }
.attr-table tbody tr.active-row { background: rgba(47,111,237,0.18); }

/* Right-click "finish drawing" menu (editorToolbar.js) — positioned at
   the click point via inline left/top, set in JS. */
.map-context-menu {
  position: absolute; z-index: 20; background: var(--panel); border: 1px solid var(--steel);
  border-radius: 6px; box-shadow: 0 4px 14px rgba(0,0,0,0.5); overflow: hidden;
}
.map-context-menu-item {
  display: block; width: 100%; white-space: nowrap; background: none; border: none;
  color: var(--paper); font-family: var(--font-body); font-size: 13px;
  padding: 9px 14px; cursor: pointer; text-align: left;
}
.map-context-menu-item:hover { background: var(--panel-raised); color: var(--yellow); }

/* Generic anchored dropdown menu — currently just the Map Builder's
   topbar "..." menu (Duplicate/Delete), but deliberately generic
   (not .map-builder-*-scoped) so a future second use case can reuse it
   directly. Same visual language as .map-context-menu just above
   (border/shadow/hover), but positioned relative to a trigger BUTTON
   (see openMoreMenu()'s own JS) rather than a map click coordinate. */
.dropdown-menu {
  position: fixed; z-index: 500; background: var(--panel); border: 1px solid var(--steel);
  border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,0.5); overflow: hidden;
  min-width: 180px;
}
.dropdown-menu-item {
  display: block; width: 100%; white-space: nowrap; background: none; border: none;
  color: var(--paper); font-family: var(--font-body); font-size: 14px;
  padding: 11px 16px; cursor: pointer; text-align: left;
}
.dropdown-menu-item:hover { background: var(--panel-raised); color: var(--yellow); }
.dropdown-menu-item-danger { color: var(--red, #e5484d); }
.dropdown-menu-item-danger:hover { background: var(--panel-raised); color: var(--red, #e5484d); }

/* Geometry reshape mode (editorToolbar.js's attachGeometryReshapeTool)
   — draggable vertex handles plus a small panel, reusing the editor
   toolbar's own bottom-right position since the two are mutually
   exclusive and never shown at the same time. */
.vertex-handle-icon { cursor: pointer !important; }
.vertex-handle-icon:active { cursor: pointer !important; }
.vertex-handle-dot {
  width: 14px; height: 14px; border-radius: 50%; box-sizing: border-box;
  background: #1E2226; border: 2px solid #F4C22B; cursor: pointer;
}
.vertex-handle-dot-first { background: #F4C22B; }

/* Snap indicator (editorToolbar.js's snapIndicatorIcon, both the
   drawing toolbar and the reshape tool) — a hollow ring shown at
   whatever candidate point a placed/dragged vertex is currently
   snapping to. Non-interactive (see the marker's `interactive: false`)
   and visually distinct from the solid vertex-handle dots above so
   "here's what you'd snap to" never reads as "here's a real vertex". */
.snap-indicator-icon { pointer-events: none !important; }
.snap-indicator-dot {
  width: 18px; height: 18px; border-radius: 50%; box-sizing: border-box;
  border: 3px solid #00C3FF; background: rgba(0, 195, 255, 0.15);
}

/* Map legend (mapIcons.js's createLegendControl) — a plain Leaflet
   control, styled to match the app rather than Leaflet's default control
   box look. Empty when there's nothing to show (see JS), so it never
   renders as an empty box. */
/* Marker shapes (mapIcons.js) are plain fills with no outline — this
   drop-shadow is what gives them definition against map tiles instead,
   for a softer, more modern look. filter:drop-shadow (not box-shadow)
   follows the actual shape's alpha, not a rectangular box, so it hugs a
   circle/star/diamond correctly rather than looking like a square behind
   it. */
.map-marker-icon { filter: drop-shadow(0 2px 3px rgba(0,0,0,0.45)); }

/* Permanent map labels (feature display names, legend-driven marker
   labels) — these color/size/weight/text-shadow values are now just
   the fallback for a layer with no saved map_label_style (see
   migrations/054 and featureLayers.js's LABEL_STYLE_DEFAULTS/
   haloTextShadow, which set the same values as inline styles — inline
   always wins the cascade, so those apply whenever a layer HAS a
   saved style, and this class is what's left visible for the ones
   that don't). White text with a black outline so it stays readable
   against any basemap or tile imagery, light or dark. Layered
   text-shadow rather than -webkit-text-stroke, which has spotty support
   on older iOS Safari. Strips Leaflet's own tooltip chrome (white box,
   border, little pointer triangle) entirely — this is meant to look
   like a map label, not a popup/tooltip. */
.leaflet-tooltip.map-label {
  background: transparent;
  border: none;
  box-shadow: none;
  padding: 0;
  color: #ffffff;
  font-weight: 700;
  font-size: 12px;
  font-family: var(--font-body);
  /* Fallback only — every label rendered via featureLayers.js's own
     resolveLabelStyle()/haloTextShadow() gets this same 12-point ring
     technique applied as an INLINE style instead (which always wins
     the cascade over this class), so this class-level value only ever
     matters if a tooltip element's inline style somehow failed to
     apply. Kept as a matching 12-point ring (radius 2px, the same
     default width LABEL_STYLE_DEFAULTS uses) rather than the old
     4-diagonal-corner-only version, which left visible gaps at the
     direct top/bottom/left/right of straight glyph edges — see
     haloTextShadow's own comment in featureLayers.js for the full
     reasoning. */
  text-shadow:
    2px 0px 0 #000, 1.73px 1px 0 #000, 1px 1.73px 0 #000, 0px 2px 0 #000,
    -1px 1.73px 0 #000, -1.73px 1px 0 #000, -2px 0px 0 #000, -1.73px -1px 0 #000,
    -1px -1.73px 0 #000, 0px -2px 0 #000, 1px -1.73px 0 #000, 1.73px -1px 0 #000,
    0 0 2px #000;
  white-space: nowrap;
}
.leaflet-tooltip.map-label::before { display: none; }

/* Locate-me control (map.js's addLocateControl) — same leaflet-bar/
   leaflet-control chrome as the built-in zoom buttons; this just
   centers our inline SVG crosshair inside that 26x26 box the way
   Leaflet's own icon-font glyphs are centered by default. */
.locate-control a { display: flex; align-items: center; justify-content: center; }

/* Task layer toggle (taskMapLayer.js) — same leaflet-bar/leaflet-control
   chrome as locate-control above; .active uses the same yellow-accent
   convention as .feature-select-toggle.active and .feature-search-
   toggle.active so "this tool is currently on" reads consistently
   across every map tool, not just this one.

   Every state below is !important, and :focus/:active are covered
   alongside :hover — not because normal specificity ties couldn't be
   won with plain rules (they can, and the first attempt here tried
   exactly that), but because *something* in Leaflet's own stylesheet
   consistently won anyway once a click was actually involved, most
   likely `.leaflet-touch .leaflet-bar a:hover` (Leaflet adds a
   `leaflet-touch` class to the map container on touch devices, and its
   touch-specific rules carry one more class than a plain
   `.leaflet-bar a:hover` — enough to beat a same-weight custom rule
   regardless of source order). Rather than keep chasing Leaflet's exact
   selector on every device/input combination, !important settles it
   outright: this is a single small custom control this app fully owns,
   not a shared style being fought over, so there's no real downside to
   just guaranteeing the outcome. */
.task-layer-control a,
.task-layer-control a:hover,
.task-layer-control a:focus,
.task-layer-control a:active {
  display: flex; align-items: center; justify-content: center;
  background: var(--panel-raised) !important; color: var(--paper) !important;
  outline: none;
}
.task-layer-control a.active,
.task-layer-control a.active:hover,
.task-layer-control a.active:focus,
.task-layer-control a.active:active {
  background: var(--yellow) !important; color: var(--asphalt) !important;
}


/* Basemap picker toggle icon (basemaps.js) — a self-contained SVG data
   URI, not Leaflet's own bundled icon (which is loaded from its CDN and
   wouldn't match this app's hand-drawn icon style anyway). Classic
   three-layer stack pictogram. */
.leaflet-control-layers-toggle {
  background-image: url('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="%23333333" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"%3E%3Cpath d="M12 2 L22 7 L12 12 L2 7 Z"/%3E%3Cpath d="M2 12 L12 17 L22 12"/%3E%3Cpath d="M2 17 L12 22 L22 17"/%3E%3C/svg%3E') !important;
  background-size: 20px 20px;
  background-position: center;
  background-repeat: no-repeat;
}

.map-legend {
  background: var(--panel);
  border: 1px solid var(--steel);
  border-radius: var(--radius);
  padding: 8px 10px;
  max-width: 200px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.35);
  /* A layer with many symbolize-by-field values (or several stacked
     layers/reference layers) can produce a legend taller than the map
     itself, which used to just run off the top of the screen with no
     way to reach the rows at the bottom. Capping height and scrolling
     internally keeps it fully usable at any size; disableScrollPropagation
     above already stops that scroll from panning the map underneath. */
  max-height: 60vh;
  overflow-y: auto;
}
.map-legend:empty { display: none; }
.map-legend-title {
  font-family: var(--font-display);
  font-size: 11px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--paper-dim);
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: 6px;
}
/* Separates the summary row from the entry rows below it — but ONLY
   while the <details> is actually open. Applied unconditionally (as
   this used to be, directly on .map-legend-title with no [open]
   qualifier), the closed/collapsed state — a <details> hides
   everything except <summary> when closed, so this margin was the last
   thing inside the box either way — still added its own space below
   the icon/text with nothing to visually balance it against above,
   since .map-legend's own 8px padding is symmetric top and bottom.
   That's what produced visibly more padding under the icon than above
   it in the collapsed pill. Scoping this to details[open] means the
   collapsed pill's padding comes from .map-legend's own symmetric 8px
   alone. */
details[open] > .map-legend-title { margin-bottom: 6px; }
/* Matches the reference "list" glyph (three lines, each with a small
   leading square) used elsewhere for this icon — drawn inline as SVG
   rather than pulled from an icon font/library, consistent with this
   app's no-new-dependencies rule. currentColor so a single color rule
   here controls the glyph — deliberately --chrome-bg (the same dark
   navy as the top/bottom chrome bars — see app.css's own --chrome-bg
   token, also themeable via Admin > Branding), not .map-legend-title's
   own --paper-dim text color; the icon and the "Legend" label are
   intentionally two different colors here, not one inherited from the
   other. */
.map-legend-icon { flex: none; color: var(--chrome-bg); }
.map-legend-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--paper);
  padding: 3px 0;
}
.map-legend-visibility-toggle { width: 14px; height: 14px; accent-color: var(--yellow); flex-shrink: 0; cursor: pointer; }
.map-legend-row span { overflow-wrap: anywhere; }
.map-legend-label-toggle {
  display: flex;
  align-items: center;
  gap: 6px;
  margin: 0 0 6px 26px; /* lines up under the row's text, past the swatch */
  font-size: 11px;
  color: var(--paper-dim);
  cursor: pointer;
}
.map-legend-label-toggle input { width: 14px; height: 14px; accent-color: var(--yellow); }

/* "Symbolize by field" legend rows (mapIcons.js's fieldValueEntries) —
   one per configured unique value, indented under their layer's own row
   so they read as belonging to it rather than being separate layers.
   Slightly smaller/dimmer than the layer row itself for the same
   visual-hierarchy reason. */
.map-legend-row-indented {
  margin-left: 22px;
  font-size: 11px;
  color: var(--paper-dim);
  padding: 2px 0;
}

/* Feature layer popups (featureLayers.js) — deliberately NOT styled to
   match the app's dark theme, same as the existing submission-pin popups:
   Leaflet's own default white-box popup chrome is left as-is everywhere
   in this app, so colors here are light-background-appropriate (grays/
   blues), not the app's own dark-theme palette. */
/* min-width lowered from 200px so a popup with genuinely short content
   (a brief title, one or two short attributes, or — after this
   session's other change — just a short "New Task" button) isn't
   forced wider than it needs to be; Leaflet's own popup minWidth
   option (see featureLayers.js's two L.popup({...}) calls) was lowered
   to match, since that's a second, independent width floor Leaflet
   applies on top of whatever this CSS says. */
.feature-popup {
  font-size: 13px; min-width: 120px;
  /* Fade-in on open (§ direct request — "windows and screens open/
     close" — a map feature popup is one of the most common "window"
     in this app). Safe to animate opacity+transform on THIS element
     specifically (not Leaflet's own outer .leaflet-popup wrapper,
     which Leaflet positions via its own transform/left/bottom — piling
     a second transform on top of that would fight its placement); this
     is purely our own content div, untouched by Leaflet's positioning
     logic. Retriggers naturally on its own every time popup.setContent()
     is called again (e.g. once for a "Loading…" placeholder, then again
     once real data arrives) since each call replaces this element
     entirely — no JS coordination needed for that. */
  animation: feature-popup-in 0.16s ease both;
}
@keyframes feature-popup-in { from { opacity: 0; transform: translateY(4px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }

.feature-popup-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.feature-popup-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.feature-popup-title { font-size: 15px; line-height: 1.2; flex: 1; }
.feature-popup-edit-btn {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 1px solid #d8d8d8;
  background: #f5f5f5;
  color: #555;
  cursor: pointer;
}
.feature-popup-edit-btn:active { background: #ececec; }

/* Attribute label/value pairs as a two-column grid — the label column
   sizes itself to the longest label, so every value lines up in a clean
   second column regardless of individual label lengths. */
.feature-popup-attrs {
  display: grid;
  grid-template-columns: auto 1fr;
  column-gap: 14px;
  row-gap: 5px;
  margin: 8px 0;
}
/* #3a3a3a, not the original #666 — direct feedback: with Bold applied
   (popupBlocks.js's styleToCssText, font-weight only) the label read
   noticeably less dark than a bold value, purely because the label's
   own base COLOR was a much lighter gray than the value's near-black
   #161616 — font-weight was never the actual gap. Still visually
   distinct from the value (a solid dark gray, not literal black) so
   label/value keep their own identity at a glance either way. */
.feature-popup-attr-label { color: #3a3a3a; font-weight: 600; white-space: nowrap; }
.feature-popup-attr-value { color: #161616; text-align: left; overflow-wrap: anywhere; }

.feature-popup-attribution { text-align: right; font-size: 10.5px; color: #999; margin-top: 8px; }

/* ArcGIS reference-layer popup (referenceLayers.js) — plain raw
   attribute dump from whatever ArcGIS returns, same light-bubble
   Leaflet popup context as .feature-popup above (hence the same fixed
   light-mode colors rather than CSS variables), but visually simpler
   since there's no edit button/photos/task section to make room for —
   this is read-only reference data, not one of the app's own features. */
.arcgis-popup { font-size: 13px; max-width: 220px; }
.arcgis-popup-table { border-collapse: collapse; margin-top: 6px; }
.arcgis-popup-table th, .arcgis-popup-table td { padding: 2px 8px 2px 0; text-align: left; vertical-align: top; }
.arcgis-popup-table th { color: #666; font-weight: 600; white-space: nowrap; }
.arcgis-popup-table td { color: #161616; overflow-wrap: anywhere; }
.feature-popup-attribution-line { text-align: right; }
.feature-popup-attribution-line + .feature-popup-attribution-line { margin-top: 2px; }

.feature-popup-photos {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin: 8px 0;
  max-height: 260px;
  overflow-y: auto;
}
.feature-popup-photo-item { display: flex; flex-direction: column; }
.feature-popup-photo {
  width: 100%;
  max-height: 160px;
  object-fit: cover;
  border-radius: 6px;
  border: 1px solid #ddd;
  cursor: pointer;
}
.feature-popup-photo-caption { font-size: 11.5px; color: #666; margin-top: 4px; }

.feature-popup-measurement {
  font-size: 12px; font-weight: 700; color: #222; margin-bottom: 8px;
}

.feature-popup-section { margin-top: 10px; padding-top: 10px; border-top: 1px solid #e4e4e4; }
/* Sized to its own content (not full popup width, see .btn's default
   inline-flex) so a short label like "New Task" doesn't force the whole
   popup wider than its other content actually needs. */
.feature-popup-new-task-btn, .feature-popup-open-task-btn {
  margin: 10px 0;
  /* Smaller than a normal .btn on purpose — this sits inside a small,
     already-compact map popup, not primary navigation, so trading a
     bit of the usual 48px touch-target minimum for a more proportional
     size here is a reasonable call; it's still comfortably tappable. */
  min-height: 32px;
  padding: 0 14px;
  font-size: 13px;
}
.feature-popup-section-title {
  text-transform: uppercase;
  font-size: 10.5px;
  letter-spacing: 0.06em;
  font-weight: 700;
  color: #8a8a8a;
  margin-bottom: 6px;
}

.feature-popup-submissions { max-height: 150px; overflow-y: auto; }
.feature-popup-submission-link {
  display: flex;
  justify-content: space-between;
  gap: 10px;
  padding: 6px 0;
  color: inherit;
  text-decoration: none;
  font-size: 12.5px;
  border-bottom: 1px solid #f0f0f0;
}
.feature-popup-submission-link:last-child { border-bottom: none; }
.feature-popup-submission-date { color: #666; flex-shrink: 0; }
.feature-popup-submission-title { color: #0057b8; text-align: right; overflow-wrap: anywhere; }

/* ---- Popup Designer: styled/laid-out popup output --------------------
   Every ".feature-popup" here — the real map popup AND the Designer's
   own live preview both use this exact class (see popupBlocks.js /
   popupDesigner.js) — reads these CSS custom properties, set inline
   per-popup by styleVarsForPopupConfig(). Falls back to the plain
   defaults above (min-width:120px etc.) when a popup has no Designer
   config at all, since the vars are simply absent then. */
.feature-popup {
  width: var(--popup-width, auto);
  max-width: var(--popup-width, 260px);
  border-radius: var(--popup-radius, 0);
}
.feature-popup .feature-popup-title { font-size: var(--popup-title-size, 15px); font-weight: var(--popup-title-weight, 600); }
.feature-popup .feature-popup-attrs { row-gap: var(--popup-row-gap, 5px); }

.popup-header-solid .feature-popup-header {
  margin: -4px -4px 8px; padding: 8px 10px; border-radius: var(--popup-radius, 6px) var(--popup-radius, 6px) 0 0;
  background: #eef4fb; /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--popup-accent, #0057b8) 10%, white);
}
.popup-header-minimal .feature-popup-header { border-bottom: 1px solid #e4e4e4; padding-bottom: 6px; }

.popup-layout-compact .feature-popup-section { margin-top: 6px; padding-top: 6px; }
.popup-layout-compact .feature-popup-attrs { row-gap: 2px; margin: 4px 0; }

.popup-layout-card .feature-popup-attrs,
.popup-layout-card .feature-popup-section {
  background: #f7f8fa; border: 1px solid #e8e9ec; border-radius: 8px; padding: 8px 10px;
}
.popup-layout-card .feature-popup-attrs { margin: 8px 0; }

.popup-block-heading { font-weight: 700; font-size: 12.5px; margin: 10px 0 4px; color: #333; }
.popup-block-text { font-size: 12.5px; color: #444; margin: 4px 0; white-space: pre-wrap; }
.popup-block-divider { border: none; border-top: 1px solid #e4e4e4; margin: 10px 0; }
.popup-block-field-bare { font-size: 13px; color: #161616; margin: 4px 0; grid-column: 1 / -1; }
.popup-block-single-field { margin: 6px 0; }
/* 2 tracks, not 4 — each field renders as ONE stacked label-over-value
   tile (popupBlocks.js's fieldTileHtml) rather than splitting label
   and value across separate grid tracks, which is what previously
   squeezed values down to just a few characters wide and made
   overflow-wrap:anywhere break every word mid-character. */
.popup-block-fieldlist-2col { grid-template-columns: 1fr 1fr; column-gap: 14px; row-gap: 10px; }
.popup-field-tile { min-width: 0; } /* lets the grid track actually shrink to its column width instead of the tile's own content forcing it wider */
.popup-field-tile .feature-popup-attr-label { display: block; white-space: normal; margin-bottom: 2px; }
.popup-field-tile .feature-popup-attr-value { display: block; overflow-wrap: break-word; word-break: normal; } /* break-word (at word boundaries first) reads far better than the 1-column layout's overflow-wrap:anywhere at this narrower width */
.popup-block-fieldlist-2col .popup-field-tile.popup-block-field-bare { grid-column: auto; }

.popup-photos-single .feature-popup-photo-item { max-width: 100%; }
.popup-photos-2col { flex-direction: row; flex-wrap: wrap; }
.popup-photos-2col .feature-popup-photo-item { width: calc(50% - 5px); }
.popup-photos-thumbs { flex-direction: row; flex-wrap: wrap; max-height: 140px; }
.popup-photos-thumbs .feature-popup-photo-item { width: 64px; }
.popup-photos-thumbs .feature-popup-photo { max-height: 64px; }

.feature-popup-zoom-btn { margin: 10px 6px 10px 0; min-height: 32px; padding: 0 14px; font-size: 13px; }

/* ---------- Forms toolbar / filters ---------------------------------------- */
.toolbar {
  display: flex; gap: 8px; padding: 10px 14px;
  border-bottom: 1px solid var(--steel);
  background: var(--panel);
  overflow-x: auto;
}
.toolbar input[type="search"] {
  flex: 1; min-height: 40px; background: var(--panel-raised);
  border: 1px solid var(--steel); border-radius: 8px; padding: 0 12px; color: var(--paper);
}
.chip {
  min-height: 36px; padding: 0 12px; border-radius: 20px;
  background: var(--panel-raised); border: 1px solid var(--steel);
  color: var(--paper-dim); font-size: 13px; white-space: nowrap;
}
.chip.active { background: var(--yellow); color: var(--asphalt); border-color: var(--yellow); }

/* ---------- Designer field builder ---------------------------------------- */
.designer-field {
  background: var(--panel);
  border: 1px solid var(--paper);
  border-radius: var(--radius);
  padding: 12px 14px;
  margin-bottom: 8px;
}
.designer-field-head { display: flex; align-items: center; gap: 8px; }
.designer-field-head select { width: auto; min-height: 36px; padding: 4px 8px; }

/* Drag-to-reorder handle. touch-action:none stops iOS from starting its own
   scroll gesture before our long-press timer gets a chance to run.
   -webkit-touch-callout and user-select stop iOS's long-press text-selection
   highlight/magnifier and "Copy/Look Up" menu from competing with the same
   long-press gesture — scoped to just the handle (not the rest of the
   card), so normal text selection in label/help-text inputs is unaffected. */
.drag-handle {
  color: var(--paper-dim);
  cursor: grab;
  padding: 8px 4px;
  display: flex;
  align-items: center;
  touch-action: none;
  flex-shrink: 0;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.drag-handle:hover { color: var(--paper); }
.designer-field.dragging {
  cursor: grabbing;
  box-shadow: 0 8px 20px rgba(0,0,0,0.45);
  opacity: 0.95;
}
.designer-field.dragging .drag-handle { cursor: grabbing; }
.field-drag-placeholder {
  border: 2px dashed var(--yellow);
  border-radius: var(--radius);
  margin-bottom: 8px;
  background: rgba(47, 111, 237, 0.08);
}
.designer-field-head input[type="text"] { min-height: 36px; padding: 6px 10px; }
.designer-toggles { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 10px; font-size: 13px; color: var(--paper-dim); align-items: center; }
.designer-toggles label { display: flex; align-items: center; gap: 6px; }
.designer-toggles .link-btn { flex: 0; }

/* Compact checkbox labels for config panels (Collapsible, Starts collapsed,
   etc.) — sizes to content instead of stretching like .choice-row does. */
.designer-check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  font-size: 13px;
  color: var(--paper-dim);
  margin-right: 16px;
  cursor: pointer;
}
/* Was 18px — one px over the standard 16px checkbox everywhere else in
   the app (input[type="checkbox"] above), which read as visibly
   inconsistent/oversized next to it. accent-color is a no-op now that
   the base rule above already fully custom-draws this checkbox, but
   left in place rather than removed since it's harmless and matches
   this rule's original intent. */

/* Manage Fields panel (Admin > Feature Layers > a layer's detail
   screen) — one row per database column, with a hairline separator
   between rows rather than around each one individually. */
.field-manage-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 0;
  border-top: 1px solid var(--steel);
}
.field-manage-row:first-child { border-top: none; }
/* Manage Fields' TABLE rows (admin.js's `class="field-manage-row
   fields-table-row"`) need NONE of the flex-row behavior above — that
   rule predates the CSS-grid table redesign and is still legitimately
   used as-is elsewhere (Symbolize by Field's own default/per-value
   rows). The real bug: .fields-table-row-summary (the grid) is a
   DIRECT CHILD of .field-manage-row, so once that parent is
   `display:flex`, the child becomes a flex item and — with no
   `flex-basis`/width of its own — sizes to its OWN CONTENT's natural
   width rather than stretching to fill the row, exactly like an
   inline-block would. Every row's grid container therefore ends up a
   different overall width depending on how long that row's own field
   name/badge/tag content happens to be, which is what was actually
   throwing off column alignment (not just the earlier `auto` Actions
   column fix, though that was real too). `.field-manage-row.fields-
   table-row` (two classes) outguns the plain `.field-manage-row`
   selector above on specificity, so this reliably wins regardless of
   file order, and resets it back to a plain block — letting
   `.fields-table-row-summary` (a normal block-level child again) take
   its full 100% width and make its grid identical across every row. */
.field-manage-row.fields-table-row {
  display: block;
  gap: 0;
  padding: 0;
  border-top: none;
}

/* Symbol Designer (symbolDesigner.js) — the Advanced tab of the Map
   symbol picker below (mapStylePicker.js), Admin > Feature Layers >
   Style, point layers only. */
.symdes-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--paper);
  margin-bottom: 14px;
}
.symdes-tab {
  border: none;
  background: none;
  padding: 8px 4px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-soft, var(--steel));
  cursor: pointer;
  border-bottom: 2px solid transparent;
  margin-right: 14px;
}
.symdes-tab.active {
  color: var(--ink, #14171A);
  border-bottom-color: var(--yellow);
}
.symdes-preview-card {
  border: 1px solid var(--paper);
  border-radius: var(--radius);
  padding: 14px;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 100px;
  background: var(--panel-raised);
}
.symdes-layer-list {
  border: 1px solid var(--paper);
  border-radius: var(--radius);
  overflow: hidden;
}
.symdes-layer-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-bottom: 1px solid var(--paper);
  cursor: pointer;
  background: var(--panel);
}
.symdes-layer-row:last-child { border-bottom: none; }
.symdes-layer-row.selected { background: rgba(47, 111, 237, 0.1); }
.symdes-layer-row:hover { background: var(--panel-raised); }
.symdes-drag-handle {
  display: flex;
  align-items: center;
  color: var(--steel);
  cursor: grab;
  touch-action: none;
  flex: 0 0 auto;
}
.symdes-icon-btn {
  border: none;
  background: none;
  padding: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-soft, var(--steel));
  cursor: pointer;
  flex: 0 0 auto;
  border-radius: 4px;
}
.symdes-icon-btn:hover { background: var(--panel-raised); }
.symdes-icon-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.symdes-layer-swatch {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
}
.symdes-layer-name {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Map symbol picker (mapStylePicker.js) — shared by the Designer's
   per-form Map Style section and the Admin's per-feature-layer one. */
.map-symbol-swatch {
  width: 44px;
  height: 44px;
  border-radius: var(--radius);
  border: 2px solid var(--steel);
  background: var(--panel-raised);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  position: relative;
}
.map-symbol-swatch.selected { border-color: var(--yellow); background: rgba(47, 111, 237, 0.12); }

/* Default-symbols / Saved-symbols card sections — each is a fixed
   6-rows-by-5-columns grid (5 * 44px swatches + 4 * 8px gaps = 252px
   wide; 6 * 44px + 5 * 8px = 304px tall) so both sections always read
   as the same shape regardless of how many symbols they hold; more
   than 30 entries scrolls inside the card instead of growing it (or
   pushing the rest of the picker's own controls further down). */
.map-symbol-card {
  border: 1px solid var(--paper);
  border-radius: var(--radius);
  padding: 10px;
  margin-bottom: 10px;
}
.map-symbol-card-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-soft, var(--steel));
  margin-bottom: 8px;
}
.map-symbol-grid {
  display: grid;
  grid-template-columns: repeat(5, 44px);
  gap: 8px;
  max-height: 304px;
  overflow-y: auto;
  padding-right: 2px;
}
/* Small always-visible delete affordance in a saved symbol's corner —
   only rendered for admins (canManageSymbolLibrary), see
   renderMapStylePicker's own saved-symbols grid. Always-visible rather
   than hover-only so it's reachable on touch devices too. */
.map-symbol-swatch-delete {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--steel);
  background: var(--panel-raised);
  color: var(--red);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  line-height: 1;
  font-size: 12px;
}

/* Label field chips (§ direct request: "let the user specify the field
   to label with... allow the user to concatenate multiple fields") —
   renderScaleAndLabelPicker's own Labels section, mapStylePicker.js.
   An ordered list of small pill chips (not a native <select multiple>
   — see that function's own comment on why order needs to be
   click-order, not alphabetical/DOM order) with inline reorder/remove
   controls, matching the compact chip-row look already established by
   .map-symbol-swatch above rather than a full table/list treatment for
   what's usually 1-3 entries. */
.salp-field-chip-list { display: flex; flex-wrap: wrap; gap: 6px; }
.salp-field-chip {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  padding: 4px 4px 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--steel);
  background: var(--panel-raised);
  font-size: 12px;
}
.salp-field-chip-label { margin-right: 2px; }
.salp-field-chip-btn {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  border: none;
  background: transparent;
  color: var(--ink-soft, var(--steel));
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  padding: 0;
  line-height: 1;
  font-size: 13px;
}
.salp-field-chip-btn:hover { background: var(--panel); }
.salp-field-chip-remove { color: var(--red); font-size: 11px; }

/* Visually shows which fields belong to which section — see the caption
   generated in renumberAndRerender() for the actual explanation. Color is
   set per-section inline (see the section's "Border color" control), this
   just supplies the shape and a default. */
.designer-section-group {
  margin: 16px 0;
  padding: 10px 12px 4px;
  border: 1px solid var(--yellow);
  border-radius: var(--radius);
}

/* ---------- Misc ------------------------------------------------------------ */
.empty-state { text-align: center; padding: 60px 24px; color: var(--paper-dim); }
.spacer-h { height: 12px; }
.row { display: flex; gap: 10px; }
.row > * { flex: 1; }
.hidden { display: none !important; }
.print-only { display: none; } /* shown only inside @media print, see below */
@keyframes toast-in { from { transform: translateX(-50%) translateY(16px); opacity: 0; } to { transform: translateX(-50%) translateY(0); opacity: 1; } }
.toast {
  position: fixed; left: 50%; bottom: 90px; transform: translateX(-50%);
  background: #000; color: #fff; border: 1px solid var(--steel);
  padding: 10px 16px; border-radius: 8px; font-size: 14px;
  /* Needs to sit above literally everything else that can be on
     screen at once, including things that already stack fairly high
     themselves (the update-banner at 300, Designer's Preview overlay
     at 200/201, app.css's own .modal-overlay at 100) — a toast firing
     while any of those is open should never end up hidden behind it.
     Matched to tasks.css's .task-toast so neither app can silently
     end up on top of the other's toast either. */
  z-index: 99999;
  animation: toast-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* Field Session tracking indicator (fieldSession.js) — deliberately
   fixed to the viewport (not inside #view-root, see that file's own
   top-of-file note) so it survives every router.navigate() call while
   a session is active/paused, matching spec §5's "GPS tracking must
   continue while workers navigate the app... without interrupting
   tracking." High enough to clear ordinary page content and floating
   map controls (those top out around 41 — see editor-toolbar/popup
   z-indices above), but BELOW .modal-overlay's 100 on purpose: an open
   modal/dialog the person is actively filling out (Start Field
   Session's own confirmation, Work Area editor, task modals, etc.)
   must always be able to fully cover this pill, not have it floating
   on top of the dialog. Also below the update banner's 300 and well
   below .toast's 99999, which should still be able to appear over
   everything including a modal. */
.field-session-pill {
  position: fixed; left: 50%; bottom: calc(var(--tabbar-h, 64px) + 10px); transform: translateX(-50%);
  display: flex; align-items: center; gap: 6px;
  background: var(--chrome-bg); color: #fff; border: 1px solid var(--chrome-cyan);
  padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 600;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  z-index: 90;
  cursor: pointer;
}
.field-session-pill.paused { border-color: var(--orange); opacity: 0.9; }
.field-session-pill-dot { color: var(--chrome-cyan); }
.field-session-pill.paused .field-session-pill-dot { color: var(--orange); }

/* "To Task" — a second, independent floating badge (fieldSession.js's
   own updateToTaskBadge()) — default position deliberately offset from
   the tracking pill above (bottom-right corner here vs. bottom-center
   for the pill) so the two don't visually collide when both happen to
   be showing at once (any task-bound active session shows both), while
   either can still be dragged anywhere the person wants — see
   .dragging below, shared by both badges. */
.to-task-badge {
  position: fixed; right: 14px; bottom: calc(var(--tabbar-h, 64px) + 10px);
  display: flex; align-items: center; gap: 6px;
  background: var(--yellow); color: var(--asphalt); border: 1px solid var(--yellow);
  padding: 6px 14px; border-radius: 999px; font-size: 13px; font-weight: 700;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
  z-index: 90;
  cursor: pointer;
}
.to-task-badge-icon { font-weight: 800; }

/* Shared by both floating badges while an active drag is in progress
   (fieldSession.js's makeBadgeDraggable()) — a visible "lifted" cue and
   a grabbing cursor, and disables the eased position transition
   neither badge otherwise has anyway (they're set via direct inline
   left/top, not CSS transition) so a fast drag never looks like it's
   lagging behind the pointer. */
.field-session-pill.dragging, .to-task-badge.dragging {
  cursor: grabbing;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
  opacity: 0.92;
}

/* "New version available" banner (app.js) — a new service worker has
   already taken control at this point (skipWaiting/clients.claim), this
   is purely about getting the currently-open page to actually load the
   new code. Deliberately a tap-to-refresh prompt, not an automatic
   reload — reloading out from under someone mid-inspection could lose
   an in-progress form. Positioned at the top rather than alongside the
   toast/tab bar area at the bottom, and above everything else in the
   app (including the Designer's Preview overlay, z-index 200/201). */
.update-banner {
  position: fixed;
  top: env(safe-area-inset-top);
  left: 0;
  right: 0;
  z-index: 300;
  background: var(--yellow);
  color: var(--asphalt);
  padding: 10px 16px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 14px;
  font-weight: 600;
}
.update-banner button {
  background: var(--asphalt);
  color: var(--yellow);
  border: none;
  border-radius: 6px;
  padding: 6px 14px;
  font-weight: 700;
  cursor: pointer;
  flex-shrink: 0;
}
.link-btn { background: none; border: none; color: var(--yellow); font-weight: 600; padding: 8px; cursor: pointer; }

/* =============================================================================
   DESKTOP LAYOUT SYSTEM
   ==========================================================================
   Everything below is gated behind @media (min-width: 1024px) — mobile and
   tablet layouts (below that width) are completely untouched, byte for
   byte, by this section. See architecture.md for the full writeup.

   The problem: every screen in this app is built mobile-first with plain
   block containers (.list-page, .task-theme, .card, etc.) that simply
   stretch to whatever width their parent (#view-root, ultimately the
   browser viewport) happens to be. That's correct on a phone; on a
   27"+ monitor it produces edge-to-edge forms, walls of empty
   whitespace either side of a single-column list, and text lines far
   past a comfortable reading width. None of that is a redesign problem
   — the existing components, colors, and spacing are all correct — it's
   purely a missing content-width constraint at large viewports.

   The fix is three reusable width primitives, applied to the existing
   containers each view already renders into rather than introducing new
   wrapper markup everywhere:
     --container-compact  (~900px)  — single-purpose forms: Capture's
                                       form-fill sheet, Feature Edit, Login.
     --container-standard (~1400px) — lists, records, admin, task screens.
     --container-fluid    (no cap)  — Map, the Form Designer, and the
                                       Dashboard's widget-heavy Builder —
                                       genuine workspace screens where more
                                       width is the point, not the problem.
   router.js stamps the active view's name onto #view-root as
   data-view="..." specifically so this stylesheet can tell which of the
   three a given screen needs without any view file having to know or
   care about desktop layout itself — .list-page/.task-theme/.sheet stay
   exactly the generic containers they already were. */

:root {
  --container-compact: 900px;
  --container-standard: 1400px;
  --desktop-gutter: 32px;
}

/* Reusable primitives themselves (architecture.md's <CompactContainer>/
   <StandardContainer>/<FluidContainer>), available for any view to use
   directly on new markup going forward, in addition to the retrofits
   below that apply them to what already exists. */
.container-compact,
.container-standard {
  width: 100%;
  margin: 0 auto;
  box-sizing: border-box;
}
.container-compact { max-width: var(--container-compact); }
.container-standard { max-width: var(--container-standard); }
.container-fluid { width: 100%; }

@media (min-width: 1024px) {

  /* ---- Form-type pages: Capture's landing/picker, Feature Edit, Login —
     these are .list-page instances whose content is a single form or a
     short picker list, not a browsable list of many records, so they get
     the narrower CompactContainer treatment ("Label / input" stretching
     to 900px is already plenty; stretching to 2000px is the actual
     complaint in the brief). */
  #view-root[data-view="capture"] > .list-page,
  #view-root[data-view="featureEdit"] > .list-page {
    max-width: var(--container-compact);
    margin: 0 auto;
    padding-left: var(--desktop-gutter);
    padding-right: var(--desktop-gutter);
  }

  /* Login has no .list-page wrapper (a plain full-height centered flex
     column, see login.js) — same CompactContainer treatment applied to
     its own root element directly. */
  #view-root[data-view="login"] > div {
    max-width: var(--container-compact);
    margin: 0 auto;
    width: 100%;
  }

  /* ---- List/management pages: Admin — StandardContainer, applied
     directly since Admin's screens are always a direct-child .list-page. */
  #view-root[data-view="admin"] > .list-page {
    max-width: var(--container-standard);
    margin: 0 auto;
    padding-left: var(--desktop-gutter);
    padding-right: var(--desktop-gutter);
  }
  /* ---- Records viewer: two different root shapes depending on state
     (see viewer.js), each centered on its own terms rather than one
     shared rule, so the two don't fight over padding:
       - Detail/print view: a direct-child .list-page, same treatment
         as Admin above.
       - List view: no .list-page at the view root at all — a
         full-height flex column (toolbar + category tabs + an
         internally-scrolling #results-area) with the card list's own
         .list-page nested one level deeper, inside that scroll region.
         Centering the OUTER column (not the inner .list-page) is what
         actually keeps the toolbar/tabs aligned with the card list
         below them; the inner .list-page deliberately gets no extra
         padding here; its existing 14px already lines up with
         .toolbar's own 14px now that both sit inside the same centered
         parent, and adding a second, larger padding just on the card
         list would misalign it against the toolbar above it. */
  #view-root[data-view="viewer"] > .list-page {
    max-width: var(--container-standard);
    margin: 0 auto;
    padding-left: var(--desktop-gutter);
    padding-right: var(--desktop-gutter);
  }
  #view-root[data-view="viewer"] > div:not(.list-page) {
    max-width: var(--container-standard);
    margin: 0 auto;
    width: 100%;
  }
  /* The Records viewer's own answer-review card is closer to a "form
     page" for reading purposes (see architecture.md's Typography note:
     avoid paragraphs/answer lists spanning a whole monitor) even though
     the surrounding Records list is a StandardContainer — so it gets a
     narrower cap of its own, nested inside the wider container above. */
  #view-root[data-view="viewer"] .answers-card-segment,
  #view-root[data-view="viewer"] .answers-section {
    max-width: var(--container-compact);
    margin-left: auto;
    margin-right: auto;
  }

  /* ---- Task module (My Work / Manage Tasks / Features browser / Task
     Detail — all render into one shared .task-theme root, see
     architecture.md §11) — StandardContainer. Explicitly excludes
     .designer-page: the Form Designer also reuses .task-theme for its
     look (§14) but is a genuine three-panel workspace screen that needs
     its own full-width treatment below, not this list-page centering.
     A descendant (not direct-child) selector on purpose: the supervisor
     switcher (tasksHome.js) renders My Work/Manage Tasks/Features into a
     nested #tasks-subview, so their own .task-theme root isn't a direct
     child of #view-root the way every other view's is. */
  #view-root[data-view="tasks"] .task-theme:not(.designer-page),
  #view-root[data-view="taskDetail"] .task-theme:not(.designer-page) {
    max-width: var(--container-standard);
    margin: 0 auto;
    padding-left: var(--desktop-gutter);
    padding-right: var(--desktop-gutter);
  }

  /* ---- Projects (§18) — the projects list and a single project's
     detail/overview screen. Same StandardContainer treatment as the
     task module above (both are list/management screens, not a
     single-purpose form or a workspace screen), and reuses the same
     .task-theme look for the same reason the task module and Form
     Designer both already do (architecture.md's "reuse the Tasks tab's
     look" precedent). Direct-child selector is correct here — unlike
     the task module, neither projects.js nor projectDetail.js nests
     its .task-theme root inside another wrapper. */
  #view-root[data-view="projects"] > .task-theme,
  #view-root[data-view="projectDetail"] > .task-theme {
    max-width: var(--container-standard);
    margin: 0 auto;
    padding-left: var(--desktop-gutter);
    padding-right: var(--desktop-gutter);
  }

  /* ---- Form-fill pages (Capture's actual full-screen form, and the
     Designer's legacy full-screen Preview overlay) — both are
     .sheet.form-render-root, deliberately left full-height/full-width
     as a page background (so it still reads as "the current screen",
     not a shrunk modal), with just the actual field content centered
     to a comfortable form width inside it. */
  .sheet.form-render-root .sheet-header,
  .sheet.form-render-root .sheet-body,
  .sheet.form-render-root .sheet-footer {
    max-width: var(--container-compact);
    margin-left: auto;
    margin-right: auto;
    width: 100%;
    box-sizing: border-box;
  }

  /* ---- Small bottom-sheet dialogs (e.g. the Records viewer's Filters
     sheet) that are appended straight to <body> rather than #view-root,
     for a short, focused set of fields — not a full page. On a phone
     these correctly slide up from the bottom edge-to-edge; on a desktop
     monitor that reads as a mobile pattern stretched wide, so past this
     breakpoint they become a normal centered dialog instead, sized to
     their content instead of the viewport. form-render-root sheets
     (handled above) are excluded — those are full pages, not dialogs. */
  body > .sheet-backdrop + .sheet:not(.form-render-root) {
    left: 50%;
    right: auto;
    bottom: auto;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    max-width: 480px;
    max-height: 80vh;
    border-radius: var(--radius);
  }

  /* ---- Generic modal (.modal-overlay/.modal-box) — already centers
     and caps at 720px at every width (see that rule's own definition),
     so no retrofit is needed here; these size variants are additive,
     opt-in classes for call sites that want something other than that
     default 720px, matching architecture.md's "small/medium/large/
     extra-large" dialog sizing note. */
  .modal-box-sm { max-width: 480px; }
  .modal-box-lg { max-width: 960px; }
  .modal-box-xl { max-width: 1200px; }

  /* ---- Task module's own bottom-sheet modal (taskCreateModal.js's
     openModal() — New Task, Assign, comments, etc.), same reasoning as
     the plain .sheet dialogs above: a full-width bottom sheet reads as
     mobile UI on a desktop monitor, so it becomes a centered dialog
     instead, sized to its content rather than the full viewport width. */
  body > .task-modal-backdrop + .task-modal {
    left: 50%;
    right: auto;
    bottom: auto;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 100%;
    max-width: 560px;
    max-height: 80vh;
    border-radius: var(--task-radius, var(--radius));
  }

  /* ---- Map: stays full-width by design (architecture.md's "some
     screens like maps may intentionally remain full width") — but its
     floating panels should read as fixed-width desktop inspector
     panels, not elements sized for a phone screen. */
  .editor-toolbar-panel { width: 300px; }
  .search-panel { width: 300px; }
  /* The multi-feature select attribute-table panel stretches to the
     map's own full width on desktop too, same as it already does on
     mobile (it used to become a small fixed-560px floating panel here,
     matching the editor-toolbar/search panels — deliberately reversed:
     a real attribute table with several columns is genuinely hard to
     work with squeezed into a 560px box on a screen that has the whole
     map's width to spare, and unlike the editor toolbar/search panels
     it's meant to be read as a data table, not a small side panel). Its
     own drag-to-resize (height only, see featureSelect.js) and
     state-mid/full/collapsed heights are untouched — this is the base
     (mobile) rule's own left:0/right:0 already, so there is nothing
     left to override here at all past this breakpoint. */

  /* ---- Typography: cap prose line length wherever the app renders a
     genuine paragraph of text (modal copy, the user guide, etc.) rather
     than short labels/values — those already read fine at any width. */
  .modal-body p,
  .modal-body li {
    max-width: 70ch;
  }

  /* ---- Desktop-only interaction polish (architecture.md's "Desktop
     enhancements" note) — gated on actual hover+precise-pointer support
     so this never fires on a touchscreen laptop/tablet in between the
     mobile and desktop breakpoints, only real mouse+trackpad use. Cards
     and rows across the app already have a "selected"/"active" state;
     this just adds a subtle hover cue on the ones that don't, so a
     mouse user gets the same "this is clickable" feedback a touch user
     gets for free from the platform's own tap highlight. */
  @media (hover: hover) and (pointer: fine) {
    .card[id],
    .task-card,
    .designer-toolbox-tile:not(.disabled) {
      transition: border-color 0.12s ease, box-shadow 0.12s ease;
    }
    .card[id]:hover {
      border-color: var(--paper-dim);
    }
    .task-card:hover {
      border-color: var(--task-text-faint);
      box-shadow: var(--task-shadow);
    }
  }
}

/* =============================================================================
   DESKTOP CONTROL DENSITY
   ==========================================================================
   Gated behind the same @media (min-width: 1024px) breakpoint the rest of
   the "Desktop Layout System" above already uses — mobile/tablet is
   completely untouched, byte for byte, by this section too.

   The problem: every text input, select, textarea, and button in this app
   is sized with --tap-min (48px) and correspondingly generous padding —
   correct, deliberate touch-target sizing for a finger on a phone, but
   oversized and "chunky" for a mouse-and-keyboard desktop session, where
   typical control heights run more like 32-38px. This never had a
   desktop-specific override the way container widths (above) already did,
   which is what actually produced the "looks optimized for mobile"
   impression on a wide monitor even once the CONTAINER widths were already
   fixed — the controls INSIDE those containers were still full mobile
   size.

   Font-size is deliberately left untouched everywhere here — 16px on
   .task-theme's own inputs specifically prevents iOS Safari's automatic
   zoom-on-focus (see tasks.css's own long comment on that input rule);
   shrinking only the box (height/padding) around that same 16px text,
   never the text itself, is what actually reads as "desktop-density"
   without reopening that mobile-only bug on any device narrow enough to
   still be mobile-styled regardless of this rule's own min-width gate.

   .task-theme's own inputs/.task-btn get the identical treatment, but
   defined in tasks.css itself rather than here — tasks.css loads AFTER
   this file, so a same-specificity override placed here would actually
   LOSE to tasks.css's own base rule regardless of source position within
   THIS file; the override has to live in the file that loads second to
   reliably win the cascade tie. */
@media (min-width: 1024px) {
  input[type="text"], input[type="number"], input[type="email"], input[type="tel"],
  input[type="password"], input[type="search"], input[type="date"], input[type="time"],
  input[type="datetime-local"], textarea, select {
    min-height: 38px;
    padding: 8px 12px;
  }
  textarea { min-height: 84px; } /* still meaningfully taller than a single-line field, just not the mobile 96px */
  .btn {
    min-height: 38px;
    padding: 0 16px;
  }
  /* .tabbar button deliberately excluded — the bottom navigation bar
     stays functionally identical (and just as reachable) on desktop as
     on mobile in this app, so its own touch-sized rows are left alone. */
}

/* Powers the "Export to PDF" button on a record's detail page (viewer.js)
   — it just calls window.print(); every browser, including iOS Safari,
   offers "Save as PDF" from its print dialog, so this needs no PDF library
   or server-side dependency at all. Scoped to #print-area (the detail
   page's own content) rather than a general app-wide print redesign. */
@media print {
  #topbar, #tabbar, .no-print { display: none !important; }

  /* This app's shell is built for on-screen mobile-app UI: a fixed-height
     viewport with an internally-scrolling content area (#view-root),
     rather than a naturally-flowing document. That's exactly wrong for
     print — a print engine doesn't paginate through a scroll container,
     it only captures whatever's visible within it, which is why the
     export was being clipped to a single page's worth of content no
     matter how long the actual record was. Overriding the whole chain
     back to a normal flowing document is what lets the browser's print
     engine break content across as many pages as it actually needs.
     Every level in the chain has to be overridden, not just #view-root —
     a fixed-height ancestor still constrains its children even if the
     child itself is set to overflow:visible. */
  html, body, #app, #view-root {
    height: auto !important;
    overflow: visible !important;
    position: static !important;
  }

  /* The app's theme is dark-on-dark, which would print as mostly solid
     black — force light/ink-friendly colors for the record content
     specifically instead. */
  body, #print-area {
    background: #ffffff !important;
    color: #000000 !important;
  }
  #print-area .card {
    background: #ffffff !important;
    border: 1px solid #999 !important;
  }
  .answers-card-segment .field + .field {
    border-top-color: #ddd !important;
  }
  /* Only this card, not .card generally (which stays full-width elsewhere
     in the app on purpose) — its content is a small map plus a couple of
     text lines, so the border should hug that instead of spanning the
     full page width. */
  #location-card {
    width: fit-content !important;
  }
  #detail-map {
    border: 1px solid #ccc !important;
  }
  #print-area h1, #print-area h2, #print-area .card-title,
  #print-area .field-label, #print-area .answers-section-heading {
    color: #000000 !important;
  }
  #print-area .card-meta { color: #444444 !important; }
  #print-area .tag {
    background: #eeeeee !important;
    color: #000000 !important;
    border: 1px solid #999 !important;
  }

  .print-only { display: block !important; }

  /* position:fixed inside a print stylesheet repeats the element on every
     printed page (each page acts as its own viewport for this purpose) —
     unlike position:sticky, which has no such repeat behavior. This is
     what makes a single element in the markup show up on every page of a
     multi-page export, not just once at the very end. */
  /* Deliberately NOT position:fixed. Chrome's print engine has a real,
     documented reliability problem with fixed-position elements during
     print pagination — instead of pinning to each individual page like
     the spec describes, it can end up positioning against the entire
     multi-page document's total height instead, landing partway down
     and off-center. Plain in-flow centering has none of that ambiguity:
     it's just a normal block element at the end of the content. The
     trade-off is that this places the logo once, at the end of the
     document, rather than repeating on every page. */
  /* float:right is a decades-old, thoroughly reliable CSS layout
     technique, unlike position:fixed in print (see the note on why that
     was abandoned, in the git history / prior conversation). Placed as
     the first element in the markup so it floats to the top-right of the
     very start of the document, with the title/heading text wrapping
     around it below. */
  .print-corner-logo {
    float: right !important;
    height: 44px !important;
    width: auto !important;
    margin: 0 0 8px 12px !important;
  }
}

/* =============================================================================
   Admin > Feature Layers > a layer > Map Style — split-screen live
   preview (desktop only). Mobile/tablet (below the same 1024px
   breakpoint the rest of the app's "Desktop Layout System" uses) keeps
   the original single-column layout with no live map at all — a real
   Leaflet map instance eating 75% of a phone screen inside a details
   section isn't useful the way it is on a wide monitor, and admin.js's
   own JS only ever creates the map instance when isDesktopViewport()
   is true in the first place, so .map-style-live-map-col below is
   belt-and-suspenders (JS already won't populate it on mobile — this
   CSS just also guarantees it takes no layout space if it somehow did).
   ========================================================================== */
.map-style-live-map-col { display: none; }

@media (min-width: 1024px) {
  #map-style-details[open] .map-style-split {
    display: flex;
    align-items: flex-start;
    gap: 24px; /* a bit more breathing room between the map and the Layer Style menu than the original 20px */
  }
  #map-style-details[open] .map-style-live-map-col {
    display: block;
    flex: 3 1 0;
    min-width: 0;
    /* Sticky so a long right-hand controls column (Symbolize by Field
       with several value rows expanded) can scroll past a shorter map
       without the map scrolling out of view with it — the whole point
       of the split view is seeing the map WHILE adjusting controls. */
    position: sticky;
    top: 12px;
  }
  #map-style-details[open] .map-style-controls-col {
    flex: 1 1 0;
    min-width: 300px;
    max-width: 420px;
    /* Its own independent scroll, matched to the live map's own fixed
       height (74vh) — without this the column just grows to whatever
       height Symbolize by Field's several expanded value rows need,
       pushing the whole PAGE to scroll instead, which drags the
       sticky live map along with it past its own container's bottom
       edge (position: sticky only holds within its parent's box) —
       defeating the entire point of a persistent live preview while
       scrolling through controls. */
    max-height: 74vh;
    overflow-y: auto;
    /* Item 1 (a later request) — border removed. No border/background
       of its own now; padding kept so content doesn't hug the scroll
       edge, and the right-side padding stays slightly smaller than
       the other three so the scrollbar doesn't crowd slider/input
       edges. */
    padding: 14px;
    padding-right: 10px;
  }
  .map-style-live-map {
    width: 100%;
    height: 74vh;
    border-radius: var(--radius);
    overflow: hidden;
    /* Matches .map-style-controls-col's own border exactly (color and
       thickness) — both were a thin 1px before; the controls panel
       moved to 2px for its own reasons (see that rule's comment) and
       the map's border was brought up to match it rather than leaving
       the two panels with two different border weights side by side. */
    border: 2px solid var(--steel);
  }
}

/* Below 1024px (and whenever the details is closed even on desktop —
   the [open] attribute selector above simply doesn't match), the split
   container is never given `display: flex`, so it stays a plain block
   and .map-style-controls-col renders exactly as the old single-column
   Map Style section always did. The Autosave checkbox itself reuses
   .designer-check's existing flex/gap layout — no extra rule needed. */

/* Saved Maps — Maps home screen card grid (maps.js). Reuses .task-card's
   existing padding/border/background via the shared class on the same
   element, rather than a second card component. */
.maps-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
  gap: 12px;
  margin-top: 10px;
}
.map-card { padding: 0; overflow: hidden; }
.map-card-thumb {
  width: 100%;
  height: 110px;
  object-fit: cover;
  display: block;
  background: var(--panel-alt, #1c2430);
}
.map-card-thumb-empty {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
  opacity: 0.5;
}
.map-card-body { padding: 10px 12px; }
.map-card-title { font-weight: 600; margin-bottom: 2px; }

/* Map Builder (mapBuilder.js) — layer panel + map, same two-column
   shape as the reference layout in the spec (Layers | MAP). Reuses
   .map-view-wrap's map container sizing where possible; this is the
   panel sitting alongside it. */
.map-builder-wrap {
  display: flex;
  flex-direction: column;
  height: 100%;
}
/* Overrides tasks.css's generic `.task-theme { padding: 14px 14px 24px; }`
   (mapBuilder.js's render() puts both classes on the same root element,
   reusing the Tasks module's light look — see that file's own header
   comment — without wanting its generic breathing-room padding too).
   Map Creator is a full-bleed workspace screen like Map/the Form
   Designer, not a padded content page, so that padding read as an
   unwanted gap between the app header and the screen's own topbar/
   panels. Needs the compound selector (not a second, separate
   `.map-builder-wrap` rule) to out-specificity tasks.css's single-class
   rule regardless of stylesheet load order — tasks.css loads after this
   file, so an equal-specificity rule here would otherwise lose. */
.task-theme.map-builder-wrap { padding: 0; }
.map-builder-topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-bottom: 1px solid var(--steel);
  flex: 0 0 auto;
}
.map-builder-name-field {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 120px;
  color: var(--paper-dim);
}
.map-builder-name-field input[type="text"] { flex: 1; min-width: 0; }
/* This class was referenced everywhere (mapBuilder.js's markup, this
   file's own comments) as if it already laid its children out in one
   row, but never actually had a CSS rule of its own — .map-builder-topbar
   itself IS a flex row, but .map-builder-topbar-controls (wrapping the
   name field + Share/⋯/Save together as ONE flex item inside it) fell
   back to a plain block box with no `display` override, so its
   children stacked in normal block/inline flow instead: the name
   field (itself `display:flex`, but still a full-width BLOCK box from
   its parent's point of view) claimed the whole line, forcing
   Share/⋯/Save onto a second line below it. § direct request: "The
   Share button, 3 dots button, and Save button need to be on the same
   row as the map name input box no matter what" — this is that "no
   matter what": the row now actually IS one flex row, the same fix
   already applied to the desktop panel's own equivalent row
   (.map-builder-panel-topbar-row) previously. */
.map-builder-topbar-controls {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1 1 auto;
  min-width: 0;
  flex-wrap: nowrap;
}
.map-builder-topbar-controls .mb-save-btn { margin-left: auto; }
.map-builder-body {
  display: flex;
  flex: 1 1 auto;
  min-height: 0;
}
.map-builder-panel {
  width: 260px;
  flex: 0 0 260px;
  border-right: 1px solid var(--steel);
  padding: 10px;
  display: flex;
  flex-direction: column;
  min-height: 0;
}
/* Only #mb-layer-list scrolls — the panel-topbar (desktop only, see
   below), the Layers header, and the Add Layer button all stay pinned
   in place instead of scrolling out of view with a long layer list.
   Base rule (not just the desktop media query) since this is a general
   improvement for the mobile bottom sheet too: the Add Layer button
   used to be the last thing in the panel's own overflow-y:auto scroll
   region, so on a layer-heavy map it could scroll out of the visible
   25vh sheet entirely. */
.map-builder-panel-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
}
.map-builder-map-col { flex: 1 1 auto; position: relative; min-width: 0; }
/* Explicit z-index:0 (not just leaving it auto) for the same reason
   #map's own rule (earlier in this file) and .walk-capture-map-wrap
   #wc-map both have one: Leaflet's internal panes/controls assign
   themselves z-index values from 200 up past 1000, and without a
   stacking context to contain them right at this element, those
   internals compare DIRECTLY against unrelated z-index values
   elsewhere in the document — including .task-modal's 41 (openModal(),
   used by mapBuilder.js's Add Layer / layer-config / Share modals) —
   and can render above a modal despite the modal's z-index looking
   much higher on paper. Missing this exact treatment here (the only
   Leaflet map in the app that didn't have it yet) was reported as
   "the Add Layer modal shows up below the map." */
.map-builder-map-col #map-builder-map { width: 100%; height: 100%; position: relative; z-index: 0; }

/* Panel header row — "LAYERS" + Basemap select + "Set Current View"
   together on one line, matching the mobile reference mockup (mobile
   layout below reorders this whole panel below the map as a bottom
   sheet, and this is the "sheet handle" row sitting right under the
   drag affordance). Wraps on very narrow widths rather than clipping —
   there's no meaningfully narrower target device than this app already
   supports elsewhere. */
.map-builder-panel-drag-affordance { display: none; } /* no longer shown at any width — the mobile bottom-sheet treatment used to render a decorative pill here, removed to match the reference screenshot (no drag handle) */

/* Name field + Share/⋯/Save, duplicated into the top of the panel for
   desktop (see mapBuilder.js render()'s own header comment on why this
   is a duplicate-and-sync rather than a reparented single copy).
   Hidden by default (mobile keeps these in .map-builder-topbar-controls
   instead); shown by the desktop media query below. */
.map-builder-panel-topbar {
  display: none;
  flex-direction: column;
  gap: 8px;
  padding-bottom: 12px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--steel);
  flex: 0 0 auto;
}
/* The back button sits on its own row above the name field (see the
   reference screenshot) — everything else (name field + Share/⋯/Save)
   now shares ONE row below it (§ direct request: "move the Share
   button, three dots, and Save button on the same row as the map name
   input box" — .map-builder-panel-topbar-row used to only wrap the
   actions, with the name field as a THIRD separate stacked row of its
   own; mapBuilder.js's render() now nests both inside this one row
   instead). */
.map-builder-panel-topbar > .mb-back-btn { align-self: flex-start; }
.map-builder-panel-topbar-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* The name field shrinks to share the row with three buttons instead
   of claiming the panel's full width (§ direct request: "make the map
   input box width smaller to fit the other buttons on the same row")
   — flex:1 1 auto still lets it take up whatever room the buttons
   don't need, but min-width is small enough that it never pushes Save
   off the edge of the 320px-wide desktop panel the way width:100%
   used to. */
.map-builder-panel-topbar-row .map-builder-name-field { flex: 1 1 auto; min-width: 0; }
.map-builder-panel-topbar-row .map-builder-name-field input[type="text"] { width: 100%; }
/* Matches .task-btn-sm's own min-height (tasks.css) exactly (§ direct
   request: "make the map name input box the same height as the other
   three buttons") — the global input[type="text"] rule (app.css) is a
   much taller mobile-tap-target size (min-height: var(--tap-min),
   padding: 12px 14px) that's appropriate for a full-width mobile form
   field but not for sitting inline next to three 32px-tall buttons on
   a 320px-wide desktop panel. */
.map-builder-panel-topbar-row input[type="text"] {
  min-height: 32px;
  padding: 6px 10px;
  font-size: 13px;
}
.map-builder-panel-topbar-row .mb-save-btn { margin-left: auto; }
.map-builder-panel-topbar-row .mb-saved-msg { white-space: nowrap; }

.map-builder-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 8px 10px;
  margin-bottom: 6px; /* was 10px (§ direct request: "make the panel height that these two buttons sit in smaller") */
}
.map-builder-panel-title {
  display: flex;
  align-items: center;
  gap: 6px;
  font-weight: 700;
  font-size: 13px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--paper);
  flex: 0 0 auto;
}
.map-builder-panel-header-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1 1 auto;
  justify-content: flex-end;
  min-width: 0;
}
/* Matches #mb-set-view-btn's own task-btn/task-btn-sm sizing exactly
   (tasks.css) — a plain <select> otherwise falls back to this app's
   global select rule (min-height: var(--tap-min), padding: 12px 14px),
   a much taller mobile-tap-target size that visibly towers over the
   32px button sitting right next to it in the same row (§ direct
   request: "The Basemap dropdown needs to be the same height as the
   Set Current View button"). A plain min-height match alone (this
   rule's own previous version) still isn't a GUARANTEED match across
   browsers — a native <select>'s own OS-drawn chrome around its
   dropdown arrow can push its real rendered height past an explicit
   min-height the same way an input's internal UA padding sometimes
   does, so height/max-height (not just min-height) plus a fully
   custom-drawn appearance (removing that native chrome entirely, same
   "guaranteed to look the same everywhere" approach this file already
   takes for checkboxes/radios above) closes that gap for good instead
   of relying on the browser to render its own arrow inside whatever
   box this CSS asks for. */
.map-builder-basemap-select {
  flex: 0 1 auto;
  min-width: 0;
  width: auto;
  height: 32px;
  min-height: 32px;
  max-height: 32px;
  padding: 0 28px 0 12px;
  font-size: 13px;
  appearance: none;
  -webkit-appearance: none;
  -moz-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23aab3c8' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
#mb-set-view-btn { white-space: nowrap; flex: 0 0 auto; }

@media (max-width: 720px) {
  .map-builder-topbar {
    gap: 6px;
    padding: 8px 10px;
    flex-wrap: nowrap;
  }
  .map-builder-topbar .task-btn {
    padding: 6px 10px;
    min-height: 32px;
    font-size: 13px;
    flex: 0 0 auto;
  }
  .map-builder-topbar #mb-back { padding: 6px 8px; }
  .map-builder-name-field { min-width: 50px; }
  /* Matches the buttons beside it in the same row (32px, set just
     above for .map-builder-topbar .task-btn) — otherwise this falls
     back to the app's global input rule (min-height: var(--tap-min),
     padding: 12px 14px), a taller mobile-tap-target size that doesn't
     match Share/⋯/Save sitting right next to it (§ direct request:
     "The map name input box needs to be the same height as the other
     boxes"). */
  .map-builder-topbar input[type="text"] {
    min-height: 32px;
    padding: 6px 10px;
    font-size: 13px;
  }
  /* Supplementary "Saved." confirmation — showToast() already gives
     the same feedback, so this is the first thing to drop when the
     topbar is tight on width rather than something needing its own
     shrink behavior. */
  #mb-saved-msg { display: none; }

  .map-builder-body { flex-direction: column; }
  /* A true 2/3 : 1/3 split of .map-builder-body's own real height (§
     direct request: "make the map height bigger" — the previous 33vh
     panel height was measured against the FULL viewport, not against
     the space actually available inside .map-builder-body, which sits
     below the topbar (a separate, fixed-height element outside this
     flex column entirely) — so the panel was silently claiming more
     than its intended third, and the map correspondingly less than
     its intended two-thirds, by exactly the topbar's own height).
     flex-basis:0 on both (the "0" in "2 1 0" / "1 1 0") is what makes
     this a real proportional split of whatever height this column
     actually has, rather than each one's own preferred/content size —
     the 2:1 ratio between their flex-grow values (the first number)
     is what then divides that real space 2/3 to the map, 1/3 to the
     panel, with no dependency on viewport height at all. Panel content
     beyond its own third still scrolls internally — see
     .map-builder-panel's own overflow-y:auto rule above, unchanged for
     this breakpoint — rather than the map ever losing its share. */
  .map-builder-map-col { order: 1; flex: 2 1 0; min-height: 0; }
  .map-builder-panel {
    order: 2;
    width: 100%;
    height: auto;
    flex: 1 1 0;
    min-height: 0;
    border-right: none;
    border-bottom: none;
    border-top: none;
    position: relative;
    z-index: 1;
    border-radius: 16px 16px 0 0;
    box-shadow: 0 -6px 18px rgba(16, 24, 48, 0.12);
    padding-top: 16px;
  }
  .map-builder-panel-header-controls { justify-content: flex-start; }
}

/* Desktop (item 1): the Layers panel floats over a full-bleed map
   instead of sharing width with it — .map-builder-map-col fills the
   entire body area and .map-builder-panel becomes an absolutely
   positioned card on top of it. The outer topbar strip is reduced to
   just the back button (its name/share/⋯/save controls move into the
   panel's own .map-builder-panel-topbar, shown here); see
   mapBuilder.js render()'s own comment for why both copies exist in
   the DOM rather than one being moved. */
/* Desktop (item 1 revision — matches the reference screenshot exactly:
   NOT a floating overlay. The outer topbar disappears entirely (its
   back button + name/share/⋯/save all live in .map-builder-panel-topbar
   instead — see mapBuilder.js render()'s own comment on the duplicate-
   and-sync approach), and .map-builder-panel goes back to being a
   plain in-flow sidebar (fixed width, border-right, full height) sitting
   beside the map — same shape the mobile-less original desktop layout
   already had, just wider and with the topbar's controls folded into
   its own top instead of a separate row above everything. */
@media (min-width: 721px) {
  .map-builder-topbar { display: none; }
  .map-builder-panel-topbar { display: flex; }
  .map-builder-panel { width: 320px; flex: 0 0 320px; }
}
.map-builder-layer-row {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Smaller than this row used to be (§ direct request: "make the
     Layer card rows height smaller") — the drag handle just below is
     the actual tallest element in this row (its own padding, not this
     one, previously dominated the row's real height), so shrinking
     just this padding wasn't enough on its own; see
     .map-builder-drag-handle's own comment for its half of this. */
  padding: 3px 0;
  border-bottom: 1px solid var(--steel);
  background: var(--panel);
  /* The drag handle below already blocks touch-selection on itself,
     but a drag gesture that starts there still drags the finger across
     this row's own name text (and, mid-reorder, briefly over
     neighboring rows) — without this here too, that motion can still
     trigger iOS/Android's own text-selection highlight over whatever
     it passes across, competing with the reorder gesture itself. */
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.map-builder-layer-row .name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Bigger than app.css's own .drag-handle (used by the Forms Designer's
   field rows) on purpose — this app is mobile-first and this handle is
   in a narrower side panel, so a larger touch target matters more here
   than visual density. Same drag-reorder mechanics as the Designer's
   own rows (see dragReorder.js's own doc comment for why it's a
   separate, simplified module rather than importing designer.js's
   version directly), just a bigger grip.
   Padding trimmed from 10px to 6px top/bottom (§ direct request, same
   "smaller Layer card rows" ask above) — this was the actual tallest
   part of each row (10px padding + the handle icon's own height, vs
   the row's own now-3px padding around the shorter checkbox/name/icon-
   buttons), so the row's real on-screen height barely moved until this
   shrank too. Still a full 6px above/below the icon, comfortably above
   any minimum real-world touch target on top of the icon's own size. */
.map-builder-drag-handle {
  color: var(--paper-dim);
  cursor: grab;
  padding: 6px 6px;
  display: flex;
  align-items: center;
  touch-action: none;
  flex-shrink: 0;
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
.map-builder-drag-handle:hover { color: var(--paper); }
.map-builder-layer-row.dragging {
  cursor: grabbing;
  box-shadow: 0 8px 20px rgba(0,0,0,0.45);
  opacity: 0.95;
  border-radius: var(--radius);
}
.map-builder-layer-row.dragging .map-builder-drag-handle { cursor: grabbing; }
.list-drag-placeholder {
  border: 2px dashed var(--yellow);
  border-radius: var(--radius);
  margin-bottom: 2px;
  background: rgba(47, 111, 237, 0.08);
}

/* Map Creator's desktop left-nav for a selected layer (renderLayerNav
   in mapBuilder.js) — replaces the flat #mb-layer-list with a small
   header identifying the layer plus a vertical Layers/Symbology/
   Labels/Popups/Filters/Preview menu, matching the reference design. */
.map-builder-nav-header {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 4px 2px 14px;
  border-bottom: 1px solid var(--steel);
  margin-bottom: 8px;
}
.map-builder-nav-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--yellow);
  flex: 0 0 auto;
}
.map-builder-nav-header-text { min-width: 0; }
.map-builder-nav-name {
  font-weight: 600;
  font-size: 14px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.map-builder-nav-type {
  font-size: 12px;
  color: var(--paper-dim);
}
.map-builder-nav-menu {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.map-builder-nav-item {
  display: flex;
  align-items: center;
  gap: 10px;
  border: none;
  background: none;
  padding: 9px 8px;
  border-radius: var(--radius);
  font-size: 13.5px;
  font-weight: 500;
  color: var(--paper);
  cursor: pointer;
  text-align: left;
  width: 100%;
}
.map-builder-nav-item:hover { background: var(--panel-raised); }
.map-builder-nav-item.active {
  background: rgba(47, 111, 237, 0.12);
  color: var(--yellow);
}
.map-builder-nav-item-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  flex: 0 0 auto;
}
.map-builder-nav-item-aa {
  font-size: 13px;
  font-weight: 700;
}
.map-builder-unavailable-banner {
  padding: 8px 12px;
  font-size: 13px;
  background: rgba(244, 194, 43, 0.12);
  border-bottom: 1px solid var(--steel);
}

.map-builder-thumb-preview {
  width: 100%;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  border: 1px solid var(--steel);
  background: var(--panel-raised);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.map-builder-thumb-preview img { width: 100%; height: 100%; object-fit: cover; }

/* Item 2 (revised) — Configure Layer's desktop panel is now INLINE,
   sitting to the right of the map as a normal sibling inside
   .map-builder-body's flex row (mapBuilder.js's own `#mb-config-panel`
   slot, rendered empty in render()'s template and populated/emptied by
   openConfigPanel()) — matching the reference screenshot exactly: the
   map stays fully visible/interactive alongside it, never dimmed under
   a modal backdrop, and shrinks to share width the same way
   .map-builder-panel on the left already does. Zero width/no border
   while empty (`.has-content` is what actually reserves space) so an
   idle Map Creator screen looks identical to before this ever existed.
   Mobile keeps the existing bottom-sheet .task-modal treatment
   entirely unchanged (see openLayerConfigModal's own isDesktopViewport()
   branch) — this slot is simply never populated there. */
.map-builder-config-panel {
  flex: 0 0 auto;
  width: 0;
  overflow: hidden;
  border-left: 0 solid var(--steel);
  background: var(--panel);
  display: flex;
  flex-direction: column;
  min-height: 0;
}
.map-builder-config-panel.has-content {
  width: 380px;
  border-left-width: 1px;
  overflow: visible;
}
@media (max-width: 720px) {
  .map-builder-config-panel { display: none; } /* never used on mobile — see this rule's own header comment */
}
.map-builder-config-panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 16px 16px 4px;
  flex: 0 0 auto;
}
.map-builder-config-panel-header h2 { margin: 0; font-size: 18px; }
.map-builder-config-panel-subtitle { padding: 0 16px 12px; margin: 0; }
.map-builder-config-panel-scroll {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  padding: 0 16px 16px;
}
/* Cancel/Apply pinned at the bottom regardless of scroll position —
   item 2's own "Keep the Cancel and Apply buttons sticky at the bottom
   of the panel." Plain flex child after the scroll region (not
   position:sticky) since the panel itself is already a fixed-height
   flex column — simpler than sticky positioning and has the same
   visual result. */
.map-builder-config-panel-footer {
  display: flex;
  gap: 10px;
  padding: 12px 16px;
  border-top: 1px solid var(--steel);
  flex: 0 0 auto;
  background: var(--panel);
}

/* Mobile Configure Layer sheet's sticky tab bar (§ direct request:
   "instead of dropdowns have sticky tabs at the top for Symbology,
   Scale Range, Labels, Filters, Popups" — openLayerConfigModal's mobile
   branch in mapBuilder.js). `position: sticky` pins the bar to the top
   of .task-modal's own scroll container (that element is the actual
   `overflow-y: auto` box — see tasks.css's own .task-modal rule — so
   sticky here resolves against it, not the viewport) as the sheet's
   content scrolls underneath. Negative side margins + matching padding
   cancel out .task-modal's own side padding so the bar's background
   spans the full sheet width instead of leaving the parent's padding
   visible as a gutter on either side.

   The top margin/padding do the SAME cancellation, vertically: without
   it, .task-modal's own `padding-top: 16px` (tasks.css) left a gap
   above the stuck bar once scrolled — the bar's own background didn't
   extend up into that padding inset, so scrolled panel content was
   visible peeking through above the tabs instead of being fully
   covered. `margin-top: -16px` pulls the bar up to flush with the
   sheet's true top edge; `padding-top: 22px` (16px reclaimed + the
   original 6px) keeps the tab buttons themselves sitting at the same
   visual inset as before — only the bar's own background/hit-area now
   reaches all the way up, not the buttons' position within it. */
.mb-config-tabs {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  gap: 6px;
  overflow-x: auto;
  background: var(--task-bg, #fff);
  margin: -16px -14px 12px;
  padding: 22px 14px 10px;
  border-bottom: 1px solid var(--task-border);
  scrollbar-width: none;
}
.mb-config-tabs::-webkit-scrollbar { display: none; }
.mb-config-tab {
  flex: 0 0 auto;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid var(--task-border);
  background: var(--task-surface);
  color: var(--task-text);
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}
.mb-config-tab.active {
  background: var(--task-accent);
  color: var(--task-accent-text);
  border-color: var(--task-accent);
}
.mb-tab-panel[hidden] { display: none; }

/* Mobile Configure Layer sheet's Cancel/Apply footer (§ direct
   request: "Make the Apply and Cancel button sticky at the bottom of
   the Labels window" — same request that moved Labels' own save onto
   this button in openLayerConfigModal's mobile branch, mapBuilder.js).
   `position: sticky; bottom: 0` mirrors .mb-config-tabs' own top-sticky
   treatment above, symmetrically: .task-modal's `padding-bottom: 24px`
   (tasks.css) is canceled out the same way that rule cancels its
   `padding-top`, so the footer's background extends all the way down
   to the sheet's true bottom edge instead of leaving a gap there that
   scrolled content could show through. */
.mb-config-footer {
  position: sticky;
  /* bottom: -24px, not 0 — matches .task-modal's own padding-bottom
     (tasks.css: padding: 16px 14px 24px) exactly, and for the same
     reason .rb-props-mobile-actions (designer.css) already documents
     for its own identical sticky footer: unlike a sticky-TOP element
     (which gets actively pulled UP to close any gap as the container's
     leading padding scrolls away above it), a sticky-BOTTOM element
     that's the last thing in the scroll flow is never pushed any
     further than its own natural position — and that natural position,
     once scrolled all the way down, still has the container's OWN
     trailing padding sitting after it. bottom: 0 left exactly that
     24px gap below the buttons, with scrolled Labels-tab content
     visible through it. -24px cancels that trailing padding the same
     way this rule's own negative side/top margins already cancel the
     side/top padding for everything else here. */
  bottom: -24px;
  z-index: 2;
  display: flex;
  gap: 10px;
  background: var(--task-bg, #fff);
  margin: 12px -14px -24px;
  padding: 12px 14px 24px;
  border-top: 1px solid var(--task-border);
}

/* =============================================================================
   POPUP DESIGNER — full-screen overlay opened from Map Creator's
   "Configure Popup" action (popupDesigner.js). Distinct from the
   bottom-sheet .task-modal shell (taskCreateModal.js) — a two-pane
   Preview/Properties layout needs real width, so this gets its own
   backdrop + shell rather than reusing that one. Mobile stacks the two
   panes; desktop shows them side by side.

   Restyled to the light, blue-accented look the rest of the Task
   module already uses (tasks.css's own .task-theme tokens —
   --task-surface/--task-border/--task-accent/etc.) rather than this
   app's normal dark "field-tool" palette: the shell carries the
   .task-theme class itself (see popupDesigner.js's openDesignerOverlay)
   so every .task-btn/.task-field-label/input/select this file already
   used resolves properly here for the first time — outside .task-theme
   those custom properties were simply undefined. Editor-chrome rules
   below lean on --task-* vars throughout for that reason; only the
   handful of rules that style the REAL popup preview output
   (.feature-popup, .popup-section-block, .popup-related-*) intentionally
   stay independent of --task-*, since that output has to look right
   OUTSIDE this Designer too (the actual Leaflet popup on the map, which
   is never wrapped in .task-theme).
   ========================================================================= */
/* Popup Designer's own shell (backdrop/positioning/animation) is now
   slidePanel.js's shared openSlidePanel({from:'right'}) — see that
   module's own header comment. Only the INSIDE of the panel
   (list pane + settings pane, no more preview pane — see
   popupDesigner.js's own header comment on why the static preview was
   replaced by live-updating whatever real popup happens to be open on
   the map) still has its own classes below. */
.popup-designer-body-flex { flex: 1 1 auto; min-height: 0; display: flex; overflow: hidden; }
/* Block list pane and Block Settings pane — the two-panel split
   (spec: "Move block Properties to a panel to the right of the block
   list"). List pane is a fixed-ish width so the Settings pane gets
   most of the remaining room to actually work in. Narrower than
   before (was 420px) now that the whole Designer is a ~780px-wide
   slide-out drawer rather than a near-full-viewport overlay. */
.popup-designer-list-pane { flex: 0 0 300px; min-width: 260px; overflow-y: auto; padding: 16px 18px; background: var(--task-surface); border-right: 1px solid var(--task-border); }
.popup-designer-settings-pane { flex: 1 1 auto; min-width: 220px; overflow-y: auto; background: var(--task-surface); }
.popup-designer-settings-empty { padding: 24px 18px; }
.popup-designer-settings-header {
  display: flex; align-items: center; gap: 8px; padding: 14px 18px;
  border-bottom: 1px solid var(--task-border); position: sticky; top: 0; background: var(--task-surface); z-index: 1;
}
.popup-designer-settings-title { flex: 1; font-weight: 700; font-size: 15px; color: var(--task-navy); }
.popup-designer-settings-body { padding: 16px 18px; display: flex; flex-direction: column; gap: 12px; }
/* Property name labels (Style, Background, Text Style, Value Style,
   Condition, etc.) a little darker/more prominent (§ direct request 7)
   — scoped to just the Designer's own list/settings panes rather than
   touching tasks.css's shared --task-text-dim var or its base
   .task-field-label rule directly, since those are used across the
   whole Task module (Create Task modal, etc.), not just here. Mixed
   partway toward --task-text (this theme's own full-contrast text
   color) instead of a hardcoded hex specifically so this stays correct
   in BOTH light and dark mode automatically — "darker" literally would
   mean the wrong direction in dark mode (where --task-text is a LIGHT
   color for contrast against a dark background); moving toward
   --task-text in either mode is the theme-correct way to express "a
   little more prominent, less faint" regardless of which mode is
   active. .task-theme prefix for specificity, same reasoning as every
   other override in this file this session — tasks.css/designer.css's
   own label rules include a `label`/`summary` element qualifier that a
   bare 2-class selector here wouldn't outrank. */
.task-theme .popup-designer-list-pane .task-field-label,
.task-theme .popup-designer-settings-body .task-field-label {
  color: #4a5570; /* fallback for browsers without color-mix() */
  color: color-mix(in srgb, var(--task-text-dim) 55%, var(--task-text) 45%);
}
/* Thin divider between Label Style and Value Style (§ direct request
   5) — reuses .popup-block-divider's shape but recolored for this
   panel's own theme instead of that class's light-popup-output
   #e4e4e4, which would look too faint against .task-theme's surfaces. */
.popup-designer-style-divider { border: none; border-top: 1px solid var(--task-border, #aab3c8); margin: 4px 0; }
.popup-designer-settings-body input[type="text"],
.popup-designer-settings-body textarea { width: 100%; }
/* Selects and buttons fit their own content instead of stretching
   across the panel (§ direct request 3) — overrides tasks.css's own
   base ".task-theme select { width:100% }" rule, which is the right
   default for a narrow bottom-sheet modal (taskCreateModal.js) but
   reads as oddly bloated in this wider two-pane settings layout. Only
   <select> is touched here — text inputs/textareas stay full-width,
   which is still the right call for something like a Heading or Title
   field. Primary action buttons (.task-btn-block, e.g. "+ Add Content
   Block", the modal footer's own buttons) are UNCHANGED — those are
   deliberately full/fixed-width CTAs, not an accidental stretch.

   Prefixed with .task-theme (redundant ancestry-wise — .popup-designer-
   settings-body is always inside .task-theme already — but NOT
   redundant for CSS SPECIFICITY: tasks.css's own rule this overrides
   is ".task-theme select" (1 class + 1 type = same specificity tier as
   a 1-class selector alone would need to beat, and HIGHER than a bare
   1-class selector), and index.html loads designer.css/tasks.css AFTER
   app.css, so a same-or-lower-specificity rule here would lose the tie
   regardless of which one "looks" more specific to the eye. Every fix
   in this section needs the same `.task-theme` prefix trick for the
   same reason — see this session's own before/after screenshots that
   caught the first attempt not actually taking effect. */
.task-theme .popup-designer-settings-body select,
.task-theme .popup-designer-list-pane select { width: auto; min-width: 0; }

.popup-designer-section { margin-bottom: 20px; }
.popup-designer-section input[type="text"] { width: 100%; }

/* Style — moved to the top of the list pane, collapsible, collapsed
   by default (spec item 3). A native <details>/<summary> rather than
   a fake JS-driven accordion, matching this app's stated preference
   for real interactive semantics over faked ones (see app.css's own
   .theme-toggle comment on the same principle applied to switches). */
/* Style — a button, not a dropdown/accordion (§ direct request), that
   opens its settings in the right-hand panel exactly like any other
   selectable row (see popupDesigner.js's #pd-style-btn click handler
   setting selectedCtx.special='style'). Sized and styled like a block
   row so it reads as part of the same list, not a separate control. */
.popup-designer-style-btn {
  display: flex; align-items: center; gap: 8px; width: 100%; margin-bottom: 20px;
  padding: 9px 10px; border: 1px solid var(--task-border); border-radius: var(--task-radius-sm);
  background: var(--task-surface); cursor: pointer; text-align: left;
}
.popup-designer-style-btn.is-selected { border-color: var(--task-accent); background: var(--task-accent-dim); }
.popup-designer-style-btn.is-selected .popup-designer-block-chevron { color: var(--task-accent); }

.popup-designer-enable-row { display: flex; align-items: center; justify-content: space-between; }

/* Real <button role="switch">, matching app.css's own .theme-toggle
   pattern (see that file's comment on why a genuine switch element is
   preferred over a checkbox styled to look like one) — just built on
   --task-* tokens instead of the dark theme's --steel/--yellow. */
.pd-toggle { display: inline-flex; align-items: center; background: none; border: none; padding: 2px; cursor: pointer; }
.pd-toggle-track {
  position: relative; display: inline-block; width: 40px; height: 22px; border-radius: 999px;
  background: var(--task-border); transition: background 0.15s ease; flex-shrink: 0;
}
.pd-toggle-thumb {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.25); transition: transform 0.15s ease;
}
.pd-toggle[aria-checked="true"] .pd-toggle-track { background: var(--task-accent); }
.pd-toggle[aria-checked="true"] .pd-toggle-thumb { transform: translateX(18px); }

.popup-designer-layout-options { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.popup-designer-radio-pill {
  display: flex; align-items: center; gap: 6px; padding: 6px 12px;
  border: 1px solid var(--task-border); border-radius: 999px; font-size: 13px; cursor: pointer;
  background: var(--task-surface); color: var(--task-text);
}
.popup-designer-radio-pill:has(input:checked) { border-color: var(--task-accent); background: var(--task-accent-dim); color: var(--task-accent); }
/* Style presets that read as pill BUTTONS rather than a radio group
   (Section block's Card/Plain/Collapsible — matches the mockup) reuse
   this same look via a plain <button>, toggled active with a class
   since there's no underlying <input> to use :has() against. */
.popup-designer-pill-btn {
  padding: 7px 14px; border: 1px solid var(--task-border); border-radius: 999px;
  background: var(--task-surface); color: var(--task-text); font-size: 13px; font-weight: 600; cursor: pointer;
  transition: border-color 0.12s ease, background-color 0.12s ease, color 0.12s ease, transform 0.08s ease;
}
.popup-designer-pill-btn:active { transform: scale(0.96); }
.popup-designer-pill-btn.is-active { border-color: var(--task-accent); background: var(--task-accent-dim); color: var(--task-accent); }
.popup-designer-pill-row { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 6px; }

/* Text/Field style controls (§ direct request 2) — Bold/Italic/
   Underline, Font Size, Text Color, and Highlight Color all sit in one
   inline row instead of stacked separate fields, matching how a
   compact text-formatting toolbar normally reads. Overrides
   designer.css's own .rb-style-toggle (flex:1, meant for a 3-button-
   only row that fills its own container) to fit-content here instead,
   consistent with request 3's "fit to content" rule for every control
   in this row. */
.popup-designer-style-row { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
/* .task-theme prefix — same specificity-tie reasoning as the select
   fix above: designer.css's ".task-theme .rb-style-toggle { flex:1 }"
   is 2 classes, exactly tied with a plain 2-class override, and
   designer.css loads after this file, so a tie loses. 3 classes here
   beats it outright regardless of load order. */
.task-theme .popup-designer-style-row .rb-style-toggle { flex: 0 0 auto; width: 34px; min-height: 34px; }
.popup-designer-style-sep { width: 1px; align-self: stretch; min-height: 20px; background: var(--task-border, #aab3c8); margin: 0 2px; }
.popup-designer-style-label { font-size: 12px; color: var(--task-text-faint, #97a0b8); white-space: nowrap; }
/* Same fix: tasks.css's base ".task-theme input { width:100% }" is 1
   class + 1 type, which already beats a bare 1-class selector on
   specificity alone (before load order even matters). 2 classes here
   beats it outright. Shared by Font Size (Text/Field style controls)
   and Divider's own Thickness field — both small numeric fields that
   read better narrow, same reasoning either place. */
.task-theme .popup-designer-number-input-sm { width: 62px; flex: 0 0 auto; min-height: 34px; padding: 6px 8px; text-align: center; }

/* Range slider (Popup Width, § direct request 5) — the base
   ".task-theme input,select,textarea" rule (tasks.css) is built for
   boxy bordered text fields, wrong for a native slider track/thumb, so
   this strips that back down to a bare track and redraws both with
   the theme's own accent color. Unlike the "fit to content" rule for
   buttons/dropdowns (§3), a slider stays full-width on purpose — that
   IS its natural full-content size, the same way a text input's is. */
.task-theme input[type="range"] {
  width: 100%; min-height: auto; padding: 0; border: none; background: transparent;
  -webkit-appearance: none; appearance: none;
}
.task-theme input[type="range"]::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: var(--task-border, #aab3c8); }
.task-theme input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%;
  background: var(--task-accent, #2f6fed); margin-top: -7px; cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
.task-theme input[type="range"]::-moz-range-track { height: 4px; border-radius: 2px; background: var(--task-border, #aab3c8); }
.task-theme input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; background: var(--task-accent, #2f6fed);
  cursor: pointer; border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}

.popup-designer-placeholder { font-size: 13px; color: var(--task-text-faint); }

/* ---- Content Blocks tree: a list of rows — drag handle,
   "{Type} – {summary}" label, a hide icon, and a chevron — where a
   'section' row's own nested blocks render right underneath it,
   indented, ALWAYS visible (not gated on selection), so the whole
   popup's structure is visible at a glance. Clicking a row (any level)
   selects it — see popupDesigner.js's selectBlock — which shows its
   settings in the separate pane to the right, NOT inline here anymore
   (spec: "Move block Properties to a panel to the right of the block
   list"). .popup-designer-nested-list is a visually-indented but
   functionally SEPARATE drag scope from its parent's own list — see
   mountBlockList's own comment on why that separation matters for
   drag-and-drop. ---- */
.popup-designer-block-list { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.popup-designer-block-row {
  border: 1px solid var(--task-border, #aab3c8); border-radius: var(--task-radius-sm, 9px); background: var(--task-surface, #fff);
  /* NOT overflow:hidden — a Section row's own "+ Add Content" menu is
     position:absolute and needs to render OUTSIDE this row's box when
     open; overflow:hidden here was silently clipping it away entirely
     (menu items existed and were wired correctly, just invisible) —
     the exact reported "can't add blocks to Sections" bug. Corners
     stay visually clean without it since .popup-designer-block-row-head
     below carries its own matching top corner radius instead of
     relying on a clipping ancestor. */
}
/* The ACTUAL root cause of the repeatedly-reported drag "highlight" —
   found by direct comparison to Map Creator's own .map-builder-layer-row
   (app.css), which explicitly sets exactly this and even has a comment
   describing this precise scenario: dragging a handle drags the
   pointer across neighboring rows' own text, and without user-select:
   none, the browser's native TEXT SELECTION highlight (the blue
   selection-color sweep you get from any click-and-drag over text)
   kicks in over whatever text it passes across — not a CSS :hover
   effect, not a stylesheet bug, a completely different browser
   behavior this file's previous drag-highlight attempts never
   addressed because they were all chasing hover/reparenting theories
   instead. Applied to the whole list (not just the row being dragged)
   since it's every OTHER row's text that was getting swept-selected,
   matching Map Creator's own scope. */
.popup-designer-block-list {
  -webkit-touch-callout: none;
  -webkit-user-select: none;
  user-select: none;
}
/* No drag highlight (§ direct request "when dragging blocks don't
   highlight anything") — dragReorder.js's shared .list-drag-placeholder
   (app.css, used by Map Creator's layer list too) normally draws a
   bright yellow-dashed/blue-tinted drop indicator; neutralized here to
   a plain blank spacer within the Popup Designer specifically, without
   touching that class's appearance anywhere else it's used. The
   dragged row itself keeps no visual change either — no opacity dip
   via CSS class, no shadow.

   Block rows (top-level and nested-in-a-Section) drag via
   popupDesigner.js's own attachTreeDragHandlers now, NOT
   dragReorder.js's attachListDragHandlers — see that function's own
   header comment for why: it deliberately does NOT reparent the
   dragged row to document.body (a plain inline `opacity` is used
   in-place for the "lifted" cue instead of a `.dragging` class), so
   the row never leaves .task-theme's subtree and its --task-* colors
   never have a reason to go missing mid-drag. Field List's own
   internal field-reorder rows (.popup-designer-fieldlist-order-row)
   still use the original attachListDragHandlers (no cross-container
   need there), which DOES reparent to document.body — the var()
   fallbacks below on rules visible during THAT drag are still load-
   bearing for that one case; kept as defense-in-depth on the block-row
   rules too even though they're no longer strictly required there. */
.popup-designer-shell .list-drag-placeholder { background: transparent; border: none; }
.popup-designer-block-row.dragging { box-shadow: none; }
/* Belt-and-suspenders on top of all of the above: while a block drag
   is active (popupDesigner.js's attachTreeDragHandlers toggles this
   class on the shell for exactly the drag's duration), every OTHER
   row's own :hover-reactive parts (background/color changes on
   .popup-designer-block-row-head, its icon buttons) are disabled
   outright via pointer-events:none — not because any ONE specific
   hover rule was confirmed to be the culprit, but because the dragged
   ghost has pointer-events:none itself (needed so the browser can
   still compute :hover on whatever's underneath it as it moves), which
   means ANY hover-reactive styling on a row it passes over WILL fire
   as the pointer crosses it, by design of how CSS :hover works — this
   removes that entire class of "something reacted as I dragged over
   it" possibility at once, regardless of which specific rule would
   otherwise have caused it. */
.popup-designer-shell.is-dragging-blocks .popup-designer-block-row-head,
.popup-designer-shell.is-dragging-blocks .popup-designer-icon-btn,
.popup-designer-shell.is-dragging-blocks .popup-designer-menu-item {
  pointer-events: none;
}
.popup-designer-fieldlist-order-row.dragging { box-shadow: none; }
.popup-designer-block-row-head { display: flex; align-items: center; gap: 8px; padding: 9px 10px; cursor: pointer; }
/* Whichever element ends up first/last inside a row gets that end's
   corners rounded to match the row's own border-radius — NOT hardcoded
   to .popup-designer-block-row-head specifically, because how many
   children a row has varies: most block types have only the row-head
   (so it's simultaneously first AND last — both rules below apply to
   it, on DIFFERENT longhand properties, so they compose rather than
   one clobbering the other), while a Section row has the row-head
   FOLLOWED by its nested-list/add-content wrapper, whose OWN last
   element needs the bottom rounding instead. This is the actual fix
   for the reported "Text block border cut off at the bottom corners"
   bug: removing the row's old overflow:hidden (this session's Section
   fix) meant a child's own square background could poke out past the
   row's rounded bottom corners wherever that child wasn't ALSO
   explicitly rounded to match — this covers every row shape at once
   instead of special-casing each one. */
.popup-designer-block-row > *:first-child { border-top-left-radius: var(--task-radius-sm, 9px); border-top-right-radius: var(--task-radius-sm, 9px); }
.popup-designer-block-row > *:last-child { border-bottom-left-radius: var(--task-radius-sm, 9px); border-bottom-right-radius: var(--task-radius-sm, 9px); }
.popup-designer-block-row.is-selected { border-color: var(--task-accent, #2f6fed); }
.popup-designer-block-row.is-selected > .popup-designer-block-row-head { background: var(--task-accent-dim, #eaf1ff); }
.popup-designer-block-drag { cursor: grab; color: var(--task-text-faint, #97a0b8); touch-action: none; font-size: 16px; line-height: 1; flex-shrink: 0; }
.popup-designer-block-label { flex: 1; font-size: 13px; color: var(--task-text, #16213a); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.popup-designer-icon-btn {
  border: none; background: none; color: var(--task-text-faint, #97a0b8); cursor: pointer;
  width: 26px; height: 26px; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; font-size: 13px; flex-shrink: 0;
  transition: background-color 0.12s ease, color 0.12s ease, transform 0.08s ease;
}
.popup-designer-icon-btn:active { transform: scale(0.9); }
.popup-designer-icon-btn svg { display: block; }
/* A little bigger (§ direct request 1) — the Trash2 SVG's own default
   18x18 reads small once actually inside the standard 26px button box
   the Hide/Close icons also use; this variant is only for the Delete
   button, not those. */
.popup-designer-icon-btn-lg { width: 32px; height: 32px; }
.popup-designer-icon-btn-lg svg { width: 20px; height: 20px; }
.popup-designer-icon-btn:hover { background: var(--task-gray-bg); color: var(--task-text); }
/* Block Delete — a red trash can (§ direct request 4), distinct from
   the other icon buttons (Hide, Close) which stay neutral-colored. */
.popup-designer-icon-btn-danger { color: var(--task-red, #e0483a); }
.popup-designer-icon-btn-danger:hover {
  background: #fbe4e1; /* fallback for browsers without color-mix() */
  background: color-mix(in srgb, var(--task-red, #e0483a) 12%, transparent);
  color: var(--task-red, #e0483a);
}
.popup-designer-block-chevron {
  color: var(--task-text-faint, #97a0b8); font-size: 18px; line-height: 1; flex-shrink: 0;
}
.popup-designer-block-row.is-selected .popup-designer-block-chevron { color: var(--task-accent); }
.popup-designer-block-hidden > .popup-designer-block-row-head { opacity: 0.5; }
.popup-designer-nested-add-wrap { padding: 0 10px 10px; position: relative; }

.popup-designer-add-content-wrap { margin-top: 10px; position: relative; }

/* ---- Custom menu (Add Content Block / Insert Field) — a button that
   opens a small absolutely-positioned list of buttons, replacing what
   were previously plain <select> pickers used only as "choose one,
   then act" menus rather than persistent value controls (see
   popupDesigner.js's wireMenuButton). ---- */
.popup-designer-menu-wrap { position: relative; }
.popup-designer-menu {
  position: absolute; left: 0; right: 0; z-index: 5;
  background: var(--task-surface); border: 1px solid var(--task-border); border-radius: var(--task-radius-sm);
  box-shadow: var(--task-shadow); max-height: 260px; overflow-y: auto; padding: 4px;
  /* Open/close animation (§ direct request — "dropdowns open/close")
     — starts scaled-down/faded/non-interactive by default; .is-open
     (added a frame after `hidden` is removed — see wireMenuButton's
     own comment on why that timing matters) transitions it to its
     resting state. transform-origin sits at the top since these
     menus open DOWNWARD by default — the one exception
     (.popup-designer-add-content-wrap, opening upward near the
     bottom of the panel) overrides it to the bottom below. */
  transform-origin: top center;
  opacity: 0; transform: scale(0.96) translateY(-4px);
  transition: opacity 0.12s ease, transform 0.12s ease;
  pointer-events: none;
}
.popup-designer-menu.is-open { opacity: 1; transform: scale(1) translateY(0); pointer-events: auto; }
.popup-designer-add-content-wrap .popup-designer-menu { bottom: calc(100% + 4px); top: auto; transform-origin: bottom center; }
.popup-designer-menu-item {
  display: block; width: 100%; text-align: left; padding: 8px 10px; border: none; background: none;
  border-radius: 6px; font-size: 13px; color: var(--task-text); cursor: pointer;
  transition: background-color 0.1s ease, color 0.1s ease;
}
.popup-designer-menu-item:hover { background: var(--task-accent-dim); color: var(--task-accent); }

/* ---- Field List block: a single draggable list, checkbox at the
   right of each field name (see fieldListRowHtml/wireFieldListEditor)
   — no separate "pick fields" grid anymore. ---- */
.popup-designer-fieldlist-order { display: flex; flex-direction: column; gap: 4px; }
.popup-designer-fieldlist-order-row {
  display: flex; align-items: center; gap: 8px; padding: 7px 8px;
  background: var(--task-bg, #f4f6fb); border: 1px solid var(--task-border, #aab3c8); border-radius: 6px; font-size: 13px;
}

.popup-designer-fieldlist-order-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Custom label input (§ direct request 1) — a "ghost" input (no
   visible border/background until hover/focus) so it reads as inline-
   editable text rather than a boxy form field crammed into an already
   tight row; the placeholder (the field's own default label) makes it
   obvious what shows if left untouched. .task-theme prefix for
   specificity — tasks.css's base ".task-theme input" rule (1 class + 1
   type) exactly TIES a same-shape override without it, and tasks.css
   loads after this file, so the tie would go the wrong way — same
   lesson as every other override in this section this session. */
.task-theme input.popup-designer-fieldlist-order-name {
  min-height: 26px; padding: 4px 6px; font-size: 13px;
  border: 1px solid transparent; background: transparent; color: var(--task-text, #16213a);
}
.task-theme input.popup-designer-fieldlist-order-name:hover,
.task-theme input.popup-designer-fieldlist-order-name:focus {
  border-color: var(--task-border, #aab3c8); background: var(--task-surface, #fff);
}
.pd-fl-drag { cursor: grab; color: var(--task-text-faint, #97a0b8); touch-action: none; }

.popup-designer-footer {
  display: flex; align-items: center; gap: 10px; padding: 12px 18px;
  border-top: 1px solid var(--task-border); flex: 0 0 auto; background: var(--task-surface);
}

@media (max-width: 860px) {
  .popup-designer-body-flex { flex-direction: column; overflow-y: auto; }
  .popup-designer-list-pane { flex: 0 0 auto; border-right: none; border-bottom: 1px solid var(--task-border); overflow-y: visible; }
  .popup-designer-settings-pane { flex: 1 1 auto; overflow-y: visible; }
}

/* ---- Popup Designer Phase 2: Sections/Cards, Related Records, and
   the Conditional Visibility editor's own condition rows. Real popup
   output (.popup-section-block, .popup-related-*) shares the same
   light-popup-bubble palette as every other .feature-popup-* class
   elsewhere (NOT --task-*, since that output renders outside this
   Designer too); editor-only chrome (.popup-designer-*) uses --task-*
   like the rest of this section. ---- */
.popup-section-block { margin: 10px 0; padding: 8px 10px; border-radius: 8px; }
.popup-section-bg-tint { background: #f7f8fa; }
.popup-section-bg-card { background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
.popup-section-bordered { border: 1px solid #e8e9ec; }
.popup-section-block-heading { font-weight: 700; font-size: 12px; color: #444; margin-bottom: 6px; cursor: default; list-style: none; }
.popup-section-block-heading::-webkit-details-marker { display: none; }
details.popup-section-block > .popup-section-block-heading { cursor: pointer; }
details.popup-section-block > .popup-section-block-heading::before { content: '\25B8'; display: inline-block; margin-right: 4px; transition: transform 0.15s; }
details.popup-section-block[open] > .popup-section-block-heading::before { transform: rotate(90deg); }
.popup-section-block-body { font-size: 12.5px; }

.popup-related-list-row { padding: 4px 0; font-size: 12.5px; border-bottom: 1px solid #f0f0f0; }
.popup-related-list-row:last-child { border-bottom: none; }
.popup-related-cards { display: flex; flex-direction: column; gap: 8px; }
.popup-related-card { background: #f7f8fa; border: 1px solid #e8e9ec; border-radius: 6px; padding: 6px 8px; }
.popup-related-card-title { font-weight: 700; font-size: 12px; margin-bottom: 4px; }
.popup-related-card-row { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; }
.popup-related-card-row span:first-child { color: #777; }

.popup-designer-nested-list { margin: 6px 10px 0; padding-left: 14px; border-left: 2px solid var(--task-border); }
.popup-designer-visibility { margin-top: 4px; }
.popup-designer-visibility .popup-designer-menu-item,
.popup-designer-condition-row { margin-bottom: 6px; }
.popup-designer-condition-row { display: flex; align-items: center; gap: 6px; }
.popup-designer-condition-row select,
.popup-designer-condition-row input { min-width: 0; flex: 1; }
.popup-designer-fieldlist-order .task-flex-row select { flex: 0 0 auto; }

/* Related Records' own "which fields to display from the target
   layer" checkbox grid — a distinct picker from the Field List block's
   single draggable list above (this one has no order/drag concept,
   just inclusion, and a different underlying data source), styled the
   same light chrome as everything else in this file. */
.popup-designer-fieldlist-picker {
  display: grid; grid-template-columns: 1fr 1fr; gap: 4px 12px;
  max-height: 160px; overflow-y: auto; padding: 8px; margin-top: 6px;
  border: 1px solid var(--task-border); border-radius: var(--task-radius-sm); background: var(--task-bg);
}

/* ============================================================================
   REDUCED MOTION — one comprehensive, standard override (MDN's own
   documented pattern for this) covering every animation/transition
   added across this file/tasks.css/designer.css this session (button
   press feedback, modal/toast/dropdown/screen entrance animations, the
   map popup fade-in, card hover lift, etc.) plus anything that already
   existed before it (.btn's own press-flash, drag-and-drop, chevron
   rotations, and so on) — rather than hunting down and re-disabling
   each new selector individually here, which would only get more
   incomplete as animations get added in the future. Someone who's told
   their OS they don't want motion gets NONE, app-wide, from one rule.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
