/*
  The look every page shares (docs/StyleGuide.md): the Monitor's pages, the upload page
  and the Memory tool, at home and in the cloud. Loaded before each page's own style.css, which
  keeps only what is the page's own. No page writes a colour of its own except for data.
*/

/* ── tokens ─────────────────────────────────────────────────────────────── */

:root {
  color-scheme: dark;
  --ground: #0f1316;
  --surface: #171c20;
  --raised: #1e252a;
  --ink: #e3e8eb;
  --muted: #8f9ba3;
  --line: #2a3238;
  --line-strong: #3b454c;
  --home: #2ec4b6;
  --home-soft: #12302e;
  --cloud: #a78bfa;
  --cloud-soft: #2a2140;
  --good: #3fb950;
  --warn: #d29922;
  --bad: #f85149;
  --on-accent: #0f1316;
  /* Until /side.js has said where the page runs, or if it could not be loaded. */
  --accent: var(--home);
  --accent-soft: var(--home-soft);
  /* Hover and pressed shift lightness, never hue: over any surface, and over the accent. */
  --tint: rgb(255 255 255 / 6%);
  --tint-strong: rgb(255 255 255 / 11%);
  --accent-hover: color-mix(in oklab, var(--accent), white 14%);
  --accent-pressed: color-mix(in oklab, var(--accent), black 18%);
  --shadow: 0 6px 24px rgb(0 0 0 / 45%);
  --radius: 8px;
  /* What sticks to the top of the window keeps clear of the header, which is this high. */
  --header-height: 3.5rem;
  /* Floating boxes keep this far from the map's edges, and as far from each other. */
  --float-gap: 10px;
  /* Bottom boxes stop above Leaflet's scale and attribution, which are never covered. */
  --float-bottom: 2.25rem;
  /* A box down the right of a map is this wide, unless the page says otherwise. */
  --side-width: 26rem;
  /* How long whatever opens, closes, folds or slides takes, and how it eases in and out. */
  --motion: 200ms;
  --motion-ease: cubic-bezier(0.4, 0, 0.2, 1);
  --mono: ui-monospace, 'SFMono-Regular', Menlo, monospace;
  font-family:
    system-ui,
    -apple-system,
    'Segoe UI',
    Roboto,
    sans-serif;
  color: var(--ink);
  background: var(--ground);
}

/* The accent says where the page runs; --home and --cloud say what a thing belongs to. */
:root[data-side='home'] {
  --accent: var(--home);
  --accent-soft: var(--home-soft);
}

:root[data-side='cloud'] {
  --accent: var(--cloud);
  --accent-soft: var(--cloud-soft);
}

[x-cloak] {
  display: none !important;
}

/*
  What x-if draws as it opens comes in as it does. Alpine moves only what x-show hides, so
  whatever opens and can be x-show is, with x-collapse or x-transition; this is for what can't,
  such as a part that reads what is not there while it is closed.
*/
.appears {
  animation: geo-appear var(--motion) var(--motion-ease);
}

@keyframes geo-appear {
  from {
    opacity: 0;
    transform: translateY(-0.4rem);
  }
}

/*
  Whatever opens, closes, folds or slides does it at once for whoever asked for less motion. The
  durations go, not the transitions, so what waits for a transition's end still hears of it. A
  spinner keeps turning, slowly: it says that something is under way.
*/
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after,
  ::backdrop {
    transition-duration: 0s !important;
    transition-delay: 0s !important;
  }

  .appears {
    animation: none;
  }
}

/* ── the page ───────────────────────────────────────────────────────────── */

html {
  scroll-padding-top: var(--header-height);
}

body {
  margin: 0;
  padding-top: var(--header-height);
  background: var(--ground);
}

main {
  max-width: 72rem;
  margin: 0 auto;
  padding: 1.5rem;
}

h2 {
  font-size: 1.05rem;
  margin: 1.5rem 0 0.75rem;
}

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

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

code,
.path {
  font-family: var(--mono);
}

.path {
  font-size: 0.85rem;
  word-break: break-all;
}

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

.hint,
.muted {
  color: var(--muted);
}

