/* Ridal web GUI styles (#115, #121).
 *
 * First-party, embedded in the binary via assets.rs. Deliberately NOT under
 * assets/vendor/ -- scripts/vendor_leaflet.sh does `rm -rf` on that
 * directory, so anything first-party placed there would be silently
 * deleted on the next Leaflet refresh.
 *
 * Every colour is a token with both a light and a dark value. Nothing
 * below hardcodes a colour, so `prefers-color-scheme: dark` is a real
 * theme rather than a declaration that leaves light-only values in place.
 */

:root {
  color-scheme: light dark;

  --color-bg: #fcfcfb;
  --color-surface: #ffffff;
  --color-surface-sunken: #eceae6;
  --color-text: #1b1b1a;
  --color-text-muted: #5f5e5a;
  --color-border: #d9d7d2;
  --color-border-strong: #b6b3ac;
  --color-accent: #1f6f8b;
  --color-accent-hover: #14556c;
  --color-warning-bg: #fdf0d5;
  --color-warning-border: #e0b23c;
  --color-warning-text: #5a4310;
  /* Distinct from warning, and deliberately so: warning means "look at
   * this", danger means "this did not happen". The carry banner uses the
   * two for exactly that difference. */
  --color-danger-bg: #fbe4e1;
  --color-danger-border: #c2452f;
  --color-danger-text: #6d1f14;

  /* Map-popup background. PFA_website's style.css:632-637 uses
   * rgba(255,255,255,0.3) + backdrop-filter blur, ported faithfully in
   * an earlier round -- but 30% alpha cannot guarantee legibility
   * against arbitrary satellite imagery underneath (PFA_website has the
   * same flaw for the same reason), and blur fixes texture, not
   * contrast. Near-opaque instead; the small blur below is now purely a
   * soft-edge touch, not what legibility depends on. */
  --color-popup-bg: rgb(255 255 255 / 92%);

  /* Theme-independent on purpose: this is the letterbox behind grayscale
   * radargram chunks. A light backdrop around a grayscale section
   * destroys perceived contrast, so it stays dark in both themes. */
  --color-viewer-bg: #202022;

  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;

  --font-sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
  --text-sm: 0.85rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;
  --text-xl: 1.6rem;
  --line-base: 1.5;

  --radius-sm: 3px;
  --radius-md: 6px;
  --table-cell-pad: 2px var(--space-3) 2px 0;
  --map-height-group: 320px;
  /* `vh` is the fallback; the `@supports` block below prefers `svh`. See
     the note there -- this is what stops the viewer jumping on a phone. */
  --map-height-viewer: 70vh;
  --map-min-height: 400px;
  --card-min: 18rem;
  --thumb-aspect: 5 / 2;
}

.bulk-users {
  margin: 1.5rem 0;
  padding: 1rem;
  border: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
}

.bulk-users summary {
  cursor: pointer;
  font-weight: 600;
}

.bulk-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1rem;
}

.bulk-risk {
  margin-top: 1rem;
}

.bulk-result {
  margin-top: 1rem;
  padding: 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
}

.bulk-table {
  width: 100%;
}

@media print {
  body * {
    visibility: hidden;
  }

  #bulk-result,
  #bulk-result * {
    visibility: visible;
  }

  #bulk-result {
    position: absolute;
    inset: 0;
    border: 0;
  }

  #bulk-result .bulk-actions {
    display: none;
  }
}

/* A phone's address bar hides on the first interaction, which grows `vh`
   and therefore the map, which re-lays the radargram under the finger that
   caused it -- the "jump" on placing a first vertex. `svh` is resolved
   against the viewport *with* the bar visible and stays put when it hides,
   trading ~10% of map height for a view that does not move. `dvh` would be
   the worst choice here: it tracks the bar continuously. */
@supports (height: 1svh) {
  :root {
    --map-height-viewer: 70svh;
  }
}

/* The dark tokens, in two places on purpose (#141).
 *
 * A person can now override what their device asks for, and CSS has no way
 * to share one declaration block between a media query and an attribute
 * selector -- so the values are written twice and pinned equal by
 * `the_two_dark_theme_blocks_agree` in assets.rs, which fails if one is
 * edited without the other.
 *
 * The media block is guarded: someone who asked for light on a device set
 * to dark means it. `color-scheme` follows the choice as well, so form
 * controls, scrollbars and the browser's own surfaces match the page
 * instead of staying whatever the device said.
 */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #16171a;
    --color-surface: #1e2024;
    --color-surface-sunken: #26282d;
    --color-text: #e6e5e1;
    --color-text-muted: #a3a19b;
    --color-border: #34373d;
    --color-border-strong: #4b4f57;
    --color-accent: #6fb7cf;
    --color-accent-hover: #9ad2e6;
    --color-warning-bg: #3a2f14;
    --color-warning-border: #8a6a20;
    --color-warning-text: #f2dfae;
    --color-danger-bg: #3d1a14;
    --color-danger-border: #b04a34;
    --color-danger-text: #f5cfc7;
    --color-popup-bg: rgb(30 32 36 / 92%);
  }
}

:root[data-theme="dark"] {
  --color-bg: #16171a;
  --color-surface: #1e2024;
  --color-surface-sunken: #26282d;
  --color-text: #e6e5e1;
  --color-text-muted: #a3a19b;
  --color-border: #34373d;
  --color-border-strong: #4b4f57;
  --color-accent: #6fb7cf;
  --color-accent-hover: #9ad2e6;
  --color-warning-bg: #3a2f14;
  --color-warning-border: #8a6a20;
  --color-warning-text: #f2dfae;
  --color-danger-bg: #3d1a14;
  --color-danger-border: #b04a34;
  --color-danger-text: #f5cfc7;
  --color-popup-bg: rgb(30 32 36 / 92%);
  color-scheme: dark;
}

:root[data-theme="light"] {
  color-scheme: light;
}

/* ---------- base ---------- */

body {
  margin: 0;
  padding: 0;
  background: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  line-height: var(--line-base);
}

header {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--color-border);
  background: var(--color-surface);
}

header a {
  text-decoration: none;
  font-weight: 600;
  color: var(--color-accent);
}

header a:hover {
  color: var(--color-accent-hover);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.brand-logo {
  height: 1.5em;
  width: auto;
}

main {
  padding: var(--space-4);
}

h1 {
  font-size: var(--text-xl);
  margin: 0 0 var(--space-4);
}

h2 {
  font-size: var(--text-lg);
  margin: 0 0 var(--space-3);
}

a {
  color: var(--color-accent);
}

a:hover {
  color: var(--color-accent-hover);
}

code {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

.warning {
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-border);
  color: var(--color-warning-text);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  margin-bottom: var(--space-3);
}

/* A failed fetch used to leave a blank map, which reads as "no data
 * here" rather than "the request failed". RIDAL.reportError overlays
 * this on the element that would otherwise be mysteriously empty, so the
 * explanation is where the user is already looking. Above Leaflet's own
 * panes (z-index 400-700) and its controls (800), below its popups. */
.error-overlay {
  position: absolute;
  z-index: 900;
  top: var(--space-2);
  /* Clears Leaflet's top-left zoom control (30px wide plus its own 10px
   * margin), which both current hosts have and which otherwise sits on
   * top of the first few characters of every line. A non-map host just
   * gets a slightly indented box. */
  left: 52px;
  right: var(--space-2);
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-border);
  color: var(--color-warning-text);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
}

