/* The map view. Loaded only where a map is drawn — see mapAssets() in the
   router — because the three stylesheets above it already reach every page in
   the product and a fourth for a view most requests never open would be a
   request spent on nothing.

   No colour is declared here. Land, boundaries and labels take existing
   surface, line and text steps; water is the one hue the map needed and it is
   a token in tokens.css; dots take --accent, which is what makes them render
   in a venue's own palette on a themed page and in ours on the feed, with no
   second rule anywhere. */

.map-view {
  display: grid;
  gap: var(--space-5);
}

/* ------------------------------------------------------------- day slider */

/* Links, all of them. Without JavaScript this is the whole control: two steps
   and seven days, each a real address. map.js turns it into a slider that
   swaps without a page load and takes nothing away if it never runs. */
.day-slider {
  display: flex;
  align-items: stretch;
  gap: var(--space-2);
  background: var(--surface-10);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: var(--space-2);
}

.day-list {
  display: flex;
  flex: 1;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.day-list li {
  flex: 1;
}

.day-step,
.day {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  border-radius: 8px;
  text-decoration: none;
  color: var(--text-muted);
  font-size: var(--text-2xs);
  line-height: 1.1;
}

.day-step {
  min-width: var(--tap);
  font-size: var(--text-base);
}

.day-step:hover,
.day:hover {
  background: var(--surface-15);
  color: var(--text);
}

.day-number {
  font-size: var(--text-base);
  font-weight: var(--wght-medium);
  color: var(--text);
}

/* Today is marked, not merely coloured — the same rule the month grid carries,
   for the same reason: no state on this surface may be carried by colour
   alone. The underline is the mark, the tone reinforces it. */
.day-today .day-number {
  text-decoration: underline;
  text-underline-offset: 3px;
}

/* The shown day is filled, and it is also the one carrying aria-current, so
   the state exists for a screen reader independently of how it looks. */
.day-current {
  background: var(--accent);
}

.day-current .day-number,
.day-current .day-weekday {
  color: var(--invert-text);
}

.day-current:hover {
  background: var(--accent);
}

.day-current:hover .day-number {
  color: var(--invert-text);
}

/* -------------------------------------------------------------- the drawing */

.map-figure {
  position: relative;
  margin: 0;
  background: var(--surface-05);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: var(--space-3);
  overflow: hidden;
}

.map {
  display: block;
  width: 100%;
  /* The outline decides its own height through the viewBox, but a tall city on
     a short viewport must not push the list off the screen entirely — the list
     is what the map exists to point into. */
  max-height: 70vh;
}

/* ------------------------------------------------------- the vector basemap */

/* The SVG drawing is the substrate: it is what renders with JavaScript off,
   what renders when data/basemap.pmtiles is not there, and what is on screen
   during the first paint before any tile arrives. The renderer replaces it
   only once it has actually painted something — which is why there is no grey
   rectangle at any point, and why a renderer that fails to start leaves a
   working map behind rather than an empty box. */
/* Qualified by the figure, and that is not tidiness. MapLibre's own stylesheet
   carries `.maplibregl-map { position: relative }`, it is loaded after this
   file, and one class beats one class by order — so a bare `.map-gl` lost, the
   canvas fell out of its `inset: 0` and collapsed to zero height. Two classes
   is the smallest thing that wins without touching their file or reordering
   ours. */
.map-figure .map-gl {
  position: absolute;
  inset: 0;
  opacity: 0;
  /* Not decoration. The canvas covers the SVG from the moment it is created,
     and an invisible element still takes clicks — so until the renderer has
     actually painted, every dot underneath it would be unreachable. If the
     tiles never arrive this is what leaves a working map behind rather than a
     transparent sheet over one. */
  pointer-events: none;
  transition: opacity var(--dur-surface) var(--ease);
}

.map-figure[data-basemap-ready] .map-gl {
  opacity: 1;
  pointer-events: auto;
}

/* Three states, one rule. `pending` is set the instant the renderer claims a
   figure and `ready` when it has painted — the drawing goes at the first, not
   the second, so nobody watches one map turn into another. It occupies its
   space either way, which keeps the panel from collapsing and jumping.

   `data-map-boot` on the root is the third, and it covers the stretch the
   other two could not: `pending` is set by a deferred script sitting behind a
   quarter of a megabyte of MapLibre, and the browser paints long before that —
   measured at 72 ms against 266 ms on localhost with a warm cache, so the
   drawing appeared and then vanished. The flag is raised by an inline script
   in the head, before any paint, and dropped at DOMContentLoaded once every
   deferred script has had its turn. See MAP_BOOT in server/web/map-view.js.

   It is qualified by `[data-basemap]`: a figure with no archive behind it is
   never getting a renderer, and hiding that one would be hiding the whole map.
   With JavaScript off the flag is never raised at all. */
[data-map-boot] .map-figure[data-basemap] .map,
.map-figure[data-basemap-pending] .map,
.map-figure[data-basemap-ready] .map {
  visibility: hidden;
}

/* The figure has padding so the SVG breathes inside its frame; the canvas is
   the frame. Reserved by aspect-ratio rather than by a height, so the box the
   tiles arrive into is the box the SVG already occupied. */
.map-figure:has(.map-gl) {
  padding: 0;
}

.map-figure:has(.map-gl) .map {
  padding: var(--space-3);
  box-sizing: border-box;
}

/* Dots over the canvas are DOM elements, never map symbols — which is what
   keeps them real links, keeps the shared hover card on them and keeps them on
   --accent with no second rule. Sized in pixels here because they no longer
   live in a viewBox. */
.map-marker {
  display: grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--surface-15);
  color: var(--invert-text);
  font-family: var(--font-sans);
  font-size: var(--text-3xs);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
}