.hint {
  font-size: 0.9rem;
}

.problem {
  color: var(--bad);
}

.warn {
  color: var(--warn);
}

.num {
  text-align: right;
}

.num,
.when,
time {
  font-variant-numeric: tabular-nums;
}

/* A moment is one unit; broken over two lines, the date and the time read as two values. */
.when {
  white-space: nowrap;
}

.row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
}

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.25rem;
  margin-bottom: 0.75rem;
}

.card h2:first-child {
  margin-top: 0;
}

.login {
  max-width: 22rem;
  margin: 3rem auto;
  display: grid;
  gap: 0.75rem;
}

.login h2 {
  margin: 0;
}

.swatch {
  display: inline-block;
  width: 0.9rem;
  height: 0.9rem;
  border-radius: 3px;
  vertical-align: middle;
}

/* ── the header and its menu ────────────────────────────────────────────── */

/* Always in sight on a wide screen, over the page as it scrolls by. */
.app-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 1050;
  box-sizing: border-box;
  height: var(--header-height);
  display: flex;
  align-items: center;
  gap: 1.75rem;
  padding: 0 1.5rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.brand {
  flex: none;
  color: var(--ink);
  font-size: 1.2rem;
  font-weight: 650;
  letter-spacing: 0.01em;
  text-decoration: none;
  white-space: nowrap;
}

.brand:hover {
  color: var(--ink);
}

.brand .geo {
  color: var(--accent);
  font-weight: 500;
}

/* A trail inside the page, such as Memory's timeframe, after the brand. */
.trail {
  min-width: 0;
  margin-left: -1.25rem;
  overflow: hidden;
  color: var(--muted);
  text-overflow: ellipsis;
  white-space: nowrap;
}

.trail button.link {
  font-size: inherit;
}

.app-menu {
  flex: 1;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 1.5rem;
}

.app-menu nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
}

.app-menu nav a {
  padding: 0.25rem 0;
  border-bottom: 2px solid transparent;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
}

.app-menu nav a:hover {
  color: var(--ink);
}

.app-menu nav a[aria-current='page'] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

.who {
  margin-left: auto;
  display: flex;
  gap: 0.75rem;
  align-items: center;
  color: var(--muted);
  white-space: nowrap;
}

.menu-button {
  display: none;
}

/* One breakpoint for every page. */
@media (max-width: 48rem) {
  html {
    scroll-padding-top: 0;
  }

  body {
    padding-top: 0;
  }

  main {
    padding: 1rem;
  }

  /* In the flow, so it scrolls out of view with the page; the menu button stays. */
  .app-header {
    position: static;
    height: auto;
    min-height: var(--header-height);
    padding: 0.5rem 4.5rem 0.5rem 1rem;
  }

  .trail {
    margin-left: -1rem;
  }

  .menu-button {
    position: fixed;
    top: var(--float-gap);
    right: var(--float-gap);
    z-index: 1060;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--raised);
    color: var(--ink);
    box-shadow: var(--shadow);
  }

  .menu-button:where(:enabled:hover) {
    background: color-mix(in oklab, var(--raised), white 6%);
    color: var(--ink);
  }

  .menu-button svg {
    width: 22px;
    height: 22px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
  }

  .menu-button .close,
  .app-header.open .menu-button .open {
    display: none;
  }

  .app-header.open .menu-button .close {
    display: block;
  }

  .app-menu {
    position: fixed;
    top: calc(var(--float-gap) + 44px + 6px);
    right: var(--float-gap);
    z-index: 1060;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    width: min(18rem, calc(100vw - 2 * var(--float-gap)));
    max-height: calc(100dvh - 44px - 3 * var(--float-gap));
    box-sizing: border-box;
    overflow-y: auto;
    padding: 0.4rem;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    background: var(--raised);
    box-shadow: var(--shadow);
    /* It drops open from the button, and back; display waits for the fade to end. */
    opacity: 0;
    transform: translateY(-0.4rem);
    transform-origin: top right;
    transition:
      opacity var(--motion) var(--motion-ease),
      transform var(--motion) var(--motion-ease),
      display var(--motion) allow-discrete;
  }

  .app-header.open .app-menu {
    display: flex;
    opacity: 1;
    transform: none;
  }

  @starting-style {
    .app-header.open .app-menu {
      opacity: 0;
      transform: translateY(-0.4rem);
    }
  }

  .app-menu nav {
    flex-direction: column;
    gap: 0;
  }

  .app-menu nav a {
    padding: 0.6rem 0.75rem;
    border-bottom: 0;
    border-left: 2px solid transparent;
    border-radius: 0 4px 4px 0;
  }

  .app-menu nav a:hover {
    background: var(--tint);
  }

  .app-menu nav a[aria-current='page'] {
    border-left-color: var(--accent);
    background: var(--tint);
  }

  .who {
    margin: 0;
    padding: 0.6rem 0.75rem;
  }

  .who button.link {
    margin-left: auto;
  }

  /* A table scrolls sideways here, so a path keeps a width to be read at. */
  .table td.path {
    min-width: 12rem;
  }

  .app-menu nav + .who {
    margin-top: 0.4rem;
    border-top: 1px solid var(--line);
  }

  /* Where the map is the page, it takes all of a narrow screen: only the menu button is left. */
  body.map-page .app-header {
    position: fixed;
    min-height: 0;
    height: 0;
    padding: 0;
    border: 0;
    overflow: visible;
  }

  body.map-page .app-header > :not(.menu-button, .app-menu) {
    display: none;
  }
}