.error-overlay-dismiss {
  margin-left: auto;
  flex: none;
  cursor: pointer;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--text-base);
  line-height: 1;
  padding: 0 var(--space-1);
}

/* The in-flow sibling of .error-overlay, for reporting something that
 * has no empty container to sit over -- a download that did not happen.
 * Same colour language; laid out as a block that pushes the page down
 * rather than as an overlay that could cover the control just used. */
.page-problem {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  max-width: 60rem;
}

/* A download that succeeded with something worth reading attached, which
 * is not a failure and should not borrow the colour of one. */
.page-problem-note {
  background: var(--color-surface-sunken);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* Same colour language inside the metadata dialog, which is a table
 * rather than a positioned container, so it needs no overlay. */
.error-text {
  color: var(--color-warning-text);
  background: var(--color-warning-bg);
}

/* ---------- index: groups and cards ---------- */

section.group {
  margin-bottom: var(--space-6);
}

.group-map {
  width: 100%;
  max-width: 700px;
  height: var(--map-height-group);
  margin-bottom: var(--space-4);
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--card-min), 1fr));
  gap: var(--space-4);
  list-style: none;
  margin: 0;
  padding: 0;
}

.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  scroll-margin-top: var(--space-5);
}

/* Deliberately not `overflow: hidden`. It used to be, to keep the
 * thumbnail's square corners inside the card's rounded ones -- but a card
 * now holds a menu, and a menu that cannot overhang its container is a
 * menu with one item visible. The rounding moved to the thumbnail, which
 * is the only thing that ever needed it. */

/* Two distinct highlight states, previously the identical accent ring --
 * which made hovering a card's track from the group map look like it did
 * nothing whenever that card was also the :target (arriving via the
 * viewer's "back to catalog" link, /#card-{id}, leaves it targeted for
 * as long as the hash stays in the URL). Now they compose visibly
 * instead of one masking the other. */

/* Persistent: "this is where you came from". */
.card:target {
  border-color: var(--color-accent);
  background: var(--color-surface-sunken);
}

/* Transient: "this is what your cursor is over right now", on either
 * end of the two-way card/track hover link (RIDAL.bindTrackHighlight). */
.card.is-hovered {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-accent);
}

.card-link {
  display: block;
  text-decoration: none;
  color: inherit;
}

.card-thumb {
  display: grid;
  place-items: center;
  aspect-ratio: var(--thumb-aspect);
  background: var(--color-surface-sunken);
  border-bottom: 1px solid var(--color-border);
  overflow: hidden;
  /* Inside the card's own border, hence the 1px. */
  border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
}

/* `contain`, not `cover`. OverviewSpec scales by width only and never
 * upscales, so a thumbnail's aspect ratio is the radargram's own
 * (traces x samples) and varies enormously -- a long profile can be
 * 512x8. `cover` would crop nearly all of it away, and cropping a
 * scientific preview misrepresents the data. Letterboxing is honest.
 * The fixed aspect-ratio box also means lazily-loaded images cause no
 * layout shift as they arrive. */
.card-thumb img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

.card-thumb.is-missing img {
  display: none;
}

.card-thumb.is-missing::after {
  content: "no preview";
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* The name and the card's own menu on one line, directly under the
 * picture. Baseline-aligned so the smaller button sits on the name's line
 * rather than floating beside it. */
.card-heading {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-3) 0;
}

.card-title {
  font-weight: 600;
  color: var(--color-accent);
  text-decoration: none;
  /* The name is what should shrink: the menu is a fixed-width control and
   * a long name would otherwise push it off the row. */
  min-width: 0;
  overflow-wrap: anywhere;
}

.card-title:hover,
.card-link:hover + .card-heading .card-title {
  color: var(--color-accent-hover);
}

/* Its own width rather than a share of the row: the name is the part that
 * should shrink. The menu's own look is a `.site-menu` variant and is set
 * with the others, below -- an override here would sit earlier in the file
 * than `.site-menu > summary` and, at equal specificity, quietly lose. */
.card-menu {
  flex: none;
}

/* Marks an unlisted card inside the disclosure that already says what
 * unlisted means, so the badge can be a plain label rather than a warning:
 * nothing is wrong, somebody chose this. */
.card-badge {
  margin: 0;
  padding: 0 var(--space-3);
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-text-muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Unlisted members hang below their group's grid rather than inside it, so
 * the grid still reads as "what this group is" at a glance. */
.unlisted-group {
  margin-top: var(--space-4);
}

.unlisted-group > summary {
  cursor: pointer;
  color: var(--color-text-muted);
  font-weight: 600;
}

.card-meta {
  display: grid;
  grid-template-columns: max-content 1fr;
  gap: var(--space-1) var(--space-3);
  margin: 0;
  padding: var(--space-3);
  font-size: var(--text-sm);
}

.card-meta dt {
  color: var(--color-text-muted);
}

.card-meta dd {
  margin: 0;
}

.card-meta .path {
  overflow-wrap: anywhere;
}

/* The "More info" <details> reuses .meta-details' summary styling (see
 * the metadata-dialog section below) but needs its own horizontal
 * padding, since .card itself carries none -- .card-meta supplies its
 * own instead, which would otherwise double up here. */
.card-more {
  padding: 0 var(--space-3) var(--space-3);
}

.card-more summary {
  padding: var(--space-1) 0;
}

.card-more .card-meta {
  padding: 0;
}

/* ---------- viewer ---------- */

/* The radargram region and the overview map never wrap on their own: a
 * wrap here was what stranded the resizer (#199), because once wrapped the
 * handle hid itself and the stacked layout kept the measurement "not side
 * by side", so it could never come back -- including after a browser zoom
 * out. Stacking is instead decided by the fixed breakpoint in the narrow
 * screen media query, which re-evaluates on zoom and is recoverable. */
.layout {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-4);
  align-items: flex-start;
}

#map {
  flex: 2 1 500px;
  min-width: 200px;
  height: var(--map-height-viewer);
  min-height: var(--map-min-height);
  background: var(--color-viewer-bg);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* The topographically corrected view (#168) has a genuine wedge of no
 * data above the sheared surface, which otherwise shows as a hard-edged
 * rectangle of `--color-viewer-bg` against the tiles' own NaN padding --
 * a different, much lighter grey. `rgb(96, 96, 96)` matches
 * `renderer.rs`'s `PAD_VALUE` exactly, so the two are indistinguishable
 * and the wedge reads as part of the image rather than as missing tiles. */
#map.map-topo {
  background: rgb(96, 96, 96);
}

/* The drag clamp in viewer.js reserves this pane's minimum plus its 1px
 * border (which `box-sizing: content-box` keeps outside the flex-basis) and
 * both gutters, so dragging cannot push the overview past the point where
 * it stops shrinking (#199). */