.map-marker[data-count] {
  width: 20px;
  height: 20px;
}

.map-marker:hover,
.map-marker:focus-visible {
  border-color: var(--text);
}

/* MapLibre ships its own control chrome. Only the two things that would
   otherwise read as somebody else's design are touched — the rest of its
   stylesheet is left alone rather than fought with. */
.maplibregl-ctrl-group {
  background: var(--surface-10);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: none;
}

.maplibregl-ctrl-group button + button {
  border-top: 1px solid var(--line);
}

.maplibregl-ctrl-group button .maplibregl-ctrl-icon {
  filter: var(--map-ctrl-filter);
}

.map-land {
  fill: var(--surface-15);
  stroke: var(--line-strong);
  stroke-width: 3;
  stroke-linejoin: round;
}

/* ------------------------------------------------------------ the region */

/* City borders in the accent on the overview, because at that scale the border
   *is* the content — there are no districts and no streets, and a city is the
   thing a reader is picking. Inside a city map the border goes quiet again and
   the accent belongs to the dots. Same token, opposite jobs, decided by which
   of the two is the smallest unit on screen. */
.map-land-city {
  stroke: var(--accent);
  stroke-width: 4;
}

.map-city:hover .map-land-city,
.map-city:focus-visible .map-land-city {
  fill: var(--surface-20);
  stroke-width: 6;
}

.map-city:focus {
  outline: none;
}

/* A city with nothing on tonight is still a link and still readable — it is
   just not where the reader is being pointed. */
.map-city:not(.map-city-busy) .map-land-city {
  stroke: var(--line-strong);
}

.map-city-name {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 26px;
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--surface-15);
  stroke-width: 6;
  stroke-linejoin: round;
}

.map-city-busy .map-city-name {
  fill: var(--text);
}

/* The regional rivers are deliberately not clipped to the city outlines: the
   stretch between Neuss and Köln belongs to no city and is exactly what stops
   four outlines reading as four unrelated islands. Thinner than inside a city,
   because the same river is a third of the units wide at this scale. */
.map-river-region {
  stroke-width: 4;
}

/* Rivers under the water polygons and in the same fill. Where a river has both
   — the Rhine does — nothing shows twice; where it has only a line, the Wupper
   still appears. */