/* ── controls ───────────────────────────────────────────────────────────── */

label {
  display: grid;
  gap: 0.25rem;
  font-size: 0.9rem;
  color: var(--muted);
}

input:not([type='checkbox'], [type='radio'], [type='range'], [type='color'], [type='file']),
select,
textarea {
  box-sizing: border-box;
  min-width: 0;
  padding: 0.45rem 0.6rem;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--ground);
  color: var(--ink);
  font: inherit;
}

select {
  padding-block: 0.4rem;
}

input:focus,
select:focus,
textarea:focus {
  border-color: var(--accent);
}

input::placeholder,
textarea::placeholder {
  color: var(--muted);
  opacity: 0.8;
}

input:disabled,
select:disabled,
textarea:disabled {
  opacity: 0.6;
}

input[type='checkbox'],
input[type='radio'],
input[type='range'],
progress {
  accent-color: var(--accent);
}

input[type='color'] {
  padding: 0;
  border: 1px solid var(--line-strong);
  border-radius: 4px;
  background: var(--ground);
}

/* Primary: the accent, with the ground's colour on it. */
button,
a.button {
  display: inline-block;
  box-sizing: border-box;
  padding: 0.45rem 1rem;
  border: 1px solid transparent;
  border-radius: 6px;
  background: var(--accent);
  color: var(--on-accent);
  font: inherit;
  font-weight: 550;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color 0.1s,
    border-color 0.1s,
    box-shadow 0.1s;
}

button:disabled {
  opacity: 0.55;
  cursor: default;
}

/*
  A button answers the pointer: lighter under it, and darker and sunk in while it is held, so a
  click that only queues something is still seen to land. :where() adds nothing to the
  specificity, so a button styled by its place keeps its own look; :enabled leaves a disabled
  one still.
*/
button:where(:enabled:hover),
a.button:hover {
  background: var(--accent-hover);
  color: var(--on-accent);
}

button:where(:enabled:active) {
  background: var(--accent-pressed);
  box-shadow: inset 0 2px 3px rgb(0 0 0 / 30%);
}

/* Secondary: outlined, on whatever it lies on. */
button.secondary,
a.button.secondary {
  background: transparent;
  border-color: var(--line-strong);
  color: var(--ink);
  font-weight: 400;
}

button.secondary:where(:enabled:hover),
a.button.secondary:hover {
  background: var(--tint);
  border-color: var(--muted);
  color: var(--ink);
}

button.secondary:where(:enabled:active) {
  background: var(--tint-strong);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 30%);
}

