:root {
  --bg: #fbfaf9;
  --surface: #ffffff;
  --ink: #1c1917;
  --ink-soft: #57534e;
  --line: #e7e5e4;
  --accent: #c2410c;
  --warn: #b45309;
  --ok: #15803d;
  --radius: 8px;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: ui-sans-serif, system-ui, -apple-system, "Helvetica Neue", sans-serif;
  color: var(--ink);
  background: var(--bg);
  line-height: 1.5;
}

main { max-width: 1100px; margin: 0 auto; padding: 1.5rem 1.25rem 4rem; }

h1 { font-size: 1.5rem; margin: 0 0 .25rem; }
h2 { font-size: 1.05rem; margin: 0 0 .5rem; }
code { background: #f5f5f4; padding: .1em .3em; border-radius: 4px; font-size: .9em; }

/* --- top bar ------------------------------------------------------------- */
.topbar {
  display: flex; align-items: center; gap: 1.25rem;
  padding: .7rem 1.25rem;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  flex-wrap: wrap;
}
.brand { font-weight: 650; text-decoration: none; color: var(--ink); }
.topbar nav { display: flex; gap: 1rem; }
.topbar nav a { color: var(--ink-soft); text-decoration: none; font-size: .9rem; }
.topbar nav a:hover { color: var(--accent); }

/* --- spend strip ---------------------------------------------------------
   Kept in the header at all times: Google imagery can't be stored, so cost
   recurs per playback and running spend is worth keeping an eye on.
*/
.spend-strip { margin-left: auto; min-width: 260px; }
.spend-strip__bar {
  height: 5px; background: var(--line); border-radius: 999px; overflow: hidden;
}
.spend-strip__fill { height: 100%; background: var(--ok); transition: width .3s; }
.spend-strip--warn .spend-strip__fill { background: var(--warn); }
.spend-strip__text {
  display: block; font-size: .74rem; color: var(--ink-soft);
  margin-top: .3rem; font-variant-numeric: tabular-nums;
}

/* --- flashes ------------------------------------------------------------- */
.flash { padding: .7rem 1.25rem; font-size: .9rem; }
.flash.notice { background: #f0fdf4; color: #14532d; border-bottom: 1px solid #bbf7d0; }
.flash.alert { background: #fef2f2; color: #7f1d1d; border-bottom: 1px solid #fecaca; }

/* --- buttons & forms ----------------------------------------------------- */
.button, button, input[type="submit"] {
  font: inherit; font-size: .9rem;
  padding: .45rem .9rem;
  border: 1px solid var(--accent);
  background: var(--accent); color: #fff;
  border-radius: var(--radius); cursor: pointer; text-decoration: none;
  display: inline-block;
}
.button:hover, button:hover { filter: brightness(1.08); }
.button--quiet {
  background: transparent; color: var(--ink-soft); border-color: var(--line);
}
.form { max-width: 34rem; }
.field { margin-bottom: 1.1rem; display: flex; flex-direction: column; gap: .3rem; }
.field label { font-weight: 600; font-size: .85rem; }
.field input[type="text"], .field select, .field input[type="file"] {
  font: inherit; padding: .45rem .55rem;
  border: 1px solid var(--line); border-radius: var(--radius); background: var(--surface);
  color: var(--ink);
}
.hint { font-size: .78rem; color: var(--ink-soft); margin: .15rem 0 0; }

/* --- tables -------------------------------------------------------------- */
.routes { width: 100%; border-collapse: collapse; font-size: .9rem; }
.routes th {
  text-align: left; font-size: .72rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-soft);
  border-bottom: 1px solid var(--line); padding: .45rem .5rem;
}
.routes td { padding: .55rem .5rem; border-bottom: 1px solid var(--line); }
.routes td:first-child a { color: var(--accent); font-weight: 600; text-decoration: none; }

.status {
  font-size: .72rem; padding: .12rem .45rem; border-radius: 999px;
  background: #f5f5f4; color: var(--ink-soft);
}
.status--ready { background: #dcfce7; color: #14532d; }
.status--probed { background: #dbeafe; color: #1e3a8a; }
.status--failed { background: #fee2e2; color: #7f1d1d; }

/* --- callouts ------------------------------------------------------------ */
.callout {
  background: var(--surface); border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius); padding: .9rem 1rem; margin: 1.25rem 0;
  font-size: .9rem;
}
.callout--cost { border-left-color: var(--warn); }
.empty { text-align: center; padding: 3rem 1rem; color: var(--ink-soft); }

/* Collapsed until asked for: replacing the geometry is a deliberate act, not a
   step in the import flow the rest of this page describes. */
.route-update > summary { cursor: pointer; font-weight: 650; }
.route-update[open] > summary { margin-bottom: .75rem; }
.route-update .form { margin: 0; }

/* --- import progress -----------------------------------------------------
   A stage list rather than a progress bar. The pipeline's slow stage writes its
   rows a source at a time, so a bar would be animating a number nobody measured;
   naming each stage and its real counts is both honest and more useful.
*/
.import {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: 1rem 1.1rem; margin: 1.25rem 0;
}
.import__header { display: flex; align-items: baseline; gap: .6rem; margin-bottom: .3rem; }
.import__header h2 { margin: 0; font-size: 1rem; }
.import__working { font-size: .78rem; color: var(--ink-soft); }
/* Only animates while a job is actually running, since the element is absent
   otherwise — no perpetual spinner. */
.import__working::after {
  content: ""; display: inline-block; margin-left: .3rem;
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
  animation: import-pulse 1.1s ease-in-out infinite;
}
@keyframes import-pulse { 0%, 100% { opacity: .25 } 50% { opacity: 1 } }

.stages { list-style: none; margin: .6rem 0 0; padding: 0; }
.stage { display: flex; gap: .7rem; padding: .5rem 0; border-top: 1px solid var(--line); }
.stage:first-child { border-top: 0; }
.stage__marker {
  flex: 0 0 1.4rem; height: 1.4rem; margin-top: .05rem;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; font-size: .78rem; font-weight: 700;
  border: 1px solid var(--line); color: var(--ink-soft);
}
.stage__body { min-width: 0; }
.stage__label { margin: 0; font-size: .9rem; font-weight: 600; }
.stage__state {
  margin-left: .4rem; font-weight: 400; font-size: .7rem; text-transform: uppercase;
  letter-spacing: .04em; color: var(--ink-soft);
}
.stage__detail { margin: .1rem 0 0; font-size: .82rem; color: var(--ink-soft); }
.stage__note { margin: .3rem 0 0; font-size: .78rem; color: var(--ink-soft); }
.stage form { margin-top: .5rem; }

.stage--done .stage__marker { background: var(--ok); border-color: var(--ok); color: #fff; }
.stage--active .stage__marker { border-color: var(--accent); color: var(--accent); }
.stage--active .stage__marker::after {
  content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent);
  animation: import-pulse 1.1s ease-in-out infinite;
}
.stage--action .stage__marker { background: var(--accent); border-color: var(--accent); color: #fff; }
.stage--stalled .stage__marker { background: var(--warn); border-color: var(--warn); color: #fff; }
.stage--stalled .stage__label, .stage--action .stage__label { color: var(--ink); }
/* Waiting and skipped stages stay quiet: they're context, not calls to action. */
.stage--waiting, .stage--skipped { opacity: .7; }

.coverage { display: inline-flex; gap: 1rem; flex-wrap: wrap; }
.coverage__item { font-size: .85rem; }
.coverage__detail { color: var(--ink-soft); font-size: .78rem; }

/* --- road class legend ---------------------------------------------------
   Colours come from RoadClass, injected inline, so there is exactly one place
   to change them and the map and legend cannot disagree.
*/
.road-legend {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius); padding: .75rem 1rem; margin-top: 1rem;
}
.road-legend__items {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .35rem 1.25rem;
}
.road-legend__item { display: flex; align-items: center; gap: .4rem; font-size: .85rem; }
.road-legend__swatch {
  width: 1rem; height: .35rem; border-radius: 2px; flex: none;
}
.road-legend__detail { color: var(--ink-soft); font-size: .78rem; }
/* A licence condition, so it stays legible rather than shrinking away. */
.road-legend__attribution {
  margin: .6rem 0 0; font-size: .75rem; color: var(--ink-soft);
}

.route-header {
  display: flex; justify-content: space-between; align-items: flex-start;
  gap: 1rem; flex-wrap: wrap; margin-bottom: .5rem;
}
.route-actions { display: flex; gap: .5rem; }
.meta { color: var(--ink-soft); font-size: .85rem; margin: 0; }

/* --- player --------------------------------------------------------------
   Two columns on wide screens: imagery and controls on the left, map on the
   right. Keeping the map visible while the timelapse runs is the point — the
   image tells you what the road looks like, the map tells you where you are.
*/
.player {
  margin-top: 1.25rem;
  display: grid; gap: .75rem;
  grid-template-columns: minmax(0, 1.9fr) minmax(0, 1fr);
  grid-template-areas: "stage map" "controls map";
  align-items: start;
}
.player__stage { grid-area: stage; }
.player__controls { grid-area: controls; }
.player__map { grid-area: map; }

@media (max-width: 820px) {
  .player {
    grid-template-columns: 1fr;
    grid-template-areas: "stage" "controls" "map";
  }
}

.player__stage {
  position: relative; background: #18181b; border-radius: var(--radius);
  overflow: hidden;
  /* 4:3 matches the Mapillary source; Google's 640x640 is
     cropped by object-fit rather than letterboxed. */
  aspect-ratio: 4 / 3; max-height: 55vh;
  display: flex; align-items: center; justify-content: center;
}
.player__stage img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* Attribution sits inside the same visual container as the imagery and must stay
   legible: Google requires it always visible and unobscured, and CC BY-SA needs
   its own credit. The gradient plus text shadow keeps contrast over both bright
   sky and dark tarmac. */
.player__attribution {
  position: absolute; bottom: 0; left: 0; right: 0;
  padding: .4rem .6rem;
  font-size: .78rem; color: #fff;
  background: linear-gradient(transparent, rgba(0, 0, 0, .68));
  text-shadow: 0 1px 2px rgba(0, 0, 0, .9);
}
.player__spinner, .player__error {
  position: absolute; color: #fafafa; font-size: .9rem; padding: .75rem 1rem;
  text-align: center;
}
.player__error { background: rgba(127, 29, 29, .92); border-radius: var(--radius); max-width: 80%; }

/* The readout gets a row of its own, below the transport controls.
   Sharing a line with the range input meant the seek bar was sized by whatever
   was left over, so the bar resized as the readout's text changed length —
   which it does constantly during playback, since road names and class labels
   vary. Anything on the bar's row therefore needs a width that doesn't depend
   on its label. */
.player__controls {
  display: flex; align-items: center; gap: .75rem; flex-wrap: wrap;
}
.player__controls input[type="range"] { flex: 1; min-width: 180px; }
.player__controls select { font: inherit; font-size: .85rem; padding: .3rem; }
/* Pinned because both labels change: the button toggles Play/Pause, and the
   select shows the chosen speed ("2 fps" through "15 fps"). */
.player__controls button { min-width: 5.5rem; text-align: center; }
.player__controls select { min-width: 6rem; }
.player__readout {
  /* No shrink, so it always wraps onto its own line rather than squeezing in
     beside the bar. */
  flex: 0 0 100%;
  font-size: .78rem; color: var(--ink-soft); font-variant-numeric: tabular-nums;
  /* Two lines held open unconditionally. One line fits the common readout, but a
     long name plus "A road dual carriageway" measures ~625px and the controls
     column is narrower than that at most widths — narrowest just above the 820px
     breakpoint, where it drops to 500px. Reserving the second line stops the
     legend and map jumping as frames advance, which is the same jitter as the
     resizing bar on the other axis. Also keeps the row from appearing out of
     nowhere on the first frame. */
  min-height: 2.4rem;
}
/* The way out to the panorama, last on the line. Accented rather than the browser's blue,
   and underlined only on hover, because it sits at the end of a run of metadata — one item
   in the list, not a call to action. nowrap keeps the label and its icon together when the
   readout wraps onto its second line. */
.player__readout a { color: var(--accent); text-decoration: none; white-space: nowrap; }
.player__readout a:hover, .player__readout a:focus-visible { text-decoration: underline; }
/* Sized in em and stroked in currentColor so the icon tracks the link's size and colour
   rather than being pinned to either. The baseline nudge is what stops it sitting low
   against .78rem text. */
.player__readout a svg {
  width: .95em; height: .95em; margin-left: .3em; vertical-align: -.12em;
}

.player__map, .route-map-only {
  border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--line); background: var(--surface);
}
/* Match the stage's height so the two columns line up. */
.player__map { aspect-ratio: 3 / 4; max-height: 55vh; min-height: 300px; }
@media (max-width: 820px) {
  .player__map { aspect-ratio: auto; height: 300px; }
}
.route-map-only { height: 420px; margin-top: 1rem; }
.map-error { padding: 1rem; color: var(--ink-soft); font-size: .85rem; }

.map-marker {
  width: 0; height: 0;
  border-left: 7px solid transparent; border-right: 7px solid transparent;
  border-bottom: 15px solid var(--accent);
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .4));
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #1c1917; --surface: #262322; --ink: #f5f5f4;
    --ink-soft: #a8a29e; --line: #3f3b39;
  }
  code { background: #3f3b39; }
  .status { background: #3f3b39; }
  .flash.notice { background: #14311f; color: #bbf7d0; border-color: #14532d; }
  .flash.alert { background: #3b1414; color: #fecaca; border-color: #7f1d1d; }
}