#overview-map {
  flex: 1 1 80px;
  min-width: 80px;
  height: var(--map-height-viewer);
  min-height: var(--map-min-height);
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* The two drag handles: #split-resizer between the radargram region and the
 * overview map, #trace-resizer between the radargram and the trace panel.
 * Both are plain col-resize handles; the narrow-screen media query removes
 * #split-resizer when the layout stacks.
 *
 * `touch-action: none` is what makes them work on a touchscreen: without
 * it the browser claims the drag for scrolling and the pointer events the
 * handlers rely on stop arriving. The `::before` widens the hit area from
 * 8px to 24px for a finger, without changing the 8px the layout sees. */
#split-resizer,
#trace-resizer {
  position: relative;
  flex: 0 0 8px;
  align-self: stretch;
  cursor: col-resize;
  touch-action: none;
  background: var(--color-border);
  border-radius: var(--radius-sm);
}

#split-resizer::before,
#trace-resizer::before {
  content: "";
  position: absolute;
  inset: 0 -8px;
}

#split-resizer:hover,
#split-resizer:focus-visible,
#trace-resizer:hover,
#trace-resizer:focus-visible {
  background: var(--color-accent);
}

/* `visibility`, deliberately not `display: none`: the trace handle must
 * keep its 8px in the flex line so hiding it cannot change whether the
 * trace panel wraps below the radargram (the same feedback loop the old
 * split handle suffered from). */
#trace-resizer.is-hidden {
  visibility: hidden;
  pointer-events: none;
}

/* The radargram and the trace panel, one flex box (#181). On a wide screen
 * the two never wrap: the region has a minimum that includes both panes, so
 * dragging the split resizer can only shrink the trace to its minimum, not
 * push it onto a row of its own with the handle. Only the narrow-screen
 * media query stacks it. `min-width` is the sum of the two panes' outer
 * minimums (their content minimum plus the 1px border `box-sizing:
 * content-box` keeps outside the flex-basis), the trace handle, and both
 * gutters; `#split-resizer`'s clamp reads it back so its drag agrees with
 * what the layout will actually do. */
.radar-region {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-4);
  align-items: flex-start;
  flex: 2 1 500px;
  min-width: calc(200px + 2px);
}

.radar-region.has-trace {
  min-width: calc(200px + 2px + 8px + 160px + 2px + 2 * var(--space-4));
}

/* Inside `.radar-region`, `#map` no longer competes with `#overview-map`,
 * so its base size is the region's own share rather than the page's. */
.radar-region > #map {
  flex: 1 1 400px;
}

/* The panel's own `display: flex` is overridden by the global
 * `[hidden] { display: none !important }` below, so `hidden` really hides
 * it. `flex-basis` equals `min-width` for the same reason as
 * `#overview-map`: the trace resizer's clamp reserves the basis, and a
 * larger basis would wrap the trace below the radargram before the drag
 * reached its limit. */
#trace-view {
  flex: 1 1 160px;
  min-width: 160px;
  height: var(--map-height-viewer);
  min-height: var(--map-min-height);
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.trace-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-2);
  border-bottom: 1px solid var(--color-border);
  font-variant-numeric: tabular-nums;
}

.trace-zoom {
  display: flex;
  gap: 2px;
}

.trace-zoom button {
  line-height: 1;
  padding: 0 var(--space-2);
}

#trace-canvas {
  flex: 1 1 auto;
  display: block;
  width: 100%;
  min-height: 0;
  /* Dragging the trace pans the radargram vertically (the two are synced),
   * so it gets the same grab affordance as a map. `touch-action: none` is
   * what lets the drag reach the pointer handlers on a touchscreen instead
   * of the browser scrolling the page. */
  cursor: grab;
  touch-action: none;
}

#trace-canvas:active {
  cursor: grabbing;
}

.trace-hint {
  margin: 0;
  padding: var(--space-1) var(--space-2) var(--space-2);
  color: var(--color-text-muted);
  font-size: 0.85em;
}

/* ---------- Leaflet popup ---------- */

.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: var(--color-popup-bg);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  color: var(--color-text);
}

.leaflet-popup-content a {
  font-weight: 600;
}

.popup-thumb {
  display: block;
  margin-top: var(--space-1);
  max-width: 220px;
  max-height: 120px;
  width: 100%;
  object-fit: contain;
}

table.meta {
  border-collapse: collapse;
  margin-bottom: var(--space-3);
}

table.meta th,
table.meta td {
  text-align: left;
  padding: var(--table-cell-pad);
  vertical-align: top;
}

table.meta th {
  color: var(--color-text-muted);
  font-weight: 600;
}

/* `hidden` must actually hide.
 *
 * The UA stylesheet's `[hidden] { display: none }` loses to any author rule
 * that sets `display`, so an element carrying both `hidden` and a class
 * like `.controls` (which is `display: flex`) stays fully visible and
 * clickable. That is not hypothetical: the picker's "Selected line" panel
 * was permanently on screen for exactly this reason, which meant its Delete
 * button could be pressed with nothing selected.
 *
 * `!important` is warranted here -- `hidden` is not a hint. */
[hidden] {
  display: none !important;
}

.controls {
  display: flex;
  gap: var(--space-4);
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}

.controls button,
.controls select {
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}

.controls button {
  cursor: pointer;
}

.controls button:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

/* Its own row, always exactly one line tall, scrolled horizontally.
 *
 * `flex-basis: 100%` is what makes populating it a no-op for layout: on a
 * narrow screen the text is long enough to wrap onto a new row anyway, so
 * reserving that row permanently costs one line and removes the shift
 * entirely.
 *
 * The height is *fixed*, not a minimum, and that is the load-bearing part.
 * A horizontal scrollbar appearing inside a fixed-height box eats into the
 * content area rather than growing the box, so the row cannot change height
 * whether the reading overflows or not -- which is the whole reason this
 * element was pinned down in the first place.
 *
 * `touch-action: pan-x` keeps a sideways swipe here scrolling the reading
 * rather than the page. The scrollbar is thin rather than hidden: on a
 * desktop it is the only clue that there is more to read. */
#cursor-readout {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  flex-basis: 100%;
  height: 1.6em;
  white-space: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  touch-action: pan-x;
  scrollbar-width: thin;
}

#cursor-readout::-webkit-scrollbar {
  height: 3px;
}

#cursor-readout::-webkit-scrollbar-thumb {
  background: var(--color-border-strong);
  border-radius: var(--radius-sm);
}

/* ---------- metadata dialog ---------- */

dialog {
  max-width: 90vw;
  max-height: 80vh;
  overflow: auto;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  padding: var(--space-4);
}

dialog::backdrop {
  background: rgb(0 0 0 / 45%);
}

dialog h2 {
  margin-top: 0;
}

dialog table {
  border-collapse: collapse;
  font-size: var(--text-sm);
}

dialog th,
dialog td {
  text-align: left;
  padding: var(--table-cell-pad);
  border-bottom: 1px solid var(--color-border);
  vertical-align: top;
  overflow-wrap: anywhere;
}

dialog th {
  color: var(--color-text-muted);
  font-weight: 600;
  white-space: nowrap;
}

/* Shared by every collapsible section of the metadata dialog (currently
 * processing steps/log and original filepaths) so they stay visually
 * consistent without duplicating the rules per section. */
.meta-details {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
}