/* Destructive: outlined in red, until it is the one being confirmed. */
button.danger {
  background: transparent;
  border-color: color-mix(in oklab, var(--bad), transparent 40%);
  color: var(--bad);
  font-weight: 400;
}

button.danger:where(:enabled:hover) {
  background: color-mix(in oklab, var(--bad), transparent 88%);
  border-color: var(--bad);
  color: var(--bad);
}

button.danger:where(:enabled:active) {
  background: color-mix(in oklab, var(--bad), transparent 80%);
}

/* In a question, the destructive button is the one being confirmed. */
dialog.question button.danger {
  background: var(--bad);
  border-color: var(--bad);
  color: var(--on-accent);
  font-weight: 550;
}

dialog.question button.danger:where(:enabled:hover) {
  background: color-mix(in oklab, var(--bad), white 14%);
  color: var(--on-accent);
}

dialog.question button.danger:where(:enabled:active) {
  background: color-mix(in oklab, var(--bad), black 18%);
}

/* A link: text in the accent. */
button.link,
a.link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-weight: inherit;
  text-decoration: none;
}

button.link:where(:enabled:hover),
a.link:hover {
  background: none;
  color: var(--accent-hover);
  text-decoration: underline;
}

button.link:where(:enabled:active) {
  background: none;
  box-shadow: none;
  color: var(--accent-pressed);
}

button.link.danger {
  color: var(--bad);
}

/* Buttons of which one is chosen, side by side as one control. */
.toggles {
  display: inline-flex;
}

/* Square where two buttons meet. Of-type rather than child: x-for leaves its template first. */
.toggles button {
  border-radius: 0;
  margin-left: -1px;
}

.toggles button:first-of-type {
  border-top-left-radius: 6px;
  border-bottom-left-radius: 6px;
  margin-left: 0;
}

.toggles button:last-of-type {
  border-top-right-radius: 6px;
  border-bottom-right-radius: 6px;
}

/* In a group, a chosen button's border goes over the neighbours' it overlaps. */
.toggles button[aria-pressed='true'] {
  position: relative;
  z-index: 1;
}

button[aria-pressed='true'],
button[aria-pressed='true']:where(:enabled:hover) {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
  font-weight: 550;
}

/* ── icon buttons ───────────────────────────────────────────────────────── */

/*
  An action on a thing is a button with a picture of what it does, its words in title and
  aria-label: a bin rather than "Remove". The pictures are masks over the text's colour, so they
  follow the theme and the button's state; a picture's own colours could not.
*/
.icons {
  display: inline-flex;
  gap: 0.15rem;
  align-items: center;
}

button.icon,
a.icon {
  display: inline-grid;
  place-items: center;
  flex: none;
  box-sizing: border-box;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  vertical-align: middle;
  cursor: pointer;
}

button.icon::before,
a.icon::before {
  content: '';
  display: block;
  width: 1.1rem;
  height: 1.1rem;
  background: currentColor;
  mask: var(--icon) center / contain no-repeat;
}

button.icon.small {
  width: 1.5rem;
  height: 1.5rem;
}

button.icon.small::before {
  width: 0.9rem;
  height: 0.9rem;
}

button.icon:where(:enabled:hover),
a.icon:hover {
  border-color: var(--line);
  background: var(--tint);
  color: var(--ink);
}

button.icon:where(:enabled:active) {
  background: var(--tint-strong);
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 30%);
}

/* Taking something away is red, as a destructive button is. */
button.icon.remove,
button.icon.remove:where(:enabled:hover) {
  color: var(--bad);
}

/* The chevron that folds what it heads, pointing down while open. */
.icon.chevron {
  color: var(--ink);
  transition: transform var(--motion) var(--motion-ease);
}

.icon.chevron.folded {
  transform: rotate(-90deg);
}