.map-river {
  fill: none;
  stroke: var(--map-water);
  stroke-width: 7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.map-water {
  fill: var(--map-water);
  stroke: none;
}

.map-districts path {
  fill: none;
  stroke: var(--line);
  stroke-width: 1.5;
}

/* Sparse by construction: the renderer only emits a name for a district that
   is actually carrying programme. Fifty names do not fit at 375px, and the
   overlap logic that would make them fit costs more than the map. */
/* Sized in viewBox units, and the viewBox is 1000 wide however narrow the
   screen is — so 30 units is roughly 11px at 375px, which is where this has to
   be legible. The halo is the land colour painted under the glyph rather than
   a box behind it: district boundaries run everywhere and a name crossing one
   is otherwise unreadable exactly where two districts meet. */
.map-labels text {
  fill: var(--text-muted);
  font-family: var(--font-sans);
  font-size: 30px;
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke;
  stroke: var(--surface-15);
  stroke-width: 6;
  stroke-linejoin: round;
}

/* -------------------------------------------------------------------- dots */

.map-hit {
  fill: transparent;
  stroke: none;
}

.map-dot circle:not(.map-hit) {
  fill: var(--accent);
  /* The land, not the figure's background: the ring exists to separate the dot
     from what is directly under it, and what is under it is the city. */
  stroke: var(--surface-15);
  stroke-width: 3;
}

.map-dot:hover circle:not(.map-hit),
.map-dot:focus circle:not(.map-hit) {
  stroke: var(--text);
}

.map-dot:focus {
  outline: none;
}

.map-dot:focus-visible circle:not(.map-hit) {
  stroke: var(--text);
  stroke-width: 5;
}

.map-dot-count {
  fill: var(--invert-text);
  font-family: var(--font-sans);
  font-size: 17px;
  font-weight: var(--wght-medium);
  text-anchor: middle;
  dominant-baseline: central;
  pointer-events: none;
}

.map-caption,
.map-credit {
  color: var(--text-faint);
  font-size: var(--text-2xs);
}

.map-caption {
  display: flex;
  gap: var(--space-3);
  padding-top: var(--space-2);
}

/* ------------------------------------------------------- the zoom controls */

/* Built by map.js and never server-rendered: a "+" that does nothing without a
   script is a dead control. The no-JS way to zoom exists and is better anyway —
   a city on the region map is a link to its own map, and the caption links back
   out. This is the continuous version of the same move. */
.map-zoom {
  position: absolute;
  top: var(--space-3);
  right: var(--space-3);
  display: grid;
  gap: var(--space-1);
}

/* One set of zoom controls, not two. Where the renderer took over it brings
   its own, and these were driving a viewBox nobody can see any more. */
.map-figure[data-basemap-ready] .map-zoom {
  display: none;
}

.map-zoom button {
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  background: var(--surface-10);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
}

.map-zoom button:hover {
  background: var(--surface-20);
}

.map-zoom button[disabled] {
  color: var(--text-faint);
  cursor: default;
}

/* Panning is a drag, and a drag that selects the district names underneath it
   reads as broken. Only while a script is actually driving the map. */
.map-figure[data-zoomable] .map {
  touch-action: none;
  cursor: grab;
}

.map-figure[data-panning] .map {
  cursor: grabbing;
  user-select: none;
}

.map-credit {
  margin: 0;
}

/* ----------------------------------------------------------- the target row */

/* A dot is an anchor to its row in the list underneath, and this is what makes
   that visible without a line of JavaScript — the third use of :target on this
   site after the share sheet and the lightbox, deliberately the same mechanism
   rather than a fourth idea. */
.tip:target {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 8px;
}

/* Or the row lands under the chrome it just scrolled past. Both pieces are
   sticky at every width as of 2026-08-13: the header (--header-h) and the date
   heading above the row. The heading's height is not a token, so this clears
   the header exactly and the heading approximately, which is the difference
   between a highlighted row you can see and one you have to scroll up for. */
.tip {
  scroll-margin-top: calc(var(--header-h) + var(--space-7));
}

/* The hover card is the month grid's, mounted a second time inside the figure.
   calendar.css positions and styles it; only the anchoring context is here. */
@media (max-width: 30rem) {
  .day-weekday {
    display: none;
  }
}

/* ------------------------------------------------- names over the basemap */

/* The basemap's own place labels are switched off in the style: they need an
   SDF glyph set, they arrive in whatever language OpenStreetMap holds, and they
   would be set in a font that is not this product's. These are ours — the same
   two names the drawing underneath carries, in the same type as everything
   else, positioned by the renderer.

   pointer-events: none throughout. A name that swallowed a click on a dot
   behind it would be a label breaking the only control on the map. */
.map-name {
  pointer-events: none;
  font-family: var(--font-sans);
  line-height: 1;
  white-space: nowrap;
  /* The land tone laid around the glyph rather than a box behind it: a name
     crossing a road or a boundary is otherwise unreadable exactly where two
     things meet, which is where names tend to sit.

     Four shadows, not `-webkit-text-stroke`. A stroke needs `paint-order` to
     sit *under* the fill, and `paint-order` on HTML text is not reliable
     outside Chromium — where it is ignored, the halo paints over the letters
     and the label disappears entirely. */
  text-shadow:
    0 1px 2px var(--surface-15), 0 -1px 2px var(--surface-15),
    1px 0 2px var(--surface-15), -1px 0 2px var(--surface-15);
}

.map-name-city {
  font-size: var(--text-sm);
  font-weight: var(--wght-medium);
  color: var(--text);
  letter-spacing: var(--track-title);
}

.map-name-district {
  font-size: var(--text-2xs);
  color: var(--text-muted);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}

/* The hover card is a sibling of the canvas, and MapLibre's container creates
   its own stacking context. Without this the card renders behind the map it
   describes. */
.map-figure .cal-card {
  z-index: 3;
}

/* Three bands, because the two kinds of name earn their place at different
   scales. A city name is what orients you when four of them are on screen; a
   quarter name only means anything once the quarter fills a decent part of it.
   Both exist the whole time — this decides when there is room. */
.map-figure[data-zoom-band="far"] .map-name-district,
.map-figure[data-zoom-band="mid"] .map-name-district {
  display: none;
}

.map-figure[data-zoom-band="close"] .map-name-city {
  display: none;
}