.meta-details summary {
  cursor: pointer;
  color: var(--color-text-muted);
  font-weight: 600;
}

.meta-details ol,
.meta-details ul {
  margin: var(--space-2) 0;
  padding-left: var(--space-4);
  overflow-wrap: anywhere;
}

#processing-log {
  font-family: var(--font-mono);
  /* The log is essentially raw text with tabs turned into indented
     newlines client-side (see viewer.html.jinja) -- pre-wrap keeps that
     structure instead of collapsing it into a run-on paragraph, while
     still wrapping long lines instead of forcing horizontal scroll. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  background: var(--color-surface-sunken);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: var(--space-2);
}

/* --- Layer management page (/layers) ------------------------------------
 *
 * The vocabulary is small -- a handful of rows -- so this is a plain table
 * rather than a card grid: the columns invite comparison across layers,
 * which is exactly what someone reviewing a vocabulary is doing.
 */

.site-nav {
  margin-left: auto;
  display: flex;
  gap: var(--space-4);
}

.lede {
  color: var(--color-text-muted);
  max-width: 60ch;
}

.hint {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
  max-width: 70ch;
}

.empty {
  color: var(--color-text-muted);
  font-style: italic;
}

/* Screen-reader-only: the actions column needs a name for table semantics
 * but a visible header would just be noise above two buttons. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.layers-table {
  border-collapse: collapse;
  width: 100%;
  margin: var(--space-4) 0;
}

.layers-table th,
.layers-table td {
  text-align: left;
  padding: var(--space-2) var(--space-3) var(--space-2) 0;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}

.layers-table th {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  font-weight: 600;
}

/* Text inputs sit inside cells, so they get no border until focused --
 * a table of bordered boxes reads as a form, not as data. */
.layers-table input[type="text"] {
  width: 100%;
  min-width: 8rem;
  padding: var(--space-1) var(--space-2);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
  font: inherit;
}

.layers-table input[type="text"]:hover {
  border-color: var(--color-border);
}

.layers-table input[type="text"]:focus {
  border-color: var(--color-accent);
  background: var(--color-surface);
  outline: none;
}

.layers-table input[type="color"] {
  width: 2.5rem;
  height: 1.6rem;
  padding: 0;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: none;
}

.layer-swatch {
  display: inline-block;
  width: 1.6rem;
  height: 1.6rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}

.add-layer {
  margin-top: var(--space-6);
  /* Bounded like `.settings-form`, and spaced from whatever follows it.
   * Unbounded, it was the only box on the settings page that grew with the
   * window, so at a wide one it stretched while the forms above and below
   * it stayed put. */
  margin-bottom: var(--space-6);
  max-width: 44rem;
  padding: var(--space-4);
  background: var(--color-surface-sunken);
  border-radius: var(--radius-md);
}

.add-layer h2 {
  margin-top: 0;
  font-size: var(--text-lg);
}

.add-layer-fields {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin: var(--space-3) 0;
}

.add-layer-fields label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.add-layer-fields label.checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
}

/* The bulk random-names switch governs the prefix beside it, so it gets a
 * row of its own rather than sharing one with the fields it affects. */
.add-layer-fields label.bulk-random {
  flex-basis: 100%;
}

.add-layer-fields input[type="text"] {
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
}

/* A disabled field must read as inert: the rule above sets a surface colour
 * and full-strength text, so without this a disabled prefix looks editable. */
.add-layer-fields input:disabled,
.add-layer-fields select:disabled {
  background: var(--color-surface-sunken);
  color: var(--color-text-muted);
  cursor: not-allowed;
}

/* The page-level actions that are plain buttons rather than menus. Sized
 * like the menus beside them so the row reads as one set of controls. */
.controls button {
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 600;
  cursor: pointer;
}

.controls button:hover:not(:disabled) {
  border-color: var(--color-accent);
}

.controls button:disabled {
  color: var(--color-text-muted);
  cursor: default;
}

/* Stacked rather than the wrapping row `.add-layer-fields` uses: each
 * control here is followed by a line saying what the field would be
 * without it, and that line has to sit under its own control rather than
 * wherever the wrap put it. */
.properties-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: var(--space-3) 0;
  min-width: min(28rem, 80vw);
}

.properties-fields label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.properties-fields label.checkbox {
  flex-direction: row;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.properties-fields input[type="text"],
.properties-fields input[type="number"],
.properties-fields select {
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  /* Number inputs size themselves from a default `size` rather than from
   * the flex row, so without this the two sit at their intrinsic width
   * with a gap of dead space after them instead of splitting the row. */
  min-width: 0;
}

/* Two controls that are one setting, side by side: a floor and a cap are
 * read together and compared against each other, so stacking them makes
 * the reader hold one while scrolling to the other. `fieldset` rather
 * than a bare div because they genuinely are a group, and the `legend`
 * is what names the pair -- the individual labels then only have to say
 * which end each one is.
 *
 * The flex row is an inner div rather than the fieldset itself: a
 * `legend` in a flexed fieldset is laid out by special rules rather than
 * as a flex item, which is exactly the kind of thing that renders fine in
 * one browser and oddly in the next. */
.properties-fields .field-group {
  border: 0;
  padding: 0;
  margin: 0;
  min-width: 0;
}

.properties-fields .field-group legend {
  padding: 0;
  margin-bottom: var(--space-1);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.properties-fields .field-row {
  display: flex;
  gap: var(--space-2);
  min-width: 0;
}

.properties-fields .field-row label {
  flex: 1 1 0;
  min-width: 0;
}

/* The "without this it would be..." lines. Tight under their control and
 * quieter than it, so the form still reads as three questions. */
.properties-fields .hint {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
}

button.danger {
  border-color: var(--color-warning-border);
  color: var(--color-warning-text);
  background: var(--color-warning-bg);
}

/* Every dialog's actions, and the ones that share their shape.
 *
 * `dialog > p > button` is the row of actions each dialog ends with. Only
 * `.danger` was styled before, so the removal dialog put a styled Remove
 * next to a browser-default Cancel — two controls of visibly different
 * shape offering one choice, which reads as one of them being an
 * afterthought. The other dialogs were consistent only because neither of
 * their buttons was styled at all. */
.add-layer button,
dialog > p > button,
button.danger {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

.add-layer button:hover:not(:disabled),
dialog > p > button:hover:not(:disabled),
button.danger:hover:not(:disabled) {
  border-color: var(--color-accent);
}

/* A disabled dialog action has to *look* disabled.
 *
 * The replace dialog's confirm button was already disabled while an
 * upload was in flight and while a refusal stood, but nothing styled
 * `:disabled` for these buttons -- so it was indistinguishable from a
 * live one, and the only way to find out was to click it and have
 * nothing happen. `.controls button` had this rule all along; the dialog
 * actions did not. */
.add-layer button:disabled,
dialog > p > button:disabled,
dialog button:disabled,
button.danger:disabled {
  color: var(--color-text-muted);
  border-color: var(--color-border);
  background: var(--color-surface-muted, var(--color-surface));
  cursor: not-allowed;
}

/* Actions inside a table row.
 *
 * One shape for every one of them, so a row reads as a set of choices
 * rather than as controls borrowed from two different pages. `danger`
 * then differs only in colour, which is the one thing it should differ
 * in: it is the same kind of button doing a more serious thing, and a
 * difference in size as well reads as an accident rather than as
 * emphasis.
 *
 * Smaller than a form's submit button, because a row is not a form. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.row-actions button {
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  font-size: var(--text-sm);
  white-space: nowrap;
  cursor: pointer;
}

.row-actions button:hover {
  border-color: var(--color-accent);
}

.row-actions button.danger {
  /* Colour only. Padding, size and shape come from the rule above, which
     this deliberately does not override. */
  border-color: var(--color-warning-border);
  color: var(--color-warning-text);
  background: var(--color-warning-bg);
}

/* Per-row confirmation that a select applied. These save on change, so
 * there is no button to go quiet afterwards and nothing else on the row
 * would otherwise move. */
.row-status {
  margin-left: var(--space-2);
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* The Access section stacks three blocks that arrived from two different
 * places -- a form styled like the layers page's and one styled like the
 * settings page's -- and they were laid out to different widths and with
 * no gap, so two grey panels of different sizes met edge to edge and read
 * as one broken thing. One width, one gap. */
#access-section .add-layer,
#access-section .settings-form,
#access-section .bulk-users,
#access-section .warning {
  max-width: 44rem;
}

#access-section .add-layer,
#access-section .settings-form {
  margin-top: var(--space-5);
  margin-bottom: var(--space-5);
}

/* --- Picking toolbar (viewer page) --------------------------------------- */

.picker-controls {
  border-top: 1px solid var(--color-border);
  padding-top: var(--space-2);
}

.pick-status {
  color: var(--color-text-muted);
  font-size: var(--text-sm);
}

/* Unsaved work is the one thing on this toolbar that must be noticeable
 * without being read. */
.pick-status.dirty {
  color: var(--color-warning-text);
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-border);
  border-radius: var(--radius-sm);
  padding: 0 var(--space-2);
  font-weight: 600;
}