/* The pictures, one for each action, for whatever else draws one beside its words. */
:root {
  --icon-remove: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 6h18M8 6V4h8v2M19 6l-1 14H6L5 6M10 11v6M14 11v6'/%3E%3C/svg%3E");
  --icon-add: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E");
  --icon-duplicate: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='9' y='9' width='12' height='12' rx='2'/%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E");
  --icon-copy: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='8' y='2' width='8' height='4' rx='1'/%3E%3Cpath d='M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2'/%3E%3C/svg%3E");
  --icon-export: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 3v12M7 10l5 5 5-5M5 21h14'/%3E%3C/svg%3E");
  --icon-send: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 2 11 13M22 2l-7 20-4-9-9-4z'/%3E%3C/svg%3E");
  --icon-retire: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='3' width='20' height='5' rx='1'/%3E%3Cpath d='M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8M10 12h4'/%3E%3C/svg%3E");
  --icon-restore: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11'/%3E%3C/svg%3E");
  --icon-retry: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 12a9 9 0 1 1-9-9c2.5 0 4.9 1 6.7 2.7L21 8M21 3v5h-5'/%3E%3C/svg%3E");
  --icon-edit: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M17 3a2.85 2.85 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5zM15 5l4 4'/%3E%3C/svg%3E");
  --icon-save: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
  --icon-preview: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z'/%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3C/svg%3E");
  --icon-reverse: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7.5h15M15 3.75 18.75 7.5 15 11.25M20 16.5H5M9 12.75 5.25 16.5 9 20.25'/%3E%3C/svg%3E");
  --icon-nest: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 10l5 5-5 5M4 4v7a4 4 0 0 0 4 4h12'/%3E%3C/svg%3E");
  --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18 6 6 18M6 6l12 12'/%3E%3C/svg%3E");
  --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

.icon.remove {
  --icon: var(--icon-remove);
}

.icon.add {
  --icon: var(--icon-add);
}

.icon.duplicate {
  --icon: var(--icon-duplicate);
}

.icon.copy {
  --icon: var(--icon-copy);
}

.icon.export {
  --icon: var(--icon-export);
}

.icon.send {
  --icon: var(--icon-send);
}

.icon.retire {
  --icon: var(--icon-retire);
}

.icon.restore {
  --icon: var(--icon-restore);
}

.icon.retry {
  --icon: var(--icon-retry);
}

.icon.edit {
  --icon: var(--icon-edit);
}

.icon.save {
  --icon: var(--icon-save);
}

.icon.preview {
  --icon: var(--icon-preview);
}

.icon.reverse {
  --icon: var(--icon-reverse);
}

.icon.nest {
  --icon: var(--icon-nest);
}

.icon.close {
  --icon: var(--icon-close);
}

.icon.chevron {
  --icon: var(--icon-chevron);
}
.loading-spinner {
  width: 1.1rem;
  height: 1.1rem;
  box-sizing: border-box;
  border: 2px solid var(--line-strong);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: geo-spin 0.8s linear infinite;
}

@keyframes geo-spin {
  to {
    transform: rotate(360deg);
  }
}

@media (prefers-reduced-motion: reduce) {
  .loading-spinner {
    animation-duration: 3s;
  }
}

/* ── tables and badges ──────────────────────────────────────────────────── */

.table {
  overflow-x: auto;
}

table {
  width: 100%;
  border-collapse: collapse;
  background: var(--surface);
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

th,
td {
  padding: 0.35rem 0.6rem;
  border-bottom: 1px solid var(--line);
  text-align: left;
  vertical-align: top;
}

th {
  background: var(--surface);
  color: var(--muted);
  font-weight: 600;
}

th.num,
td.num {
  text-align: right;
}

tr.failed td {
  color: var(--bad);
}

/* A column head that sorts its table: the head's own text, with an arrow that says which way. */
button.sort {
  display: inline-flex;
  gap: 0.3rem;
  align-items: baseline;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-weight: inherit;
  white-space: nowrap;
}

th[aria-sort='ascending'],
th[aria-sort='descending'] {
  color: var(--ink);
}

button.sort .arrow {
  font-size: 0.75em;
}

th[aria-sort='none'] button.sort .arrow {
  opacity: 0.35;
}

button.sort:where(:enabled:hover, :enabled:active) {
  background: none;
  box-shadow: none;
  color: var(--ink);
}

button.sort:where(:enabled:active) {
  opacity: 0.7;
}

/* A name that wraps reads from the left, as the column does. */
td button.link {
  text-align: left;
}

tr.clickable {
  cursor: pointer;
}

tr.clickable:hover td {
  background: var(--tint);
}

/*
  A table with a scroll container of its own keeps its head at the top of it. The shadow stands
  in for the bottom border, which a collapsed border leaves behind when the head sticks.
*/
.table.scroll {
  max-height: calc(100vh - 9rem);
  overflow: auto;
}

.table.scroll thead th {
  position: sticky;
  top: 0;
  z-index: 1;
  box-shadow: inset 0 -1px 0 var(--line);
}

/*
  The page's own tables keep their heads below the header on a wide screen, where it is fixed.
  Only there do they give up scrolling sideways, which would make their box the one the head
  sticks to; a narrow screen has no fixed header and scrolls them instead.
*/
@media (min-width: 48.01rem) {
  .table:not(.scroll, .float .table, .leaflet-container .table) {
    overflow: visible;
  }

  .table:not(.scroll, .float .table, .leaflet-container .table) thead th {
    position: sticky;
    top: var(--header-height);
    z-index: 1;
    box-shadow: inset 0 -1px 0 var(--line);
  }
}

.badge {
  display: inline-block;
  padding: 0.05rem 0.5rem;
  border-radius: 999px;
  background: var(--tint-strong);
  color: var(--muted);
  font-size: 0.8rem;
}

.badge.added,
.badge.good {
  background: color-mix(in oklab, var(--good), transparent 84%);
  color: var(--good);
}

.badge.replaced,
.badge.warn {
  background: color-mix(in oklab, var(--warn), transparent 84%);
  color: var(--warn);
}

.badge.failed,
.badge.bad {
  background: color-mix(in oklab, var(--bad), transparent 84%);
  color: var(--bad);
}

/* ── the notice ─────────────────────────────────────────────────────────── */

/*
  What the last action came to, fixed to the window so a button far down the page still shows
  it: one toast at the top centre, below the header.
*/
.toast {
  position: fixed;
  z-index: 1100;
  top: calc(var(--header-height) + 0.75rem);
  left: 50%;
  transform: translateX(-50%);
  box-sizing: border-box;
  width: max-content;
  max-width: min(32rem, calc(100vw - 2rem));
  display: flex;
  align-items: flex-start;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem 0.6rem 0.9rem;
  border: 1px solid var(--line);
  border-left: 4px solid var(--good);
  border-radius: 6px;
  background: var(--raised);
  color: var(--ink);
  box-shadow: var(--shadow);
}

.toast.failed {
  border-color: color-mix(in oklab, var(--bad), transparent 55%);
  border-left-color: var(--bad);
}

/* The cross beside the sentence, at the height of its first line. */
.toast button.icon {
  margin: -0.1rem -0.25rem -0.1rem 0;
}

body.map-page .toast {
  top: calc(var(--header-height) + var(--float-gap));
}

@media (max-width: 48rem) {
  /* Below the menu button, which keeps the top right corner. */
  .toast,
  body.map-page .toast {
    top: calc(44px + 2 * var(--float-gap));
  }
}

/* ── dialogs ────────────────────────────────────────────────────────────── */

/* Every question a page asks is asked in a dialog of its own, never confirm() or alert(). */
dialog.question {
  z-index: 1200;
  box-sizing: border-box;
  width: min(30rem, calc(100vw - 2rem));
  padding: 1.25rem 1.25rem 1rem;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--raised);
  color: var(--ink);
  box-shadow: var(--shadow);
}

/*
  A dialog rises into place over a darkening page, and sinks back as it closes. Display and the
  top layer wait for the fade to end; a browser without allow-discrete closes it at once.
*/
dialog.question {
  transition:
    opacity var(--motion) var(--motion-ease),
    transform var(--motion) var(--motion-ease),
    overlay var(--motion) allow-discrete,
    display var(--motion) allow-discrete;
}

dialog.question:not([open]) {
  opacity: 0;
  transform: translateY(0.75rem) scale(0.98);
}

@starting-style {
  dialog.question[open] {
    opacity: 0;
    transform: translateY(0.75rem) scale(0.98);
  }
}

dialog.question::backdrop {
  background: rgb(0 0 0 / 0%);
  transition:
    background-color var(--motion) var(--motion-ease),
    overlay var(--motion) allow-discrete,
    display var(--motion) allow-discrete;
}

dialog.question[open]::backdrop {
  background: rgb(0 0 0 / 55%);
}

@starting-style {
  dialog.question[open]::backdrop {
    background: rgb(0 0 0 / 0%);
  }
}

/* What the dialog asks for, when it asks for a line of text, such as the name of something new. */
dialog.question label.field {
  margin-bottom: 1rem;
}

dialog.question label.field input {
  width: 100%;
}

/* Why what was asked for could not be done; the dialog stays open, to change it and try again. */
dialog.question p.problem {
  margin-top: -0.25rem;
}

/* A dialog of a page's own, with more in it than a question, such as a table to choose from. */
dialog.question.wide {
  width: min(56rem, calc(100vw - 2rem));
}

dialog.question h2 {
  margin: 0 0 0.5rem;
}

dialog.question p {
  margin: 0 0 1rem;
  line-height: 1.45;
}

dialog.question textarea {
  width: 100%;
  margin-bottom: 1rem;
  font-family: var(--mono);
  font-size: 0.85rem;
}

dialog.question .actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.6rem;
}

/* ── maps and their floating boxes ──────────────────────────────────────── */

/* Where the map is the page, it fills the window below the header, and the page never scrolls. */
body.map-page {
  height: 100vh;
  height: 100dvh;
  box-sizing: border-box;
  overflow: hidden;
}

body.map-page > main {
  position: relative;
  box-sizing: border-box;
  height: 100%;
  max-width: none;
  padding: 0;
}

.map-stage,
.map-stage > .map-fill {
  position: absolute;
  inset: 0;
}

/*
  The controls lie over the map in boxes. The overlay lets clicks through to the map, and each
  box takes them back, so the map between the boxes still pans.
*/
.overlay {
  position: absolute;
  inset: 0;
  z-index: 1010;
  pointer-events: none;
}

.float {
  position: absolute;
  box-sizing: border-box;
  max-width: calc(100% - 2 * var(--float-gap));
  max-height: calc(100% - var(--float-gap) - var(--float-bottom));
  overflow: auto;
  overscroll-behavior: contain;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: var(--raised);
  color: var(--ink);
  box-shadow: var(--shadow);
  pointer-events: auto;
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}

.float.top-right,
.float.top,
.float.right {
  top: var(--float-gap);
}

.float.top-right,
.float.bottom-right,
.float.right {
  right: var(--float-gap);
}