.pick-download {
  font-size: var(--text-sm);
}

/* A crosshair says "this click places something" in a way a label cannot.
 * Applied to the radargram pane only while picking is active. */
#map.picking {
  cursor: crosshair;
}

#map.picking .leaflet-interactive {
  cursor: crosshair;
}

#download-dialog {
  max-width: 32rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
}

#download-dialog select {
  padding: var(--space-1) var(--space-2);
}

.picker-selection {
  background: var(--color-surface-sunken);
  border-radius: var(--radius-sm);
  padding: var(--space-2) var(--space-3);
}

/* --- Vertex handles and overhang markers ---------------------------------
 *
 * Handles are Leaflet divIcons, so they are styled here rather than through
 * path options. Sized for a finger first: 16px is already near the lower
 * bound for a touch target, and coarse pointers get more below.
 */

.pick-handle {
  box-sizing: border-box;
  border-radius: 50%;
  border: 2px solid var(--color-text);
  background: var(--color-surface);
  box-shadow: 0 0 0 1px rgb(0 0 0 / 35%);
  cursor: grab;
}

.pick-handle:active {
  cursor: grabbing;
}

/* Hollow means splittable: it is the only visual difference between a
 * vertex that will split the line and one that will not, so it has to be
 * legible at a glance. */
.pick-handle-interior {
  background: var(--color-surface);
}

.pick-handle-end,
.pick-handle-draft {
  background: var(--color-text);
}

/* An overhang marker sits on top of a handle, so it must read as a warning
 * without hiding what is underneath. */
.pick-overhang {
  box-sizing: border-box;
  border-radius: 50%;
  border: 3px solid #d7263d;
  background: rgb(215 38 61 / 22%);
  pointer-events: auto;
}

/* Allowed on this layer: still shown, because it changes how the layer
 * exports, but muted so it does not read as an error. */
.pick-overhang-allowed {
  border-color: var(--color-warning-border);
  background: rgb(224 178 60 / 22%);
}

/* Marker sizes are set in picker.js, not here: Leaflet derives each icon's
 * anchor from its size, and overriding the size in CSS left the anchor
 * behind -- every handle sat a few pixels below its line. */

/* The status text changes on every vertex, and must not resize the toolbar
 * when it does: a toolbar that grows pushes the map down mid-tap.
 *
 * It takes the remaining space on its row and truncates, rather than
 * wrapping or claiming a row of its own. An earlier attempt gave it
 * `flex-basis: 100%`, which did stop the reflow but made the toolbar a line
 * taller -- enough to make the page scroll on a phone, where the first tap
 * then goes to collapsing the address bar instead of to the control under
 * it. Stability has to come from *not changing size*, not from more space. */
.picker-controls .pick-status {
  flex: 1 1 0;
  min-width: 6rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

@media (max-width: 40rem) {
  .picker-controls,
  .picker-selection {
    gap: var(--space-2);
  }

  .picker-controls button,
  .picker-selection button {
    min-height: 2.75rem;
  }
}

/* The picking error is a fixed toast rather than a block in the flow.
 *
 * In flow it changed the page height every time it appeared or cleared,
 * which on a phone moves the map and the toolbar under the finger that
 * triggered it -- and picking errors appear and clear constantly, once per
 * refused tap. Taking it out of the flow means no picking interaction can
 * reflow the page at all.
 *
 * Bottom rather than top: on a phone the top of the screen is the furthest
 * point from the thumb, and the toolbar it refers to is at the bottom of
 * the controls anyway. */
#pick-error {
  position: fixed;
  left: var(--space-3);
  right: var(--space-3);
  bottom: var(--space-3);
  z-index: 1000;
  margin: 0;
  max-width: 44rem;
  margin-inline: auto;
  box-shadow: 0 2px 12px rgb(0 0 0 / 30%);
}

/* The invisible companion that makes a line tappable -- picked reflectors
 * on the radargram, and radargram tracks on the geographic maps.
 *
 * `pointer-events: stroke` hit-tests the stroke area regardless of whether
 * it is painted, so a fully transparent line still receives taps. Chrome
 * happens to hit-test a `stroke-opacity: 0` path anyway; saying so
 * explicitly is what makes it portable. */
.hit-line {
  pointer-events: stroke;
  cursor: pointer;
}

/* --- Narrow screens ------------------------------------------------------
 *
 * Two problems, one cause: the radargram and the overview map both used the
 * full viewer height, so at almost any scroll position the screen was
 * entirely map. Leaflet claims touch drags inside a map for panning, which
 * left only a sliver of page to scroll with -- and the sliver was at the
 * edge, where a thumb cannot reach reliably.
 *
 * So: neither map takes the whole screen, and the picking toolbar sticks to
 * the top. Between them there is always a band of ordinary page to drag,
 * and the controls you need while drawing never scroll away in the first
 * place. */
@media (max-width: 40rem) {
  :root {
    --map-height-viewer: 52svh;
    --map-min-height: 240px;
  }

  /* Shorter still: it is context, not the thing being worked on. */
  #overview-map {
    height: 30svh;
    min-height: 180px;
  }

  /* Stack the radargram region above the overview map, and drop the
   * horizontal handle that would then make no sense. The trace panel
   * stacks within the region too, which is why both minimums go. */
  .layout {
    flex-wrap: wrap;
  }

  #split-resizer {
    display: none;
  }

  .radar-region,
  .radar-region.has-trace {
    flex-wrap: wrap;
    min-width: 0;
  }

  .picker-controls {
    position: sticky;
    top: 0;
    z-index: 500;
    background: var(--color-bg);
    border-bottom: 1px solid var(--color-border);
    padding-bottom: var(--space-2);
  }
}