.float.bottom-left,
.float.bottom-right,
.float.bottom {
  bottom: var(--float-bottom);
}

.float.bottom-left,
.float.bottom {
  left: var(--float-gap);
}

/* Leaflet's zoom keeps the top left corner, so a box there starts beside it. */
.float.top-left {
  top: var(--float-gap);
  left: calc(var(--float-gap) + 44px);
}

.float.top,
.float.bottom {
  right: var(--float-gap);
}

.float.right {
  bottom: var(--float-bottom);
  width: var(--side-width);
}

/*
  A box down the right takes that side from top to bottom, and Leaflet's attribution moves to its
  left rather than holding the box above itself; folded, the box is its head, and the attribution
  goes back to the corner. --credit-right is set on the stage with no weight, so a page whose box
  does not lie over its map, or not down all of it, sets it back to 0 with any selector.
*/
@media (min-width: 48.01rem) {
  .float.right {
    bottom: var(--float-gap);
    max-height: none;
  }

  :where(.map-stage:has(> .overlay > .float.right:not(.folded))) {
    --credit-right: calc(var(--side-width) + 2 * var(--float-gap));
  }
}

.map-stage .leaflet-bottom.leaflet-right {
  right: var(--credit-right, 0px);
  transition: right var(--motion) var(--motion-ease);
}

.float.top {
  left: calc(var(--float-gap) + 44px);
}

/* A box's head names it and folds it; folded, the head is all there is of it. */
.float-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.6rem 0.75rem;
  background: var(--raised);
  border-bottom: 1px solid var(--line);
}

.float.folded .float-head {
  border-bottom: 0;
}

.float.folded > :not(.float-head) {
  display: none;
}

/* A box down an edge is as tall as its head once folded. */
.float.folded.right {
  bottom: auto;
}

.float-head > h2,
.float-head > strong {
  margin: 0;
  font-size: 1rem;
}

button.fold {
  flex: none;
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  margin-left: auto;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
}

button.fold:where(:enabled:hover) {
  background: var(--tint);
  color: var(--ink);
}

button.fold svg {
  width: 16px;
  height: 16px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform 0.15s;
}

.float.folded button.fold svg {
  transform: rotate(-90deg);
}

/* Inside a box, its cards are sections of it, parted by a line. */
.float .card {
  margin: 0;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: 0;
  background: none;
}

.float .card:last-child {
  border-bottom: 0;
}

.float table,
.float th {
  background: none;
}

@media (max-width: 48rem) {
  /*
    The menu button keeps the top right corner of a narrow screen and Leaflet's zoom the top
    left, and a box the width of the screen starts below both.
  */
  .float.top-right,
  .float.right {
    top: calc(64px + 2 * var(--float-gap));
  }

  .float.top-right,
  .float.right,
  .float.bottom-left,
  .float.bottom-right {
    left: var(--float-gap);
    right: var(--float-gap);
    width: auto;
  }
}

/* ── Leaflet, in the dark ───────────────────────────────────────────────── */

/* The tiles are never inverted or filtered; Leaflet's own controls are darkened instead. */
.leaflet-container {
  background: var(--surface);
  font: inherit;
  font-size: 0.85rem;
}

.leaflet-container a {
  color: var(--accent);
}

.leaflet-bar,
.leaflet-touch .leaflet-bar {
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  box-shadow: var(--shadow);
}

.leaflet-bar a,
.leaflet-touch .leaflet-bar a {
  border-bottom-color: var(--line);
  background-color: var(--raised);
  color: var(--ink);
}

.leaflet-bar a:hover,
.leaflet-bar a:focus,
.leaflet-touch .leaflet-bar a:hover {
  background-color: color-mix(in oklab, var(--raised), white 8%);
  color: var(--ink);
}

.leaflet-bar a.leaflet-disabled,
.leaflet-touch .leaflet-bar a.leaflet-disabled {
  background-color: var(--surface);
  color: var(--line-strong);
}

.leaflet-touch .leaflet-bar a:first-child {
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
}

.leaflet-touch .leaflet-bar a:last-child {
  border-bottom-left-radius: 5px;
  border-bottom-right-radius: 5px;
}

.leaflet-control-attribution,
.leaflet-container .leaflet-control-attribution {
  background: rgb(15 19 22 / 78%);
  color: var(--muted);
}

.leaflet-control-scale-line {
  border-color: var(--muted);
  background: rgb(15 19 22 / 70%);
  color: var(--ink);
  text-shadow: none;
}

.leaflet-control-layers,
.leaflet-touch .leaflet-control-layers {
  border: 1px solid var(--line-strong);
  background: var(--raised);
  color: var(--ink);
}

.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: var(--raised);
  color: var(--ink);
  box-shadow: var(--shadow);
}

.leaflet-popup-content-wrapper {
  border: 1px solid var(--line);
  border-radius: var(--radius);
}

.leaflet-container a.leaflet-popup-close-button {
  color: var(--muted);
}

.leaflet-container a.leaflet-popup-close-button:hover {
  color: var(--ink);
}

.leaflet-tooltip {
  border-color: var(--line);
  background: var(--raised);
  color: var(--ink);
  box-shadow: var(--shadow);
}

.leaflet-tooltip-top::before {
  border-top-color: var(--raised);
}

.leaflet-tooltip-bottom::before {
  border-bottom-color: var(--raised);
}

.leaflet-tooltip-left::before {
  border-left-color: var(--raised);
}

.leaflet-tooltip-right::before {
  border-right-color: var(--raised);
}