/* `svh` is the stable unit (see the note on --map-height-viewer); this is
   the fallback for browsers without it. */
@media (max-width: 40rem) {
  @supports not (height: 1svh) {
    :root {
      --map-height-viewer: 52vh;
    }

    #overview-map {
      height: 30vh;
    }
  }
}

/* --- Header menu ---------------------------------------------------------
 *
 * A `<details>` disclosure, so it opens and closes with no JavaScript and is
 * keyboard-operable for free. app.js only adds dismissal on Escape or an
 * outside click, which `<details>` does not do on its own. */

.site-menu {
  position: relative;
}

.site-menu > summary {
  list-style: none;
  cursor: pointer;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-accent);
  font-size: var(--text-sm);
  font-weight: 600;
  user-select: none;
}

/* Safari still paints the default triangle without this. */
.site-menu > summary::-webkit-details-marker {
  display: none;
}

.site-menu > summary::before {
  content: "\2261"; /* identical-to: three bars, without needing an icon font */
  margin-right: var(--space-2);
  font-size: 1.1em;
  line-height: 1;
}

.site-menu > summary:hover,
.site-menu[open] > summary {
  border-color: var(--color-accent);
}

/* Above Leaflet's control corners, which sit at 1000 and would otherwise
 * win on DOM order alone -- the map comes after this row in the document,
 * so its zoom buttons painted straight through the open panel. Below
 * nothing in particular: this is the topmost thing on the page while it is
 * open, which is what a menu should be. */
.site-menu > ul {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 1200;
  margin: 0;
  padding: var(--space-1);
  min-width: 12rem;
  list-style: none;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: 0 4px 16px rgb(0 0 0 / 22%);
}

.site-menu > ul a {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  text-decoration: none;
  font-weight: 500;
  white-space: nowrap;
}

.site-menu > ul a:hover,
.site-menu > ul a:focus-visible {
  background: var(--color-surface-sunken);
}

/* The items are buttons, not links: some of them open a dialog rather than
 * navigating, and a link that does not navigate is a lie to anyone
 * middle-clicking it.
 *
 * On `.site-menu` rather than `.download-menu`, which is where it started:
 * the reasoning above is about menu items in general, and the second menu
 * to use buttons (a card's Edit) should not have to restate it to look the
 * same. Matches `.site-menu > ul a` above, so a menu of links and a menu
 * of buttons are indistinguishable to look at -- which is the point, since
 * the difference is about what the item does, not what it is. */
.site-menu > ul button {
  display: block;
  width: 100%;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border: none;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-accent);
  font: inherit;
  font-size: var(--text-sm);
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
}

.site-menu > ul button:hover,
.site-menu > ul button:focus-visible {
  background: var(--color-surface-sunken);
}

/* A destructive item keeps the menu's shape and changes only its colour.
 * The full warning treatment belongs on the button that actually does the
 * thing -- this one opens a confirmation, and dressing it as the deed
 * itself would cry wolf. */
.site-menu > ul button.danger {
  color: var(--color-warning-text);
}

/* The account block at the foot of the menu.
 *
 * Its first row is a caption rather than a link, which is the one thing in
 * this list that cannot be clicked. Left unstyled it inherited neither the
 * links' horizontal padding nor their colour, so it sat out of alignment
 * between two identical-looking rows and read as a link that had broken.
 * Same padding, muted, and a rule above the whole block so the boundary
 * between "where to go" and "who you are" is visible rather than implied. */
.site-menu-note,
.site-menu-separated {
  margin-top: var(--space-1);
  padding-top: var(--space-1);
  border-top: 1px solid var(--color-border);
}

.site-menu-note {
  padding-right: var(--space-3);
  padding-bottom: var(--space-1);
  padding-left: var(--space-3);
  font-size: var(--text-sm);
  line-height: var(--line-base);
  white-space: nowrap;
  cursor: default;
}

.site-menu-note-label {
  color: var(--color-text-muted);
}

/* --- Back link -----------------------------------------------------------
 *
 * Styled as a control rather than a bare link: it sits alone above the
 * heading, where an unstyled anchor reads as leftover markup. */
.back-link {
  display: inline-block;
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-accent);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.back-link:hover,
.back-link:focus-visible {
  border-color: var(--color-accent);
  color: var(--color-accent-hover);
}

/* The picking toast, when it is explaining rather than complaining. Same
 * element and same fixed position, so it still costs no layout. */
#pick-error.toast-info {
  background: var(--color-surface);
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

/* The handle between two vertices that adds a third.
 *
 * Two elements, on purpose: the outer one is the touch target and is
 * invisible, the inner dot is what is seen. They used to be the same
 * element, which forced a choice between a dot small enough to read as
 * "not yet a vertex" and a target big enough to drag -- and the small dot
 * won, so it could be tapped but not dragged. */
.pick-midpoint {
  background: none;
  border: none;
  display: grid;
  place-items: center;
  cursor: grab;
}

.pick-midpoint:active {
  cursor: grabbing;
}

.pick-midpoint-dot {
  width: 12px;
  height: 12px;
  box-sizing: border-box;
  border: 2px dashed var(--color-text);
  border-radius: 50%;
  background: var(--color-surface);
  opacity: 0.75;
}

.pick-midpoint:hover .pick-midpoint-dot {
  opacity: 1;
}

@media (pointer: coarse) {
  /* The dot only -- the target around it is sized in picker.js. */
  .pick-midpoint-dot {
    width: 16px;
    height: 16px;
  }
}

/* A metadata value that is an absence rather than a number. Muted so a
 * grid of cards can be scanned for the ones carrying a real count, without
 * the row itself looking different from its neighbours. */
.meta-none {
  color: var(--color-text-muted);
  font-style: italic;
}

/* --- Project settings ---------------------------------------------------- */

.settings-summary {
  margin-bottom: var(--space-5);
}

/* The three areas of the settings page: my preferences, the project
 * defaults, and access. Separated by a rule rather than by spacing alone,
 * because "who may change this" differs between them and a reader should
 * be able to see where one authority ends and the next begins. */
.settings-section {
  margin-bottom: var(--space-6);
  padding-bottom: var(--space-5);
  border-bottom: 1px solid var(--color-border);
}

.settings-section:last-child {
  border-bottom: none;
}

.settings-section h2 {
  font-size: var(--text-lg);
}

.settings-section h3 {
  font-size: var(--text-base);
}

/* A settings section holding edits the server has not seen (#177 review).
 *
 * Adding a basemap or an overlay puts a card on the page, which looks like
 * it happened. The rule down the side and the badge beside the heading say
 * that it has not yet; `beforeunload` in settings.js is what stops the page
 * being closed on top of it.
 *
 * Warning colours rather than danger: nothing is broken, something is
 * merely not finished. */
.settings-section.is-unsaved {
  border-left: 3px solid var(--color-warning-border);
  padding-left: var(--space-4);
  /* Pulled back by its own indent so the heading stays in line with the
   * sections above and below rather than stepping right when it changes. */
  margin-left: calc(-1 * var(--space-4) - 3px);
}

.settings-section.is-unsaved > h2::after {
  content: "unsaved";
  margin-left: var(--space-3);
  padding: 0 var(--space-2);
  border: 1px solid var(--color-warning-border);
  border-radius: var(--radius-sm);
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  font-size: var(--text-sm);
  font-weight: 400;
  vertical-align: middle;
}

/* A checkbox reads as one line, not as a stacked label over a control. */
.settings-field.checkbox {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2);
}

.settings-field.checkbox .hint {
  flex-basis: 100%;
}

/* The invite link is the one string on this page that must be selected
 * and copied exactly, so it wraps on any character rather than overflowing
 * or breaking only at spaces it does not contain. */
#invite-link {
  display: inline-block;
  overflow-wrap: anywhere;
  user-select: all;
}

.settings-form {
  padding: var(--space-4);
  background: var(--color-surface-sunken);
  border-radius: var(--radius-md);
  max-width: 44rem;
}

.settings-form h2 {
  margin-top: 0;
  font-size: var(--text-lg);
}

.settings-field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  margin-bottom: var(--space-4);
}

.settings-label {
  font-weight: 600;
}

.settings-field select {
  align-self: flex-start;
  min-width: 14rem;
  font: inherit;
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
}

.settings-form button {
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

.settings-form button:hover {
  border-color: var(--color-accent);
}

/* One basemap's fields (#177). A card rather than a table row: there are
 * eight fields, most of them optional, and a row of eight boxes is
 * unreadable on a phone and barely better on a laptop. The fields inside
 * reuse `.add-layer-fields`, which already wraps a labelled row. */
.basemap-card {
  margin: var(--space-3) 0;
  padding: var(--space-3) var(--space-4);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface-sunken);
  max-width: 44rem;
}

.basemap-card legend {
  padding: 0 var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

.basemap-card input[type="text"],
.basemap-card input[type="number"] {
  padding: var(--space-2);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  color: var(--color-text);
  font: inherit;
}

/* Tile size, max zoom and zoom offset are two or three characters wide;
 * left at the browser's default they take as much room as the address. */
.basemap-card input.narrow-number {
  width: 6rem;
}

/* Leaflet's layer control is a white panel with dark text of its own, in
 * both themes -- it sits on imagery, which looks the same either way.
 *
 * Its radio buttons and checkboxes are native controls, so they follow the
 * *document's* `color-scheme`, which is dark for a reader in dark mode.
 * That left dark checkboxes in a bright panel: not what Leaflet looks like
 * anywhere else, and the only part of the GUI where the two disagreed.
 * Declaring the scheme on the control keeps the widgets matching the panel
 * they are drawn in rather than the page around it. */
.leaflet-control-layers {
  color-scheme: light;
}

/* An overlay feature's popup (#177): a name, and a description that is
 * whatever HTML the file carried. Bounded rather than trusted to be short
 * -- the description comes from somebody else's data, and a long one would
 * otherwise grow the popup past the map. */
.overlay-popup {
  max-width: 18rem;
  max-height: 14rem;
  overflow-y: auto;
}

.overlay-popup-body {
  margin-top: var(--space-1);
}

/* Foreign HTML, so the elements it may contain are sized here rather than
 * left to the browser's defaults, which assume a document rather than a
 * popup. */
.overlay-popup-body img {
  max-width: 100%;
  height: auto;
}

.overlay-popup-body table {
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.overlay-popup-body :is(h1, h2, h3, h4, p) {
  margin: var(--space-1) 0;
  font-size: var(--text-base);
}

/* A layer swatch sitting beside a select rather than in a table cell.
 *
 * Sized in `em` so it tracks the control it labels, and vertically centred
 * against the select's text. Kept under the row height on purpose: this
 * toolbar must not change size when the picker updates it, and a swatch
 * taller than the buttons beside it would set a new row height. */
.layer-swatch-inline {
  width: 1.1em;
  height: 1.1em;
  vertical-align: -0.2em;
  margin: 0 var(--space-1) 0 var(--space-2);
  border-color: var(--color-border-strong);
}

/* The download menu reuses the header menu's disclosure styling, but sits
 * in a controls row where the surrounding buttons set the type size.
 *
 * It also has to be anchored differently. The header's menu button is at
 * the right edge of the page, so a panel hung from its right edge opens
 * leftward into the page. This one sits partway along a row, so the same
 * rule ran the panel off the left of a phone screen entirely.
 *
 * Anchoring to the row instead of to the button fixes it at every width:
 * the panel starts at the row's left edge and opens rightward, which is on
 * screen by construction. That means the row is the containing block, so
 * the menu itself must not be one. */
.controls {
  position: relative;
}

.download-menu {
  position: static;
}

.download-menu > ul {
  left: 0;
  right: auto;
  max-width: 100%;
}

/* The compact variants: a card's Edit menu and the download menus. Smaller
 * than the header's because they sit inside content rather than in a bar of
 * their own, and identical to each other because there is no reason for a
 * reader to have to notice which one they are looking at. */
.card-menu > summary,
.group-menu > summary,
.download-menu > summary {
  font-size: var(--text-sm);
  padding: var(--space-1) var(--space-2);
}

.download-menu > summary::before {
  content: "\2193"; /* downwards arrow, in place of the header's bars */
}

#image-dialog {
  max-width: 34rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
}

#image-dialog select {
  padding: var(--space-1) var(--space-2);
}

/* A group's heading and its download menu on one line. The menu is what
 * ties the downloads to *this* group rather than the page. */
.group-heading {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  /* The h2 below has its margins zeroed so it aligns with the menu button,
   * which also removed the gap that normally separates a heading from what
   * it labels -- leaving the button sitting on the map. Put it back on the
   * row instead, where wrapping cannot lose it. */
  margin-bottom: var(--space-2);
}

/* Pushed to the right edge, so a group reads the same way a card does:
 * what it is on the left, what can be done to it on the right. */
.group-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

.group-menu {
  flex: none;
}

.group-heading h2 {
  margin: 0;
}

/* The group menu anchors to the heading rather than to a controls row, so
 * it needs its own containing block -- unlike the viewer's, which borrows
 * the row's. Left-aligned for the same reason as that one: the button is
 * near the left of the page, so opening leftward would run off screen. */
.group-heading .download-menu {
  position: relative;
}

#group-download-dialog {
  max-width: 34rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: var(--color-text);
}

#group-download-dialog select {
  padding: var(--space-1) var(--space-2);
}

/* What carrying picks onto this revision cost (#148).
 *
 * A banner rather than the `#pick-error` toast. The toast is `position:
 * fixed` and transient, for something that just happened; this is a
 * standing condition of everything on screen and has to keep saying so
 * for as long as it is true. It takes layout accordingly.
 *
 * Warning-coloured by default, because the ordinary case for showing it
 * at all is that the picks are not where they were drawn. `is-refused`
 * is the one that is not a caveat but a failure — nothing is being
 * shown — and it reads differently. */
.carry-banner {
  margin: 0 0 var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-warning-border);
  border-radius: var(--radius-sm);
  background: var(--color-warning-bg);
  color: var(--color-warning-text);
  font-size: var(--text-sm);
}

.carry-banner strong {
  margin-right: var(--space-2);
}

/* Refusal is not a caveat: nothing is being drawn.
 *
 * It used to fall back to the warning colours through `var(..., ..)`
 * defaults for tokens that were never declared, so the state that means
 * "these picks are not on screen at all" looked identical to the one that
 * means "they moved". The left border carries it as well as the colour,
 * so the distinction survives a monochrome display or a viewer who cannot
 * separate the two hues. */
.carry-banner.is-refused {
  border-color: var(--color-danger-border);
  background: var(--color-danger-bg);
  color: var(--color-danger-text);
  border-left-width: 4px;
}

.carry-banner details {
  margin-top: var(--space-2);
}

.carry-banner summary {
  cursor: pointer;
}

.carry-banner dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-2) 0 0;
}

.carry-banner dt {
  font-weight: 600;
}

.carry-banner dd {
  margin: 0;
}

/* The consequence report inside the replace dialog (#148).
 *
 * Two columns so the question and its answer line up down the page: an
 * operator reads this to find the one row that is not "carries cleanly",
 * and a paragraph would hide it. */
.replace-detail {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  margin: var(--space-2) 0 0;
  font-size: var(--text-sm);
}

.replace-detail dt {
  font-weight: 600;
}

.replace-detail dd {
  margin: 0;
}

#replace-dialog {
  max-width: 40rem;
}

/* --- Layer panel, derived items and the expression editor (#209) --------- */

.layer-panel {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: var(--radius, 4px);
  padding: var(--space-2);
  /* Never wider than the map, however long a layer name is: the control sits
   * at the map's right edge with a margin, so a cap tied to `vw` alone still
   * overflows a phone. The scrolling lives on the body below, so a long row
   * pans sideways instead of pushing the control off screen. */
  max-width: min(22rem, calc(90vw - 1rem));
  box-sizing: border-box;
}

.layer-panel-heading {
  font-weight: 600;
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-text-muted);
  margin: var(--space-2) 0 var(--space-1);
}

.layer-panel-heading:first-child {
  margin-top: 0;
}

.layer-panel-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.1rem 0;
  cursor: pointer;
  white-space: nowrap;
}

.layer-panel-swatch {
  width: 0.75rem;
  height: 0.75rem;
  border-radius: 2px;
  border: 1px solid var(--color-border-strong);
  flex: none;
}

/* The textarea sits on top of a highlighted <pre>; both share one box model
 * so the tokens line up under the caret exactly. The textarea's own text is
 * transparent -- the <pre> is what is read -- but its caret stays visible. */
.editor-input {
  position: relative;
  margin: var(--space-2) 0;
}

#derived-expression,
#derived-highlight {
  box-sizing: border-box;
  width: 100%;
  min-height: 5.5rem;
  margin: 0;
  padding: var(--space-2);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius, 4px);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 0.9rem;
  line-height: 1.4;
  white-space: pre-wrap;
  word-break: break-word;
  tab-size: 2;
}

#derived-highlight {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

#derived-expression {
  position: relative;
  z-index: 1;
  resize: vertical;
  background: transparent;
  color: transparent;
  caret-color: var(--color-text);
}

#derived-expression:focus {
  outline: 2px solid var(--color-accent);
  outline-offset: -1px;
}

.tok-builtin {
  color: var(--color-accent);
  font-weight: 600;
}

.tok-keyword {
  color: #9a3f9a;
  font-weight: 600;
}

.tok-layer {
  color: #2f7d32;
}

.tok-number {
  color: #b06000;
}

.tok-op {
  color: var(--color-text-muted);
}

.editor-status {
  font-size: 0.85rem;
  color: var(--color-text-muted);
  min-height: 1.2em;
  margin: var(--space-1) 0;
}

.editor-status.editor-error {
  color: var(--color-danger-text);
}

.editor-warning {
  font-size: 0.85rem;
  background: var(--color-warning-bg);
  border: 1px solid var(--color-warning-border);
  color: var(--color-warning-text);
  border-radius: var(--radius, 4px);
  padding: var(--space-2);
}

/* --- Layer panel disclosure and derived management (#209) ---------------- */

/* The panel is a `<details>`, closed by default. Reusing the header menu's
 * pattern means the open/close is keyboard-accessible with no JavaScript; it
 * deliberately does not close on an outside click, because the map is what is
 * being looked at while layers are toggled. */
.layer-panel > summary {
  cursor: pointer;
  font-weight: 600;
  padding: var(--space-1) var(--space-2);
  list-style: none;
}

.layer-panel > summary::-webkit-details-marker {
  display: none;
}

.layer-panel > summary::marker {
  content: "";
}

.layer-panel-body {
  padding: 0 var(--space-2) var(--space-2);
  max-height: 60vh;
  overflow: auto;
  /* Horizontal touch panning is the phone case: a long expression or layer
   * name scrolls sideways rather than being clipped. `pan-x pan-y` keeps
   * that gesture off the map behind it. */
  touch-action: pan-x pan-y;
  -webkit-overflow-scrolling: touch;
}

.layer-panel-derived-row {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.layer-panel-toggle {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  cursor: pointer;
  white-space: nowrap;
}

.layer-panel-confirm {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  font-size: 0.75rem;
}

.layer-panel-error {
  color: var(--color-danger-text);
  font-size: 0.8rem;
  margin-bottom: var(--space-1);
  white-space: normal;
}

/* The shared derived-item editor (app.js). The range fields are disabled as a
 * group until the checkbox is on, so the control reads as one choice. */
#derived-editor {
  max-width: 42rem;
}

#derived-range-fields {
  border: 1px solid var(--color-border);
  border-radius: var(--radius, 4px);
  padding: var(--space-2);
  margin: var(--space-2) 0;
}

#derived-range-fields.is-disabled .add-layer-fields {
  opacity: 0.5;
}

#derived-editor input[type="color"] {
  padding: 0;
  width: 2.2rem;
  height: 1.6rem;
  vertical-align: middle;
}

/* The layers-panel toggle in the viewer controls row. `.controls button:hover`
 * only recolours the border and text, which is easy to miss on a small
 * control, so this one fills with the accent colour (and animates) on hover.
 * `var(--color-surface)` as the text keeps it legible in both themes: it is
 * light where the accent is dark and dark where the accent is light. */
#panel-visibility {
  cursor: pointer;
  transition:
    background-color 0.12s ease,
    border-color 0.12s ease,
    color 0.12s ease;
}

#panel-visibility:hover {
  border-color: var(--color-accent-hover);
  background: var(--color-accent-hover);
  color: var(--color-surface);
}

#panel-visibility[aria-pressed="true"] {
  border-color: var(--color-accent);
}

/* The disclosure summary ("Layers (N)") is the control people actually click
 * on the map, so it gets the same affordance. */
.layer-panel > summary {
  border-radius: var(--radius, 4px);
  transition: background-color 0.12s ease;
}

.layer-panel > summary:hover {
  background: var(--color-surface-sunken);
}
