/* ==========================================================================
   Spektral SMP — map theme
   --------------------------------------------------------------------------
   Brand: black / white / grey, with colour used only as an accent.
   The grey ramp below is lifted straight from spektral-logo.svg's own fill
   values, so the UI and the mark share one palette instead of running two.

   Loaded AFTER styles.css so it can override without editing it.
   On a Pl3xMap upgrade: drop in the new upstream styles.css untouched and
   just re-add the <link> to this file in index.html. Nothing here needs
   merging by hand.
   ========================================================================== */

:root {
  /* ---- greys, taken from the logo ---------------------------------------- */
  --spk-white:  #ffffff;  /* logo .cls-3 */
  --spk-g1:     #a7a7a7;  /* logo .cls-1 */
  --spk-g2:     #797979;  /* logo .cls-2 */
  --spk-g3:     #6e6e6e;  /* logo .cls-4 */
  --spk-g4:     #4c4c4c;  /* logo .cls-5 */
  --spk-black:  #000000;  /* logo, unclassed shapes */

  /* ---- surfaces ---------------------------------------------------------- */
  --spk-void:          #000000;
  --spk-panel:         #121212;
  --spk-panel-raised:  #1e1e1e;

  --spk-text:          var(--spk-white);
  --spk-text-dim:      var(--spk-g1);
  --spk-text-faint:    var(--spk-g3);

  --spk-line:          rgba(255, 255, 255, 0.11);
  --spk-line-strong:   rgba(255, 255, 255, 0.30);

  /* ---- the hint of colour ------------------------------------------------
     Deliberately confined to three places: the active sidebar tab's rail, the
     heading underline, and a featured entry's tile. The top ribbon was a
     fourth until it went monochrome and animated — see --spk-ribbon below.
     Everything else is black / white / grey, and the focus ring takes
     --spk-accent-solid. Change this one value to re-tint all of them.
     ---------------------------------------------------------------------- */
  /* Was electric lime. The brand is black / white / grey, so the default accent
     is now white through silver: the active rail, the focus ring, the spawn
     boundary and the plot cards' FREE pill and button all read as light rather
     than as a colour, and the world border stays the one warm thing on the page.
     Themes still re-tint everything by overriding these two values. */
  --spk-accent: linear-gradient(100deg, #ffffff 0%, #dcdcdc 50%, #a7a7a7 100%);
  --spk-accent-solid: #e6e6e6;

  /* ---- region palette ----------------------------------------------------
     One colour per region TYPE, read by the outlines on the map, the legend
     swatches and the dot on a plot card alike, so a theme re-palettes the
     whole town in one block instead of three. These defaults suit a green map
     with blue water; a theme whose map is warm, pale or pink overrides the
     ones that would vanish on it (Toussaint's shops were invisible in yellow
     on gold). --spk-rc-casing is the dark line drawn under every outline. */
  --spk-rc-road: #f6f1e6;
  --spk-rc-shop: #ffc93c;
  --spk-rc-landmark: #b06bff;
  --spk-rc-townhall: #ff4757;
  --spk-rc-netherhub: #ff7a18;
  --spk-rc-dock: #25c9ff;
  --spk-rc-npc: #ff5ec4;
  /* the Orc Fortress (2026-10-10): its area (a still dashed line), the King's arena, the bone-white build plots */
  --spk-rc-fortress: #ff8a3d;
  --spk-rc-arena: #d10f3a;
  --spk-rc-project: #f5deb3;
  --spk-rc-spawn: var(--spk-accent-solid);
  --spk-rc-casing: rgba(0, 0, 0, 0.62);

  /* The one warm colour on the site, reserved for the world border. Cyan means
     "a place you can go"; this means "the world stops here", and red is the
     only colour that says that without a label. Softened well off pure red so
     it sits on the dark map instead of vibrating against it. */
  --spk-edge: #ff6b5b;

  /* The top ribbon runs monochrome and drifting instead of taking the accent,
     so the one moving element on the page does not also compete for colour.
     It opens and closes on the same white and is painted at twice the viewport
     width, so sliding it by exactly one width loops with no visible seam. */
  --spk-ribbon: linear-gradient(
    90deg,
    #ffffff 0%,
    #9a9a9a 10%,
    #141414 22%,
    #6e6e6e 34%,
    #ffffff 46%,
    #6e6e6e 58%,
    #141414 70%,
    #9a9a9a 84%,
    #ffffff 100%
  );

  /* ---- Pl3xMap UI variables ---------------------------------------------
     Declared outside any media query so the chrome stays dark in every OS
     mode. A dark map deserves dark chrome; the stock light palette fights it.
     ---------------------------------------------------------------------- */
  color-scheme: dark;

  --ui-font: "Inter", "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --ui-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;

  --ui-button-size: 4.6rem;
  --ui-element-spacing: 1.1rem;
  --ui-border-radius: 0.8rem;
  --ui-border: 1px solid var(--spk-line);
  --ui-text-shadow: none;

  /* Was solid #ffffff — invisible against the stock light panels. Everything
     is dark now, so white reads; the offset keeps it off the border. */
  --ui-outline: solid var(--spk-white) 0.22rem;

  --ui-background:          rgba(18, 18, 18, 0.94);
  --ui-background-hover:    rgba(32, 32, 32, 0.97);
  --ui-background-active:   rgba(44, 44, 44, 0.97);
  --ui-background-selected: rgba(38, 38, 38, 0.97);
  --ui-background-disabled: rgba(12, 12, 12, 0.9);

  --ui-text:          var(--spk-text);
  --ui-text-hover:    var(--spk-white);
  --ui-text-active:   var(--spk-white);
  --ui-text-selected: var(--spk-white);
  --ui-text-disabled: var(--spk-g4);

  --ui-box-shadow:
    0 2px 6px rgba(0, 0, 0, 0.6),
    0 0 0 1px rgba(0, 0, 0, 0.3);

  --ui-sidebar-width: 27rem;
}

/* Hold the same dark theme if the OS asks for light mode. */
@media (prefers-color-scheme: light) {
  :root {
    --ui-border: 1px solid var(--spk-line);
    --ui-background:          rgba(18, 18, 18, 0.94);
    --ui-background-hover:    rgba(32, 32, 32, 0.97);
    --ui-background-active:   rgba(44, 44, 44, 0.97);
    --ui-background-selected: rgba(38, 38, 38, 0.97);
    --ui-background-disabled: rgba(12, 12, 12, 0.9);
    --ui-text:          var(--spk-text);
    --ui-text-hover:    var(--spk-white);
    --ui-text-active:   var(--spk-white);
    --ui-text-selected: var(--spk-white);
    --ui-text-disabled: var(--spk-g4);
  }
}

/* ==========================================================================
   The `hidden` attribute is only enforced by the UA rule `[hidden]{display:none}`,
   which ANY author `display` declaration silently overrides. Several elements
   below are toggled via el.hidden AND carry a display rule (#spk-places,
   .spk-places__item, .spk-online), so restate it as a hard guard. Without this
   the Places panel stayed visible underneath the Worlds/Layers/Players tabs.
   ========================================================================== */

[hidden] {
  display: none !important;
}

/* ==========================================================================
   Map surface
   The world is mostly unrendered, so the void is a large part of what people
   actually see. A neutral vignette makes it read as "nothing here yet"
   rather than "broken".
   ========================================================================== */

html,
body {
  background: var(--spk-void);
}

/* Pl3xMap sets a per-world starfield (images/sky/*.png) as an INLINE style on
   .map, so a plain `background` here loses to it — and shouldn't win anyway;
   the starfield is worth keeping. Instead, vignette it with an overlay stacked
   above the container background but below .leaflet-tile-pane (z-index 200),
   so the tiles themselves are never touched. */
.map::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: radial-gradient(
    125% 85% at 50% 40%,
    rgba(255, 255, 255, 0.05) 0%,
    rgba(0, 0, 0, 0.45) 55%,
    rgba(0, 0, 0, 0.85) 100%
  );
}

/* Leaflet 1.9.4 gives tiles `mix-blend-mode: plus-lighter` (additive) to hide
   seams while they fade in. Additive means they blend with everything painted
   beneath them — so the vignette above would tint the terrain itself, measured
   at up to +25/+19/+42 RGB before this rule. On a map where colour IS the
   data, that's wrong. Isolating the tile pane gives it its own stacking
   context: tiles still blend additively with EACH OTHER (seam fix preserved)
   but no longer with the decorative layers below. Terrain colours stay true. */
.leaflet-tile-pane {
  isolation: isolate;
}

/* ==========================================================================
   Header
   The map is shortened rather than covered, so no map area is lost behind an
   opaque bar. spektral.js calls map.invalidateSize() once so Leaflet picks up
   the new container height.
   ========================================================================== */

:root {
  --spk-header-h: 6rem;
}

/* Body becomes a flex column: header takes its natural height, the map takes
   whatever is left. No viewport units involved — `100vh` is not the visible
   height wherever browser chrome is dynamic (measured 863px against an actual
   809px here), which made the map exactly one header too tall. */
html,
body {
  height: 100%;
}

body {
  display: flex;
  flex-direction: column;
}

#spk-header {
  flex: 0 0 auto;
  position: relative;
  z-index: 1500;
  height: var(--spk-header-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.6rem;
  padding: 0 1.8rem;
  background: #0a0a0a;
  border-bottom: 1px solid var(--spk-line);
  box-shadow: 0 2px 14px rgba(0, 0, 0, 0.6);
  font-family: var(--ui-font);
  user-select: none;
}

/* Thin ribbon along the very top edge. Slowly drifting white-to-grey-to-black,
   which gives the page one small sign of life without pulling attention off
   the map. 18s is deliberately slow — fast enough to notice if you look at it,
   slow enough that it never reads as a loading bar. */
body::before {
  content: "";
  position: fixed;
  inset: 0 0 auto 0;
  height: 3px;
  background: var(--spk-ribbon);
  background-size: 200% 100%;
  animation: spk-ribbon-drift 18s linear infinite;
  opacity: 0.9;
  z-index: 2000;
  pointer-events: none;
}

@keyframes spk-ribbon-drift {
  from {
    background-position: 0% 0;
  }
  to {
    background-position: 200% 0;
  }
}

/* Motion is decorative here, so it goes away entirely for anyone who has asked
   the OS for less of it — the ribbon still reads as a ribbon standing still. */
@media (prefers-reduced-motion: reduce) {
  body::before {
    animation: none;
  }
}

/* The map fills the remaining column space. min-height:0 is required — flex
   items default to min-height:auto and would refuse to shrink. */
.map {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
}

/* styles.css pins these to the viewport; they must follow the shorter map. */
.leaflet-control-container {
  width: 100%;
  height: 100%;
}

/* The sidebar panel is absolutely positioned against #sidebar, so it cannot
   inherit the map's height. dvh tracks the *visible* viewport; the vh line
   above it is the fallback for browsers without dvh. */
#sidebar #sidebar__content {
  height: calc(100vh - var(--spk-header-h));
  height: calc(100dvh - var(--spk-header-h));
}

/* ---- brand ------------------------------------------------------------- */

.spk-brand {
  display: flex;
  align-items: center;
  gap: 1.1rem;
  min-width: 0;
}

/* Both brand assets are single-colour white SVGs, so they sit on the black
   bar with no treatment. Sized by height; widths come from their own aspect
   ratios (mark 707.88x768.86 = 0.921, wordmark 707.84x93.93 = 7.535). */

.spk-mark {
  flex: 0 0 auto;
  display: block;
  height: 4rem;
  width: calc(4rem * 0.921);
  background: url("images/spektral-mark.svg") no-repeat center / contain;
}

.spk-word {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
  min-width: 0;
}

/* The real wordmark art, not a system-font approximation of it. */
.spk-word__mark {
  display: block;
  flex: 0 1 auto;
  height: 1.7rem;
  width: calc(1.7rem * 7.535);
  max-width: 100%;
  background: url("images/spektral-wordmark.svg") no-repeat left center / contain;
}

/* The wordmark art reads "SPEKTRAL" only, so "SMP" is set alongside it. */
.spk-word__sub {
  font-size: 1.35rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--spk-g2);
  line-height: 1;
  white-space: nowrap;
}

/* ---- actions ----------------------------------------------------------- */

.spk-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-width: 0;
}

/* online badge */
.spk-online {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0 1.1rem;
  height: 3.6rem;
  border-radius: var(--ui-border-radius);
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--spk-g1);
  font-size: 1.3rem;
  font-weight: 600;
  white-space: nowrap;
}

.spk-online__count {
  color: var(--spk-white);
  font-variant-numeric: tabular-nums;
}

.spk-online__dot {
  width: 0.7rem;
  height: 0.7rem;
  border-radius: 50%;
  background: #3ba55d;
  box-shadow: 0 0 8px rgba(59, 165, 93, 0.8);
}

.spk-online.is-empty .spk-online__dot {
  background: var(--spk-g4);
  box-shadow: none;
}

/* server IP — click to copy */
.spk-ip {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 0.1rem;
  height: 3.6rem;
  padding: 0 1.2rem;
  border-radius: var(--ui-border-radius);
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--spk-white);
  cursor: pointer;
  line-height: 1;
  white-space: nowrap;
  transition: border-color 0.18s ease, background-color 0.18s ease;
}

.spk-ip:hover {
  border-color: var(--spk-line-strong);
  background: rgba(255, 255, 255, 0.07);
}

.spk-ip__label {
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--spk-g3);
}

.spk-ip__value {
  font-family: var(--ui-mono);
  font-size: 1.3rem;
  color: var(--spk-white);
}

.spk-ip.is-copied {
  border-color: #3ba55d;
}

.spk-ip.is-copied .spk-ip__value {
  color: #4ade80;
}

.spk-ip.is-failed {
  border-color: var(--spk-g2);
}

/* Discord — the primary call to action, so it is the only solid white chip */
.spk-discord {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  height: 3.6rem;
  padding: 0 1.4rem;
  border-radius: var(--ui-border-radius);
  border: 1px solid var(--spk-white);
  background: var(--spk-white);
  color: #000000;
  font-size: 1.35rem;
  font-weight: 700;
  text-decoration: none;
  white-space: nowrap;
  transition: opacity 0.18s ease, transform 0.18s ease;
}

.spk-discord:hover {
  opacity: 0.86;
}

.spk-discord:active {
  transform: translateY(1px);
}

.spk-discord__icon {
  width: 1.8rem;
  height: 1.8rem;
  flex: 0 0 auto;
}

/* ==========================================================================
   Buttons and control panels
   ========================================================================== */

/* ==========================================================================
   Buttons and control panels
   ========================================================================== */

button,
.leaflet-control-button,
.leaflet-bar a,
.leaflet-touch .leaflet-bar a,
.leaflet-control-panel,
#sidebar #sidebar__buttons button,
.leaflet-control-layers .leaflet-control-layers-toggle {
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  transition:
    background-color 0.18s ease,
    border-color 0.18s ease,
    color 0.18s ease,
    box-shadow 0.18s ease,
    transform 0.18s ease;
}

button:hover,
.leaflet-control-button:hover,
.leaflet-bar a:hover,
#sidebar #sidebar__buttons button:hover {
  border-color: var(--spk-line-strong);
  box-shadow:
    0 2px 10px rgba(0, 0, 0, 0.65),
    0 0 0 1px var(--spk-line-strong);
}

button:active,
.leaflet-control-button:active,
.leaflet-bar a:active {
  transform: translateY(1px);
}

/* Sidebar tab buttons: white lift, plus an accent rail while open. */
#sidebar #sidebar__buttons button {
  position: relative;
  overflow: hidden;
}

#sidebar #sidebar__buttons button[aria-expanded="true"] {
  background-color: var(--ui-background-selected);
  border-color: var(--spk-line-strong);
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.65);
  color: var(--spk-white);
}

/* Colour spot 2. */
#sidebar #sidebar__buttons button[aria-expanded="true"]::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--spk-accent);
}

.svg-icon {
  filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55));
}

.leaflet-bar a,
.leaflet-touch .leaflet-bar a {
  font-family: var(--ui-font);
  font-weight: 500;
}

/* ==========================================================================
   Bottom readouts — coordinates / blockinfo / link
   ========================================================================== */

.leaflet-control-panel {
  box-shadow: var(--ui-box-shadow);
}

.leaflet-control-coordinates {
  font-family: var(--ui-mono);
  font-variant-numeric: tabular-nums;
  color: var(--spk-white);
  letter-spacing: 0.14rem;
}

.leaflet-control-coordinates:before {
  color: var(--spk-g3);
  text-transform: uppercase;
  letter-spacing: 0.12rem;
  font-family: var(--ui-font);
  font-weight: 600;
  margin-bottom: 0.1rem;
}

.leaflet-control-blockinfo {
  font-family: var(--ui-mono);
  color: var(--spk-g1);
  font-size: 1.4rem;
  line-height: 1.9rem;
}

.leaflet-control-link svg {
  color: var(--spk-g1);
}

.leaflet-control-link:hover svg {
  color: var(--spk-white);
}

/* --- attribution ---------------------------------------------------------
   leaflet.css hardcodes background: rgba(255,255,255,.8) and styles.css
   forces link colour to black, so the stock build shows a bright white chip
   in the corner of an otherwise dark UI.
   ------------------------------------------------------------------------- */

.leaflet-container .leaflet-control-attribution,
.leaflet-control-attribution {
  background: rgba(0, 0, 0, 0.7);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
  color: var(--spk-g3);
  border: 1px solid var(--spk-line);
  border-radius: var(--ui-border-radius);
  padding: 0.2rem 0.7rem;
  font-size: 1rem;
}

.leaflet-control-attribution a,
.leaflet-control-attribution a:visited,
.leaflet-control-attribution a:active,
.leaflet-control-attribution a:hover {
  color: var(--spk-g2);
}

.leaflet-control-attribution a:hover {
  color: var(--spk-white);
}

/* ==========================================================================
   Sidebar
   ========================================================================== */

#sidebar #sidebar__content {
  background: rgba(14, 14, 14, 0.97);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: none;
  border-left: 1px solid var(--spk-line);
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.6);
  padding: 1.6rem 1.8rem 3rem;
  scrollbar-width: thin;
  scrollbar-color: var(--spk-line-strong) transparent;
}

#sidebar #sidebar__content::-webkit-scrollbar {
  width: 8px;
}

#sidebar #sidebar__content::-webkit-scrollbar-track {
  background: transparent;
}

#sidebar #sidebar__content::-webkit-scrollbar-thumb {
  background: var(--spk-line-strong);
  border-radius: 4px;
}

#sidebar #sidebar__content::-webkit-scrollbar-thumb:hover {
  background: var(--spk-g1);
}

/* Panel headings ("Players (0/20)", "Worlds", "Layers") — white text,
   accent only in the rule beneath it. Colour spot 3. */
#sidebar__content h2 {
  font-size: 2.2rem;
  line-height: 2.8rem;
  font-weight: 700;
  letter-spacing: -0.02rem;
  color: var(--spk-white);
  margin-bottom: 1.6rem;
  padding-bottom: 1.2rem;
  border-bottom: 1px solid var(--spk-line);
  position: relative;
}

#sidebar__content h2::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 4.5rem;
  height: 2px;
  background: var(--spk-accent);
}

/* Empty states — "No players are currently online" etc. */
#sidebar__content > p {
  color: var(--spk-g3);
  font-size: 1.4rem;
  font-style: italic;
  margin: 0 0 1rem;
}

#sidebar__content > p:focus {
  outline: none;
}

/* World group labels (<legend> per world) — unstyled upstream. */
#sidebar__content legend {
  color: var(--spk-g2);
  font-size: 1.15rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.14rem;
  padding: 0 0 0.7rem;
  width: 100%;
}

#sidebar__content fieldset fieldset {
  margin-bottom: 1.8rem;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
}

/* ==========================================================================
   Menus — world/renderer pickers, layer toggles, player list
   ========================================================================== */

.menu input:checked + label {
  background-color: var(--ui-background-selected);
  background-image: none;
  border-color: var(--spk-line-strong);
  color: var(--spk-white);
}

/* --- renderer icon tiles -------------------------------------------------- */

.menu input.renderer + label {
  width: 3.8rem;
  height: 3.8rem;
  border-radius: 0.6rem;
  margin: 0 0.6rem 0 0;
  background-size: cover;
  opacity: 0.55;
  filter: saturate(0.6);
}

.menu input.renderer + label:hover {
  opacity: 0.85;
  filter: saturate(0.9);
}

.menu input.renderer:checked + label {
  opacity: 1;
  filter: saturate(1);
  /* Upstream sets border-color:#000000 here, which just disappears. */
  border-color: var(--spk-white);
  box-shadow: 0 0 0 1px var(--spk-white);
}

/* --- layer toggles --------------------------------------------------------
   These are checkboxes whose <input> is clipped to 1px by styles.css, so the
   only "on" cue upstream is a slightly lighter row — six enabled layers all
   look identical. Give them a real checkbox.
   ------------------------------------------------------------------------- */

#sidebar__layers .menu label {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 0.7rem 1.1rem;
  font-size: 1.4rem;
  line-height: 2rem;
  border-radius: 0.7rem;
}

#sidebar__layers .menu label::before {
  content: "";
  flex: 0 0 auto;
  width: 1.7rem;
  height: 1.7rem;
  border-radius: 0.4rem;
  border: 1px solid var(--spk-line-strong);
  background: transparent no-repeat center / 1.3rem 1.3rem;
  transition: background-color 0.15s ease, border-color 0.15s ease;
}

#sidebar__layers .menu input:checked + label::before {
  background-color: var(--spk-white);
  border-color: var(--spk-white);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round' d='M3.5 8.4l3 3 6-6'/%3E%3C/svg%3E");
}

#sidebar__layers .menu input:checked + label {
  font-weight: 600;
}

/* --- player rows ---------------------------------------------------------- */

.menu input.players + label {
  border-radius: 0.7rem;
  padding: 0.7rem 1rem 0.7rem 4.6rem;
  background-size: 28px 28px;
  background-position: 8px center;
  image-rendering: pixelated;
  font-size: 1.45rem;
  line-height: 2.2rem;
  margin-bottom: 0.5rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  display: block;
}

.menu input.players:checked + label {
  box-shadow: 0 0 0 1px var(--spk-line-strong);
}

.menu input.players + label[disabled] {
  opacity: 0.5;
  filter: grayscale(1);
}

/* ==========================================================================
   Tooltips and popups
   Upstream leaves Leaflet's white defaults in place, so marker tooltips
   (mobs, spawn, banners) and claim popups render as white boxes while the
   player nameplates — styled in layers/players.yml — are dark. Unify them.
   ========================================================================== */

.leaflet-tooltip {
  background-color: rgba(10, 10, 10, 0.94);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  border: 1px solid var(--spk-line-strong);
  border-radius: 0.6rem;
  color: var(--spk-white);
  box-shadow: 0 3px 14px rgba(0, 0, 0, 0.7);
  font-family: var(--ui-font);
  font-size: 1.2rem;
  font-weight: 500;
  padding: 0.5rem 0.8rem;
}

.leaflet-tooltip-top:before    { border-top-color: var(--spk-line-strong); }
.leaflet-tooltip-bottom:before { border-bottom-color: var(--spk-line-strong); }
.leaflet-tooltip-left:before   { border-left-color: var(--spk-line-strong); }
.leaflet-tooltip-right:before  { border-right-color: var(--spk-line-strong); }

.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
  background: rgba(18, 18, 18, 0.97);
  color: var(--spk-text);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.65);
}

.leaflet-popup-content-wrapper {
  border: 1px solid var(--spk-line-strong);
  border-radius: 0.8rem;
  font-family: var(--ui-font);
}

.leaflet-popup-content {
  font-size: 1.3rem;
  line-height: 1.85rem;
  margin: 1.2rem 2.2rem 1.2rem 1.4rem;
}

.leaflet-popup-content hr {
  border: none;
  border-top: 1px solid var(--spk-line);
  margin: 0.8rem 0;
}

.leaflet-container a.leaflet-popup-close-button {
  color: var(--spk-g3);
  width: 26px;
  height: 26px;
  font-size: 18px;
  line-height: 26px;
}

.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
  color: var(--spk-white);
}

/* ==========================================================================
   Layers control (the floating Leaflet one, if it ever shows)
   ========================================================================== */

.leaflet-control-layers.leaflet-control-layers-expanded .leaflet-control-layers-list,
.leaflet-touch .leaflet-control-layers.leaflet-control-layers-expanded .leaflet-control-layers-list {
  background: rgba(18, 18, 18, 0.96);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.6);
  padding: 0.8rem 1rem;
}

.leaflet-control-layers .leaflet-control-layers-selector {
  padding: 0.4rem 0.5rem;
  font-size: 1.4rem;
  cursor: pointer;
}

.leaflet-control-layers .leaflet-control-layers-selector:hover {
  background: rgba(255, 255, 255, 0.07);
}

.leaflet-control-layers .leaflet-control-layers-separator {
  border: none;
  border-top: 1px solid var(--spk-line);
}

.leaflet-control-layers input[type="checkbox"],
.leaflet-control-layers input[type="radio"] {
  accent-color: var(--spk-white);
}

/* ==========================================================================
   Focus visibility — colour spot 4
   ========================================================================== */

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

/* ==========================================================================
   Small screens
   ========================================================================== */

/* Tighten the header before it has a chance to wrap. */
@media (max-width: 860px) {
  .spk-ip__label {
    display: none;
  }

  .spk-online__label {
    display: none;
  }
}

@media (max-width: 640px) {
  :root {
    --ui-button-size: 4.2rem;
    --ui-sidebar-width: 22rem;
    --spk-header-h: 5.4rem;
  }

  #spk-header {
    padding: 0 1.1rem;
    gap: 1rem;
  }

  /* The wordmark is the first thing to go — the mark still identifies the
     server, and the Discord button is what actually needs the room. */
  .spk-word {
    display: none;
  }

  .spk-mark {
    height: 3.4rem;
    width: calc(3.4rem * 0.921);
  }

  .spk-actions {
    gap: 0.7rem;
  }

  /* The three bottom controls sit in an equal 3-column grid and each overflows
     its own cell once the viewport is narrow — the attribution's opaque
     background ends up painted over the tail of the coordinate readout.
     Flex sizes each to its content and lays them out end to end, so they
     cannot overlap however long the coordinates get. */
  .leaflet-control-container-bottom {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 0.6rem;
  }

  .leaflet-bottom {
    min-width: 0;
  }

  /* BlockInfo is driven by mousemove, so on a touch device it can only ever
     read "Unknown block" — dropping it is both the fix for the remaining
     crowding and the honest thing to do. */
  .leaflet-control-blockinfo {
    display: none;
  }

  .leaflet-control-coordinates {
    min-width: 0;
    font-size: 1.4rem;
    letter-spacing: 0.05rem;
    padding: 0.4rem 0.8rem;
  }

  .leaflet-control-attribution {
    font-size: 0.9rem;
  }

  #sidebar__content h2 {
    font-size: 1.9rem;
  }
}

/* Very narrow: drop the IP chip, keep identity + the call to action. */
@media (max-width: 420px) {
  .spk-ip {
    display: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
  }
}

/* ==========================================================================
   Places panel
   A markers sidebar tab built in spektral.js — upstream ships one but leaves
   it commented out in SidebarControl.ts. Styled to match the native tabs.
   ========================================================================== */

#spk-places {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.spk-places__filter {
  appearance: none;
  width: 100%;
  margin-bottom: 1.4rem;
  padding: 0.8rem 1.1rem;
  border-radius: var(--ui-border-radius);
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.04);
  color: var(--spk-white);
  font-family: var(--ui-font);
  font-size: 1.4rem;
  line-height: 2rem;
}

.spk-places__filter::placeholder {
  color: var(--spk-g3);
}

.spk-places__filter:focus {
  border-color: var(--spk-line-strong);
  outline: var(--ui-outline);
  outline-offset: 2px;
}

.spk-places__group {
  margin-bottom: 2rem;
}

.spk-places__group h3 {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  margin: 0 0 0.9rem;
  font-size: 1.15rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--spk-g2);
}

.spk-places__count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.9rem;
  height: 1.9rem;
  padding: 0 0.5rem;
  border-radius: 1rem;
  background: rgba(255, 255, 255, 0.09);
  color: var(--spk-g1);
  font-size: 1.05rem;
  letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}

/* Rows are buttons — keyboard reachable, and they read as actions because
   clicking one moves the map. */
.spk-places__item {
  display: flex;
  flex-direction: row;
  align-items: flex-start;
  gap: 1.1rem;
  width: 100%;
  margin-bottom: 0.6rem;
  padding: 0.9rem 1.2rem;
  border-radius: 0.7rem;
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.03);
  text-align: left;
  line-height: 1.4;
  cursor: pointer;
  transition: border-color 0.15s ease, background-color 0.15s ease;
}

.spk-places__icon {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.2rem;
  height: 3.2rem;
  margin-top: 0.1rem;
  border-radius: 0.6rem;
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.06);
  color: var(--spk-white);
}

.spk-places__icon svg {
  width: 1.8rem;
  height: 1.8rem;
}

/* A place carrying its own PNG (see "icon" in places.json). Resource-pack art
   is pixel art, so keep it crisp instead of smoothed, and let it sit larger
   than the vector glyphs — those are line drawings that need breathing room,
   while a rendered icon reads better filling the tile. */
.spk-places__icon .spk-place-img {
  width: 2.4rem;
  height: 2.4rem;
  image-rendering: pixelated;
}

.spk-places__body {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
  min-width: 0;
  flex: 1 1 auto;
}

/* Descriptions are the point of a curated list, so let them wrap to two
   lines rather than truncating after a few words. */
.spk-places__desc {
  font-size: 1.2rem;
  line-height: 1.6rem;
  color: var(--spk-g1);
  max-width: 100%;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Featured entries get the accent, matching the other colour spots. */
.spk-places__item.is-featured .spk-places__icon {
  background: var(--spk-accent);
  border-color: transparent;
  color: #000000;
}

/* ...but not when the entry supplies its own artwork. The tile and the accent
   exist to back the monochrome glyphs; a finished icon ends up framed twice.
   Listed after the featured rule, and matching its specificity, so it wins. */
.spk-places__icon.is-art,
.spk-places__item.is-featured .spk-places__icon.is-art {
  background: none;
  border-color: transparent;
}

.spk-places__icon.is-art .spk-place-img {
  width: 3rem;
  height: 3rem;
}

.spk-places__item:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: var(--spk-line-strong);
}

.spk-places__title {
  font-size: 1.45rem;
  font-weight: 600;
  color: var(--spk-white);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.spk-places__meta {
  font-family: var(--ui-mono);
  font-size: 1.15rem;
  color: var(--spk-g1);
  font-variant-numeric: tabular-nums;
}

.spk-places__extra {
  font-size: 1.15rem;
  color: var(--spk-g3);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.spk-places__empty {
  color: var(--spk-g3);
  font-size: 1.4rem;
  font-style: italic;
  margin: 0;
}

/* ---- curated place markers on the map ----------------------------------- */

.spk-place-marker {
  background: none;
  border: none;
}

.spk-place-marker__disc {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: rgba(10, 10, 10, 0.88);
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.7);
  color: var(--spk-white);
  transition: transform 0.15s ease, border-color 0.15s ease;
}

.spk-place-marker:hover .spk-place-marker__disc {
  transform: scale(1.12);
  border-color: #ffffff;
}

.spk-place-marker__icon {
  width: 17px;
  height: 17px;
  /* Leaflet marks marker icons pixelated for Minecraft skins; these are
     vector glyphs and need the opposite. */
  image-rendering: auto;
}

/* Custom PNG in the map pin. Pixel art, so restore the crisp scaling the rule
   above turns off, and fill more of the 30px disc than a glyph would. */
.spk-place-marker__icon.spk-place-img {
  width: 22px;
  height: 22px;
  image-rendering: pixelated;
}

/* Artwork stands on its own — drop the disc it would otherwise sit inside and
   let the icon fill the marker. The shadow replaces the contrast the disc was
   providing, which the map still needs over snow and other pale terrain. */
.spk-place-marker__disc.is-art {
  background: none;
  border: none;
  box-shadow: none;
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
}

.spk-place-marker__disc.is-art .spk-place-img {
  width: 30px;
  height: 30px;
}

/* ---- place footprints ---------------------------------------------------
   A place can draw its own outline (see "area" in places.json) rather than
   leaning on the WorldGuard claims layer, which can only style every region
   identically. Styled here instead of through Leaflet's path options so the
   outline can animate: the dashes travel slowly around the edge, the same
   idea as the top ribbon, which makes a region read as somewhere alive rather
   than as one more static claim box.
   dasharray 9 + 7 is a 16px period, so a -16px offset per cycle loops with no
   visible jump. ------------------------------------------------------------ */
.spk-place-area,
/* Regions are coloured by what they ARE, not all in one accent. Ninety regions
   in a single lime on green terrain was unreadable - you could not tell a road
   from a shop from the town hall. Each type sets --rc and everything below
   reads from it, so a theme can retint one line rather than every rule.

   Hues are picked to survive the map underneath them, which is mostly green
   with blue water: warm and magenta hues read clearly, a green plot colour
   would not. */
.spk-region--type-road      { --rc: var(--spk-rc-road); }
.spk-region--type-shop      { --rc: var(--spk-rc-shop); }
.spk-region--type-landmark  { --rc: var(--spk-rc-landmark); }
.spk-region--type-townhall  { --rc: var(--spk-rc-townhall); }
.spk-region--type-netherhub { --rc: var(--spk-rc-netherhub); }
.spk-region--type-dock      { --rc: var(--spk-rc-dock); }
.spk-region--type-npc       { --rc: var(--spk-rc-npc); }
.spk-region--type-fortress  { --rc: var(--spk-rc-fortress); }
.spk-region--type-arena     { --rc: var(--spk-rc-arena); }
.spk-region--type-project   { --rc: var(--spk-rc-project); }
.spk-region--type-other,
.spk-region--type-spawn     { --rc: var(--spk-rc-spawn); }

.spk-region {
  fill: var(--rc, var(--spk-accent-solid));
  fill-opacity: 0.34;
  stroke: var(--rc, var(--spk-accent-solid));
  stroke-width: 1.5;
  stroke-opacity: 0.95;
  stroke-linejoin: round;
  cursor: pointer;
  transition: fill-opacity 120ms ease-out;
}

.spk-region:hover {
  fill-opacity: 0.55;
}

/* ---- plot states ---------------------------------------------------------
   Read from the feed's owners: a FREE shop is an empty dashed outline, a
   RESERVED lot (landmark, NPC, hall) is a faint wash waiting for staff, and a
   CLAIMED plot is solid. The town fills in as people buy deeds. */
.spk-region--free {
  fill-opacity: 0.06;
  stroke-dasharray: 3 3;
  stroke-opacity: 0.85;
}

.spk-region--free:hover {
  fill-opacity: 0.22;
}

.spk-region--reserved {
  fill-opacity: 0.16;
  stroke-opacity: 0.6;
}

.spk-region--claimed {
  fill-opacity: 0.44;
  stroke-width: 2;
}

.spk-region--claimed:hover {
  fill-opacity: 0.62;
}

/* One pulse the moment a plot flips to claimed while someone is watching. */
.spk-region--just-claimed {
  animation: spk-claim-pulse 3s ease-out 1;
}

@keyframes spk-claim-pulse {
  0%   { fill-opacity: 0.95; stroke-width: 5; }
  60%  { fill-opacity: 0.44; stroke-width: 2; }
  80%  { fill-opacity: 0.8;  stroke-width: 4; }
  100% { fill-opacity: 0.44; stroke-width: 2; }
}

/* ---- names and faces on the map ------------------------------------------
   Both are Leaflet layers that exist at every zoom; the map container's
   spk-zoom-1 / spk-zoom-2 classes (set on zoomend in spektral.js) decide when
   there is room to show them: faces at 2 px per block, names at 4. */
.leaflet-tooltip.spk-plot-label {
  display: none;
  padding: 0.1rem 0.45rem;
  border: 0;
  border-radius: 4px;
  background: rgba(10, 12, 10, 0.72);
  color: #fff;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.01em;
  white-space: nowrap;
  box-shadow: none;
  pointer-events: none;
}

.leaflet-tooltip.spk-plot-label:before {
  display: none;
}

.spk-zoom-2 .leaflet-tooltip.spk-plot-label {
  display: block;
}

.spk-plot-head {
  display: none;
  background: none;
  border: 0;
}

.spk-zoom-1 .spk-plot-head {
  display: block;
}

/* Sit the face above the name so the two never overlap at the closest zoom. */
.spk-zoom-2 .spk-plot-head {
  transform: translateY(-16px);
}

.spk-plot-head img {
  display: block;
  width: 22px;
  height: 22px;
  border-radius: 4px;
  box-shadow: 0 0 0 2px rgba(10, 12, 10, 0.75);
  image-rendering: pixelated;
}

.leaflet-tooltip.spk-region-tip {
  padding: 0.25rem 0.55rem;
  border: 0;
  border-radius: 6px;
  background: rgba(10, 12, 10, 0.86);
  color: #fff;
  font-size: 12px;
  line-height: 1.3;
  box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
}

.leaflet-tooltip.spk-region-tip:before {
  border-top-color: rgba(10, 12, 10, 0.86);
}

/* Roads are the connective tissue, not a plot - they should read as ground the
   town is laid out on rather than compete with the things sitting on it. */
/* Roads is ONE region over the whole town footprint - the public realm a single
   owner can hold - not the paving. Filled, it would be a slab across the plaza and
   every gap between plots, so it is a faint dashed town-limits line and nothing more. */
.spk-region--type-road {
  fill: none;
  stroke-width: 1.5;
  stroke-opacity: 0.55;
  stroke-linecap: butt;
  stroke-dasharray: 4 4;
}

.spk-region--type-road:hover {
  fill: none;
  stroke-opacity: 0.9;
}

/* The Orc Fortress is an area like spawn: an outline, never a slab over its plots. Dashed but still: the
   map body carries no motion (the spawn boundary's march is the one exception kept from before). */
.spk-region--type-fortress {
  fill: none;
  stroke-width: 2;
  stroke-opacity: 0.85;
  stroke-linecap: butt;
  stroke-dasharray: 8 6;
}

.spk-region--type-fortress:hover {
  fill: none;
  stroke-opacity: 1;
}

.spk-region-casing--type-fortress {
  stroke-width: 4;
  stroke-opacity: 0.6;
  stroke-dasharray: 8 6;
}

/* ---- casing ---------------------------------------------------------------
   A dark line under every outline: spektral.js draws each region twice, the
   casing first in its own renderer so it sits beneath. It is what keeps a
   yellow shop plot readable on gold ground or an amber one on snow, the way a
   road on a printed map is cased in black. Dashes match the outline above so
   the two stay aligned. Never interactive. Themes with a dark map lighten
   --spk-rc-casing instead. */
.spk-region-casing {
  fill: none;
  stroke: var(--spk-rc-casing);
  stroke-width: 4;
  stroke-linejoin: round;
  stroke-linecap: butt;
  pointer-events: none;
}

.spk-region-casing--free {
  stroke-dasharray: 3 3;
  stroke-opacity: 0.8;
}

.spk-region-casing--reserved {
  stroke-opacity: 0.5;
}

.spk-region-casing--claimed {
  stroke-width: 4.5;
}

.spk-region-casing--type-road {
  stroke-width: 3.5;
  stroke-opacity: 0.6;
  stroke-dasharray: 4 4;
}

/* Spawn is the 640x640 boundary. Filling it would bury the whole town under one
   wash, so it stays an outline - and keeps the marching dashes, which now mark
   "this is a boundary, not a plot" rather than being every region's default. */
.spk-place-area--spawn,
.spk-region--type-spawn {
  fill: none;
  stroke-width: 2.5;
  stroke-opacity: 0.9;
  stroke-linecap: butt;
  stroke-dasharray: 9 7;
  animation: spk-area-march 2.4s linear infinite;
}

.spk-region--type-spawn:hover {
  fill: none;
}

@keyframes spk-area-march {
  to {
    stroke-dashoffset: -16;
  }
}

/* ---- region key ----------------------------------------------------------
   Colour alone only helps if the reader knows what the colours mean. Built in
   JS from the regions actually present, so it never advertises a plot type
   this server does not have.

   Bottom right: the left edge already carries the roster at the top and the
   coordinate/blockinfo card at the bottom, so this is the one free corner.
   ---------------------------------------------------------------------- */
.spk-legend {
  position: fixed;
  right: 1.1rem;
  bottom: calc(1.9rem + (var(--ui-button-size) * 2));
  z-index: 900;
  width: 12.5rem;
  padding: 0.5rem 0.65rem 0.6rem;
  border-radius: var(--ui-border-radius);
  border: var(--ui-border);
  background: var(--ui-background);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--ui-box-shadow);
  font-size: 0.78rem;
  line-height: 1.25;
}

.spk-legend__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: 0 0 0.45rem;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  font-size: 0.68rem;
  opacity: 0.72;
  cursor: pointer;
}

.spk-legend__toggle:after {
  content: "\2013";
  opacity: 0.7;
}

.spk-legend.is-collapsed .spk-legend__toggle {
  padding-bottom: 0;
}

.spk-legend.is-collapsed .spk-legend__toggle:after {
  content: "+";
}

.spk-legend.is-collapsed .spk-legend__list {
  display: none;
}

.spk-legend__list {
  display: grid;
  gap: 0.32rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.spk-legend__row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

.spk-legend__swatch {
  flex: none;
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
  /* An inset dark edge instead of a border: the pale road swatch vanishes into
     the panel without one. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.45);
}

.spk-legend__swatch--road      { background: var(--spk-rc-road); }
.spk-legend__swatch--shop      { background: var(--spk-rc-shop); }
.spk-legend__swatch--landmark  { background: var(--spk-rc-landmark); }
.spk-legend__swatch--townhall  { background: var(--spk-rc-townhall); }
.spk-legend__swatch--netherhub { background: var(--spk-rc-netherhub); }
.spk-legend__swatch--dock      { background: var(--spk-rc-dock); }
.spk-legend__swatch--npc       { background: var(--spk-rc-npc); }
.spk-legend__swatch--fortress  { background: var(--spk-rc-fortress); }
.spk-legend__swatch--arena     { background: var(--spk-rc-arena); }
.spk-legend__swatch--project   { background: var(--spk-rc-project); }
.spk-legend__swatch--other     { background: var(--spk-rc-spawn); }

.spk-legend__label {
  flex: 1 1 auto;
}

.spk-legend__count {
  opacity: 0.55;
  font-variant-numeric: tabular-nums;
}

/* ---- region popup --------------------------------------------------------
   Pl3xMapExtras publishes only the region id in bold, which said nothing once
   the town had ninety regions in it. Rebuilt in drawServerRegions from what
   the feed implies - kind, size, and how many regions form the plot.
   ---------------------------------------------------------------------- */
.spk-rpop {
  min-width: 11rem;
}

.spk-rpop__name {
  font-weight: 700;
  font-size: 0.95rem;
  letter-spacing: 0.01em;
}

/* The type colour is a DOT beside the word, not the pill's background. With
   themes free to re-palette the town, a black plot colour (Bees of Ra) on a
   black pill would have swallowed the text. */
.spk-rpop__type {
  display: inline-flex;
  align-items: center;
  gap: 0.38rem;
  margin-top: 0.25rem;
  padding: 0.12rem 0.5rem 0.12rem 0.42rem;
  border-radius: 999px;
  font-size: 0.68rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: #fff;
  background: rgba(255, 255, 255, 0.1);
}

.spk-rpop__type::before {
  content: "";
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--rc, var(--spk-accent-solid));
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.4);
}

.spk-rpop__stats {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.15rem 0.6rem;
  margin: 0.55rem 0 0;
  font-size: 0.8rem;
}

.spk-rpop__stats dt {
  opacity: 0.6;
}

.spk-rpop__stats dd {
  margin: 0;
  font-variant-numeric: tabular-nums;
}

/* ---- the plot card -------------------------------------------------------
   A coloured stripe for the kind of plot, the shop's name (or the owner's) as
   the title, a status pill, the owner with their face, friends, size, and for
   a free shop the button that flies to Fynn. */
.spk-rpop {
  min-width: 13rem;
  max-width: 17rem;
  padding-left: 0.6rem;
  border-left: 4px solid var(--rc, var(--spk-accent-solid));
}

.spk-rpop--type-road      { --rc: var(--spk-rc-road); }
.spk-rpop--type-shop      { --rc: var(--spk-rc-shop); }
.spk-rpop--type-landmark  { --rc: var(--spk-rc-landmark); }
.spk-rpop--type-townhall  { --rc: var(--spk-rc-townhall); }
.spk-rpop--type-netherhub { --rc: var(--spk-rc-netherhub); }
.spk-rpop--type-dock      { --rc: var(--spk-rc-dock); }
.spk-rpop--type-npc       { --rc: var(--spk-rc-npc); }

.spk-rpop__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
}

.spk-rpop__title {
  font-weight: 700;
  font-size: 0.98rem;
  line-height: 1.2;
}

.spk-rpop__sub {
  margin-top: 0.1rem;
  font-size: 0.72rem;
  opacity: 0.6;
  font-variant-numeric: tabular-nums;
}

.spk-rpop__pill {
  flex: 0 0 auto;
  margin-top: 0.1rem;
  padding: 0.12rem 0.5rem;
  border-radius: 999px;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #10120f;
  background: #cfd3cc;
}

.spk-rpop__pill--free     { background: var(--spk-accent-solid); }
/* Dark with the type colour as a ring, for the same reason as the dot above. */
.spk-rpop__pill--claimed  { background: #10120f; color: #fff; box-shadow: inset 0 0 0 1.5px var(--rc, var(--spk-accent-solid)); }
.spk-rpop__pill--reserved { background: #9aa39a; }

.spk-rpop__owner {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  margin-top: 0.6rem;
}

.spk-rpop__face {
  width: 32px;
  height: 32px;
  border-radius: 5px;
  box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);
  image-rendering: pixelated;
}

.spk-rpop__k {
  font-size: 0.68rem;
  opacity: 0.6;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

.spk-rpop__v {
  font-weight: 600;
}

.spk-rpop__cta {
  display: block;
  width: 100%;
  margin-top: 0.65rem;
  padding: 0.45rem 0.6rem;
  border: 0;
  border-radius: 8px;
  background: var(--spk-accent-solid);
  color: #10120f;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.spk-rpop__cta:hover {
  filter: brightness(1.08);
}

.spk-rpop__note {
  margin-top: 0.55rem;
  font-size: 0.75rem;
  opacity: 0.6;
}

/* Phones already stack the roster and the readout card over the map, and a
   third floating panel there is one too many. The breakpoint is deliberately
   lower than the 760px used elsewhere: at 760 the key vanished from ordinary
   half-width desktop windows, where the roster and readouts were both still
   showing, which just looked broken. */
@media (max-width: 620px) {
  .spk-legend {
    display: none;
  }
}

/* ---- world border -------------------------------------------------------
   Drawn from the server's own feed (see drawWorldBorder in spektral.js), so it
   tracks /worldborder set with nothing to update by hand.

   No fill: a border is an edge, and shading the 144 million blocks inside it
   would bury the map. Longer dashes than a region outline because this shape is
   only ever seen zoomed out, where a fine pattern collapses into a solid line.
   dasharray 18 + 10 is a 28px period, matching the -28px offset per cycle so
   the march loops seamlessly. Dash lengths are screen pixels rather than world
   units, so one pattern holds at every zoom level. ------------------------- */
.spk-worldborder {
  fill: none;
  stroke: var(--spk-edge);
  stroke-width: 2;
  stroke-opacity: 0.9;
  stroke-linecap: butt;
  stroke-dasharray: 18 10;
  animation: spk-border-march 4s linear infinite;
  /* Has to stay legible over both dark ocean and bright snow. */
  filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.7));
}

@keyframes spk-border-march {
  to {
    stroke-dashoffset: -28;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spk-place-area,
  .spk-region,
  .spk-worldborder {
    animation: none;
  }
}

/* ---- themes -------------------------------------------------------------
   Every theme redefines the SAME four tokens and nothing else: the accent
   gradient, the accent solid (which is also the focus ring), the world border
   colour, and the top ribbon. Optionally a map tint and falling particles.

   Keeping the surface this small is what stops themes becoming a maintenance
   tax - a token added to the default palette later inherits everywhere instead
   of needing a value in each of six themes.

   Two rules every theme respects:
     - accent-solid is the focus indicator, so it must stay clearly visible
       against dark panels. Pastels are brightened rather than used raw.
     - accent and border must stay tellable apart. The accent means "somewhere
       you can go", the border means "the world stops here"; a theme that makes
       both the same hue throws that distinction away.

   "dark" is the base palette in :root and carries no attribute at all.
   ---------------------------------------------------------------------- */

/* Night. The default interface, untouched, over a map lit like a city after
   dark: cool, deep and low in contrast, so the plot outlines, the faces and the
   player markers read as the lights. The chrome deliberately inherits every
   default token - this is a map theme, not a UI variant. */
:root[data-theme="night"] {
  /* Tested against three alternatives: darker went to black mud, and an
     inverted duotone read as daytime pastel. This keeps the terrain legible. */
  --spk-tint: grayscale(1) sepia(1) hue-rotate(190deg) saturate(1.9) brightness(0.45)
    contrast(1.05);
  /* A dark casing would vanish on a dark map. */
  --spk-rc-casing: rgba(255, 255, 255, 0.3);
}

/* Toussaint. Late-summer light over vineyards, after the duchy in The Witcher:
   the map is warmed towards gold and lifted a little, the way Beauclair sits
   under an afternoon sun. Accent gold, border the red of Sangreal wine,
   lavender in the ribbon. Replaces the old Fantasy theme, which was the same
   idea shouted. */
:root[data-theme="toussaint"] {
  --spk-accent: linear-gradient(100deg, #fbe7a8 0%, #e9b949 50%, #c98a1e 100%);
  --spk-accent-solid: #e9b949;
  --spk-edge: #b3213f;
  --spk-ribbon: linear-gradient(
    90deg,
    #e9b949 0%,
    #2a1a10 22%,
    #b59ad6 48%,
    #2a1a10 74%,
    #e9b949 100%
  );
  /* Sepia is what makes a map look old, so it is kept very low here - anything
     past about 0.2 turned the forests olive. The gold comes from the light
     overlay below instead, which warms without draining the greens. */
  --spk-tint: sepia(0.14) saturate(1.3) brightness(1.08) contrast(1.02);
  /* Yellow on gold-green ground was invisible. Shops take the blue of the
     Beauclair palace roofs, the nether hub goes rust so it is not another
     gold, the road goes soil brown, and the spawn boundary matches the
     wine-red world border - both are boundaries. */
  --spk-rc-road: #5b3a29;
  --spk-rc-shop: #2457c5;
  --spk-rc-netherhub: #9a3412;
  --spk-rc-spawn: #b3213f;
}

/* The vignette becomes the light: a golden sun low in the top-left corner, a
   thin warm wash across the whole map that cools to lavender at the far edge,
   and a brown rather than black falloff, because shadows in sunlight are
   warm too. */
:root[data-theme="toussaint"] .map::after {
  background-image:
    linear-gradient(160deg, rgba(255, 200, 90, 0.18) 0%, rgba(255, 200, 90, 0.06) 45%, rgba(120, 90, 160, 0.1) 100%),
    radial-gradient(95% 75% at 15% 5%, rgba(255, 205, 95, 0.45) 0%, rgba(255, 190, 80, 0.16) 38%, transparent 68%),
    radial-gradient(125% 85% at 50% 40%, rgba(255, 230, 180, 0.06) 0%, rgba(50, 25, 0, 0.32) 55%, rgba(25, 12, 0, 0.72) 100%);
}

/* ---- Blueprint ------------------------------------------------------------
   A map drawn as a plan: white ink on drafting blue. The terrain goes to a
   Prussian-blue duotone, a faint drafting grid lies over it, and the chrome
   becomes the same sheet of paper. The border is orange, the colour of a
   revision pen, so "the world stops here" still reads as a warning against all
   that blue. Like the unlockables this retints the panels, because grey chrome
   on blue paper would look like a half-finished theme rather than a plan. */
:root[data-theme="blueprint"] {
  --spk-accent: linear-gradient(100deg, #ffffff 0%, #dbe9ff 50%, #9fc0ff 100%);
  --spk-accent-solid: #eaf2ff;
  --spk-edge: #ff8a3d;
  --spk-ribbon: linear-gradient(
    90deg,
    #eaf2ff 0%,
    #0b2a5c 24%,
    #6ea0ff 50%,
    #0b2a5c 76%,
    #eaf2ff 100%
  );
  --spk-void: #061a3a;
  --spk-panel: #0b2a5c;
  --spk-panel-raised: #10366f;
  --spk-line: rgba(234, 242, 255, 0.18);
  --spk-line-strong: rgba(234, 242, 255, 0.42);
  --ui-background: rgba(11, 42, 92, 0.94);
  --ui-background-hover: rgba(16, 54, 111, 0.97);
  --ui-background-active: rgba(22, 66, 130, 0.97);
  --ui-background-selected: rgba(19, 60, 120, 0.97);
  --ui-background-disabled: rgba(8, 30, 66, 0.9);
  --spk-tint: grayscale(1) sepia(1) hue-rotate(195deg) saturate(2.4) brightness(0.68)
    contrast(1.15);
  --spk-rc-casing: rgba(255, 255, 255, 0.3);
}

/* ---- Bees of Ra -----------------------------------------------------------
   A tribute theme, in honour of a player who has moved on. Honey and black:
   the map is warmed to gold, the chrome goes to a dark honey-black with a
   striped ribbon, the header carries honeycomb, a sun and two bees crossing
   it, and a small plaque on the map reads "In Honor of Ra The Sun Deity" -
   spektral.js builds that whenever this theme is applied. In the myth, bees
   were born from the tears of Ra. */
:root[data-theme="bees"] {
  --spk-accent: linear-gradient(100deg, #ffe08a 0%, #f5b301 50%, #d98c00 100%);
  --spk-accent-solid: #f5b301;
  --spk-edge: #ff4d1f;
  /* Bee stripes. Ten equal bands, so the drifting ribbon loops on a band
     boundary and never shows a seam. */
  --spk-ribbon: linear-gradient(
    90deg,
    #f5b301 0%, #f5b301 10%, #0b0900 10%, #0b0900 20%,
    #f5b301 20%, #f5b301 30%, #0b0900 30%, #0b0900 40%,
    #f5b301 40%, #f5b301 50%, #0b0900 50%, #0b0900 60%,
    #f5b301 60%, #f5b301 70%, #0b0900 70%, #0b0900 80%,
    #f5b301 80%, #f5b301 90%, #0b0900 90%, #0b0900 100%
  );
  --spk-void: #0a0700;
  --spk-panel: #171100;
  --spk-panel-raised: #241a00;
  --spk-line: rgba(245, 179, 1, 0.22);
  --spk-line-strong: rgba(245, 179, 1, 0.5);
  --ui-background: rgba(23, 17, 0, 0.94);
  --ui-background-hover: rgba(36, 26, 0, 0.97);
  --ui-background-active: rgba(50, 36, 0, 0.97);
  --ui-background-selected: rgba(44, 32, 0, 0.97);
  --ui-background-disabled: rgba(12, 9, 0, 0.9);
  --spk-tint: sepia(0.55) saturate(1.6) hue-rotate(-10deg) brightness(1.04) contrast(1.06);
  /* Bee stripes on the ground too: black outlines with a light edge. */
  --spk-rc-casing: rgba(255, 255, 255, 0.5);
  --spk-rc-road: #3f2a00;
  --spk-rc-shop: #0b0b0b;
  --spk-rc-townhall: #7f1d1d;
  --spk-rc-netherhub: #ffffff;
  --spk-rc-spawn: #0b0b0b;
}

:root[data-theme="bees"] .map::after {
  background-image: radial-gradient(
    125% 85% at 50% 40%,
    rgba(255, 210, 90, 0.08) 0%,
    rgba(40, 24, 0, 0.5) 55%,
    rgba(12, 8, 0, 0.88) 100%
  );
}

/* Halloween. Pumpkin-orange ground under a purple-black sky: the map goes to
   dusk, the chrome to the same purple, and a moon and bats take the header.
   The first version only nudged the tint and left the chrome grey, and the
   owner called it weak. He was right. */
:root[data-theme="halloween"] {
  --spk-accent: linear-gradient(100deg, #ff7518 0%, #ff9d3c 45%, #b14aed 100%);
  --spk-accent-solid: #ff8c2b;
  /* Purple, so the border stays distinct from the orange accent. */
  --spk-edge: #b14aed;
  --spk-ribbon: linear-gradient(
    90deg,
    #ff7518 0%,
    #241633 22%,
    #b14aed 46%,
    #241633 72%,
    #ff7518 100%
  );
  --spk-void: #07030c;
  --spk-panel: #150a20;
  --spk-panel-raised: #21113a;
  --spk-line: rgba(177, 74, 237, 0.22);
  --spk-line-strong: rgba(177, 74, 237, 0.48);
  --ui-background: rgba(21, 10, 32, 0.94);
  --ui-background-hover: rgba(33, 17, 58, 0.97);
  --ui-background-active: rgba(46, 24, 78, 0.97);
  --ui-background-selected: rgba(40, 21, 68, 0.97);
  --ui-background-disabled: rgba(12, 6, 20, 0.9);
  --spk-tint: sepia(0.5) hue-rotate(-25deg) saturate(1.6) brightness(0.76) contrast(1.18);
  /* Yellow and orange plots vanish on orange ground: shops go slime green,
     the hall goes black, the nether hub bone white. */
  --spk-rc-shop: #a3e635;
  --spk-rc-landmark: #c084fc;
  --spk-rc-townhall: #0f0f14;
  --spk-rc-netherhub: #f8fafc;
}

/* Winter. The world under snow: colour drained to near white with an icy
   cast, water left dark, slate-blue chrome, snow falling in the header and
   icicles hanging under it. */
:root[data-theme="winter"] {
  --spk-accent: linear-gradient(100deg, #a5f3fc 0%, #38bdf8 50%, #e0f2fe 100%);
  /* Deeper than the old sky blue so it still reads on the pale map. */
  --spk-accent-solid: #38bdf8;
  /* Amber against all that blue - a warning has to read as a warning. */
  --spk-edge: #fbbf24;
  --spk-ribbon: linear-gradient(
    90deg,
    #e0f2fe 0%,
    #1a2733 24%,
    #7dd3fc 50%,
    #1a2733 76%,
    #e0f2fe 100%
  );
  --spk-void: #06101a;
  --spk-panel: #0d1b28;
  --spk-panel-raised: #14283a;
  --spk-line: rgba(125, 211, 252, 0.2);
  --spk-line-strong: rgba(125, 211, 252, 0.46);
  --ui-background: rgba(13, 27, 40, 0.94);
  --ui-background-hover: rgba(20, 40, 58, 0.97);
  --ui-background-active: rgba(28, 54, 78, 0.97);
  --ui-background-selected: rgba(24, 48, 70, 0.97);
  --ui-background-disabled: rgba(6, 14, 22, 0.9);
  --spk-tint: grayscale(0.85) sepia(0.3) hue-rotate(170deg) saturate(1.8) brightness(1.18) contrast(0.95);
  /* Pale outlines disappear on snow: the road goes slate, shops amber, the
     dock and the spawn boundary a deeper blue. */
  --spk-rc-road: #334155;
  --spk-rc-shop: #f59e0b;
  --spk-rc-dock: #0284c7;
  --spk-rc-spawn: #0369a1;
}

/* Cherry Bloom. The whole world in blossom: a rose duotone map, plum chrome,
   a branch in flower across the header with petals drifting over it. */
:root[data-theme="bloom"] {
  --spk-accent: linear-gradient(100deg, #fbcfe8 0%, #f472b6 50%, #f0abfc 100%);
  --spk-accent-solid: #f472b6;
  /* Leaf green: the one thing that still reads as a warning on a pink map. */
  --spk-edge: #15803d;
  --spk-ribbon: linear-gradient(
    90deg,
    #fbcfe8 0%,
    #2a1a24 23%,
    #f0abfc 49%,
    #2a1a24 74%,
    #fbcfe8 100%
  );
  --spk-void: #12060d;
  --spk-panel: #1f0f18;
  --spk-panel-raised: #2c1724;
  --spk-line: rgba(244, 114, 182, 0.2);
  --spk-line-strong: rgba(244, 114, 182, 0.46);
  --ui-background: rgba(31, 15, 24, 0.94);
  --ui-background-hover: rgba(44, 23, 36, 0.97);
  --ui-background-active: rgba(60, 31, 49, 0.97);
  --ui-background-selected: rgba(52, 27, 43, 0.97);
  --ui-background-disabled: rgba(14, 6, 10, 0.9);
  --spk-tint: grayscale(0.9) sepia(0.9) hue-rotate(292deg) saturate(1.5) brightness(1.12) contrast(0.95);
  /* Pink and pale colours melt into a pink map: NPC plots go teal, landmarks
     deep violet, the hall ink, the road plum. */
  --spk-rc-road: #4a044e;
  --spk-rc-landmark: #6d28d9;
  --spk-rc-townhall: #1e293b;
  --spk-rc-netherhub: #c2410c;
  --spk-rc-npc: #0d9488;
  --spk-rc-spawn: #9f1239;
}

/* ---- 500 Discord Members: cyberpunk ------------------------------------
   Discord blurple pushed through a neon filter. The map goes cold cyan-blue
   duotone so the hot magenta and blurple of the interface read as light
   sources against it - that contrast between a dead world and living neon is
   the whole cyberpunk idiom.

   Like The End, this is an UNLOCKABLE theme and so is allowed past the
   four-token rule the everyday themes follow. Earning it should feel like
   more than a hue change.
   ---------------------------------------------------------------------- */
:root[data-theme="cyber"] {
  --spk-accent: linear-gradient(100deg, #00e5ff 0%, #5865f2 50%, #ff2e88 100%);
  /* Bright enough to work as the focus ring on a near-black panel. */
  --spk-accent-solid: #7c8cff;
  --spk-edge: #ff2e88;
  --spk-ribbon: linear-gradient(
    90deg,
    #00e5ff 0%,
    #08081a 22%,
    #5865f2 48%,
    #08081a 74%,
    #ff2e88 100%
  );
  --spk-void: #04040d;
  --spk-panel: #0c0c1b;
  --spk-panel-raised: #14142a;
  --spk-line: rgba(124, 140, 255, 0.2);
  --spk-line-strong: rgba(124, 140, 255, 0.46);
  --ui-background: rgba(12, 12, 27, 0.94);
  --ui-background-hover: rgba(22, 22, 46, 0.97);
  --ui-background-active: rgba(32, 32, 64, 0.97);
  --ui-background-selected: rgba(28, 28, 56, 0.97);
  --ui-background-disabled: rgba(8, 8, 18, 0.9);
  /* Tested live, not guessed. The previous values just made a navy map - the
     crush is what matters: darks go to near-black and highlights blow out to
     electric blue, so the terrain reads as neon-lit rather than tinted. */
  --spk-tint: grayscale(1) sepia(1) hue-rotate(186deg) saturate(3.8) brightness(0.52)
    contrast(1.7);
  --spk-rc-casing: rgba(255, 255, 255, 0.3);
}

/* ---- The End ------------------------------------------------------------
   The reward theme, and the only one allowed to be genuinely strange. Four
   things happen here that happen nowhere else:

     1. the void turns violet and gains a slowly drifting starfield
     2. the terrain BREATHES - a slow hue and contrast cycle, so the map is
        never quite still
     3. particles rise instead of falling, which is the single cheapest way to
        signal "the rules are different here"
     4. the region outlines glow

   All of it is CSS. None of it costs anything on the other six themes.
   ---------------------------------------------------------------------- */
:root[data-theme="theend"] {
  --spk-accent: linear-gradient(100deg, #e9d5ff 0%, #c084fc 50%, #7c3aed 100%);
  --spk-accent-solid: #cba6ff;
  --spk-edge: #f0abfc;
  --spk-ribbon: linear-gradient(
    90deg,
    #e9d5ff 0%,
    #12031f 22%,
    #7c3aed 48%,
    #12031f 74%,
    #e9d5ff 100%
  );
  --spk-void: #0a0016;
  --spk-ember: rgba(226, 200, 255, 0.95);
  --spk-rc-casing: rgba(255, 255, 255, 0.3);

  /* This theme DELIBERATELY breaks the four-token rule the others follow.

     The rule exists to keep the common case cheap - six themes should not mean
     six values for every new token. But The End is the one people unlock, and
     neutral grey panels sitting on a violet void undercut the whole reveal. A
     flagship is allowed to spend more; the others still must not. */
  --spk-panel: #170a26;
  --spk-panel-raised: #23103a;
  --spk-line: rgba(203, 166, 255, 0.16);
  --spk-line-strong: rgba(203, 166, 255, 0.4);
  --ui-background: rgba(23, 10, 38, 0.94);
  --ui-background-hover: rgba(38, 18, 60, 0.97);
  --ui-background-active: rgba(52, 25, 80, 0.97);
  --ui-background-selected: rgba(46, 21, 72, 0.97);
  --ui-background-disabled: rgba(14, 5, 24, 0.9);
}

/* The terrain never settles. Slow enough that you notice it without being able
   to point at it, which is the effect worth having. */
:root[data-theme="theend"] .leaflet-tile-container {
  animation: spk-void-breathe 16s ease-in-out infinite;
}

/* grayscale -> sepia -> hue-rotate, not hue-rotate alone.

   Rotating the hue of a finished image shifts every colour RELATIVE to what it
   already was, so the result depends entirely on the source. On the overworld
   that turned the oceans blood red; the nether, already red, went somewhere
   else again. Stripping to grey first and re-tinting through sepia throws the
   original hues away, so every dimension lands on the same violet.

   Note the contrast is BELOW 1. Raising it stretches the bright end further
   apart, which kept the grass pale lilac no matter how far brightness came
   down - it read as cotton candy rather than a void. Compressing the range
   first and then sinking it is what makes the terrain go deep. */
@keyframes spk-void-breathe {
  0%,
  100% {
    filter: grayscale(1) sepia(1) hue-rotate(232deg) saturate(2.9) brightness(0.5)
      contrast(0.84);
  }
  50% {
    filter: grayscale(1) sepia(1) hue-rotate(244deg) saturate(3.3) brightness(0.6)
      contrast(0.9);
  }
}

/* Replaces the neutral vignette with a violet void and three parallax star
   layers, each drifting at its own rate. The last background is the vignette
   itself, kept so unrendered areas still read as empty rather than broken. */
:root[data-theme="theend"] .map::after {
  background-image:
    radial-gradient(circle at 12% 18%, rgba(255, 255, 255, 0.9) 0 1px, transparent 1.5px),
    radial-gradient(circle at 63% 41%, rgba(226, 200, 255, 0.8) 0 1.2px, transparent 1.7px),
    radial-gradient(circle at 84% 77%, rgba(255, 255, 255, 0.65) 0 0.9px, transparent 1.4px),
    radial-gradient(
      125% 85% at 50% 40%,
      rgba(124, 58, 237, 0.2) 0%,
      rgba(20, 4, 38, 0.62) 55%,
      rgba(6, 0, 14, 0.93) 100%
    );
  background-size: 300px 300px, 220px 220px, 160px 160px, cover;
  animation: spk-stars 90s linear infinite;
}

@keyframes spk-stars {
  to {
    background-position: 300px 0, -220px 0, 160px 0, 0 0;
  }
}

/* Outlines glow, because everything in the End does. */
:root[data-theme="theend"] .spk-region {
  filter: drop-shadow(0 0 5px var(--rc, var(--spk-accent-solid)));
}

/* The tint is only ever applied while a theme defines one. Painting a filter
   on the tile container unconditionally - even `none` - promotes it to its own
   compositing layer for no reason. The End drives its own via the animation
   above, so it is excluded here to avoid two filters fighting. */
:root[data-theme="night"] .leaflet-tile-container,
:root[data-theme="toussaint"] .leaflet-tile-container,
:root[data-theme="blueprint"] .leaflet-tile-container,
:root[data-theme="bees"] .leaflet-tile-container,
:root[data-theme="halloween"] .leaflet-tile-container,
:root[data-theme="winter"] .leaflet-tile-container,
:root[data-theme="cyber"] .leaflet-tile-container,
:root[data-theme="bloom"] .leaflet-tile-container {
  filter: var(--spk-tint, none);
}


/* The End's breathing and starfield stop under reduced motion - motion is the
   whole point of both. */
@media (prefers-reduced-motion: reduce) {
  :root[data-theme="theend"] .leaflet-tile-container,
  :root[data-theme="theend"] .map::after {
    animation: none;
  }
  :root[data-theme="theend"] .leaflet-tile-container {
    filter: grayscale(1) sepia(1) hue-rotate(238deg) saturate(3) brightness(0.55)
      contrast(0.87);
  }
}
/* ==========================================================================
   Town panel
   A sidebar tab (spektral.js, initTown) for what the map draws over the town:
   outlines, names, faces and the key can each be switched off, claimed plots
   can be hidden to leave only what is still for sale, and a button flies to
   a free plot. Choices are classes on <html>, so the stylesheet does the
   hiding and nothing is redrawn; the feed keeps polling underneath. They are
   remembered per browser.
   ========================================================================== */
#spk-town {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.spk-town__lead {
  margin: 0 0 0.8rem;
  font-size: 0.82rem;
  line-height: 1.4;
  opacity: 0.7;
}

.spk-town__h {
  margin: 1.1rem 0 0.35rem;
  font-size: 0.66rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 0.55;
}

.spk-town__row {
  display: flex;
  align-items: flex-start;
  gap: 0.65rem;
  padding: 0.5rem 0.55rem;
  border-radius: 8px;
  cursor: pointer;
  font-size: 0.9rem;
  line-height: 1.25;
}

.spk-town__row:hover {
  background: var(--ui-background-hover);
}

.spk-town__row input {
  flex: none;
  width: 16px;
  height: 16px;
  margin: 0.1rem 0 0;
  accent-color: var(--spk-accent-solid);
}

.spk-town__row small {
  display: block;
  margin-top: 0.1rem;
  font-size: 0.74rem;
  opacity: 0.6;
}

.spk-town__btn {
  display: block;
  width: 100%;
  margin-top: 0.7rem;
  padding: 0.6rem 0.7rem;
  border: 0;
  border-radius: 8px;
  background: var(--spk-accent-solid);
  color: #10120f;
  font: inherit;
  font-weight: 700;
  cursor: pointer;
}

.spk-town__btn:hover {
  filter: brightness(1.08);
}

.spk-town__btn:disabled {
  opacity: 0.45;
  cursor: default;
  filter: none;
}

.spk-town__hint {
  margin: 0.55rem 0 0;
  font-size: 0.76rem;
  line-height: 1.4;
  opacity: 0.6;
}

/* The switches. !important because some of what they hide is SHOWN by rules
   of higher specificity (the zoom classes on the map container). */
:root.spk-town-noregions .spk-region,
:root.spk-town-noregions .spk-region-casing,
:root.spk-town-noregions .spk-plot-head,
:root.spk-town-noregions .leaflet-tooltip.spk-plot-label,
:root.spk-town-noregions .spk-legend,
:root.spk-town-nolabels .leaflet-tooltip.spk-plot-label,
:root.spk-town-noheads .spk-plot-head,
:root.spk-town-nolegend .spk-legend,
:root.spk-town-onlyfree .spk-region:not(.spk-region--free):not(.spk-region--type-spawn):not(.spk-region--type-road),
:root.spk-town-onlyfree .spk-region-casing:not(.spk-region-casing--free):not(.spk-region-casing--type-road),
:root.spk-town-onlyfree .spk-plot-head,
:root.spk-town-onlyfree .leaflet-tooltip.spk-plot-label {
  display: none !important;
}

/* ---- tribute plaque (Bees of Ra) ------------------------------------------
   Built by spektral.js when that theme is applied. Opens as a small bar at
   the bottom of the map, folds itself into a sun badge after a few seconds,
   opens again on click. The close button dismisses it until the theme is
   next applied. */
.spk-tribute {
  position: fixed;
  left: 50%;
  bottom: 1.2rem;
  z-index: 950;
  display: flex;
  align-items: center;
  gap: 0.7rem;
  max-width: calc(100vw - 2rem);
  padding: 0.5rem 0.55rem 0.5rem 0.6rem;
  border-radius: 999px;
  border: 1px solid rgba(245, 179, 1, 0.5);
  background: rgba(23, 17, 0, 0.94);
  box-shadow:
    0 6px 24px rgba(0, 0, 0, 0.55),
    0 0 18px rgba(245, 179, 1, 0.2);
  color: #ffe9b0;
  font-family: var(--ui-font);
  font-size: 0.86rem;
  line-height: 1.2;
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  transform: translateX(-50%);
  animation: spk-tribute-in 600ms ease-out;
}

@keyframes spk-tribute-in {
  from {
    opacity: 0;
    transform: translate(-50%, 10px);
  }
}

.spk-tribute__sun {
  flex: none;
  width: 30px;
  height: 30px;
  color: #f5b301;
  filter: drop-shadow(0 0 6px rgba(245, 179, 1, 0.55));
}

.spk-tribute__text b {
  display: block;
  font-weight: 700;
  color: #fff3c4;
  letter-spacing: 0.01em;
}

.spk-tribute__text span {
  display: block;
  margin-top: 0.1rem;
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  opacity: 0.75;
}

.spk-tribute__close {
  flex: none;
  width: 26px;
  height: 26px;
  margin-left: 0.2rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.08);
  color: inherit;
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
}

.spk-tribute__close:hover {
  background: rgba(255, 255, 255, 0.16);
}

.spk-tribute.is-collapsed {
  gap: 0;
  padding: 0.35rem;
  cursor: pointer;
}

.spk-tribute.is-collapsed .spk-tribute__text,
.spk-tribute.is-collapsed .spk-tribute__close {
  display: none;
}

/* On a phone the centre of the bottom edge is where the coordinates box
   already is, so the plaque sits a step above it. */
@media (max-width: 640px) {
  .spk-tribute {
    bottom: 4.8rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spk-tribute {
    animation: none;
  }
}

/* ---- theme picker --------------------------------------------------------
   Lives in the header rather than the sidebar. The sidebar is map data -
   worlds, layers, players, places - and a theme is a preference about the
   site, so it sits beside the server IP and the Discord link instead of
   adding a fifth button to a column of map controls.

   A popover, not a panel: seven short rows do not need a full-height sidebar.
   The row IS the control, so the native radio is hidden and :has() carries the
   checked and focus states.
   ---------------------------------------------------------------------- */

/* Matched to the IP and online chips beside it - those use a hairline border
   over a barely-there wash, not the heavier --ui-background panel fill, and a
   button that does not match its neighbours reads as bolted on. */
.spk-themebtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.6rem;
  height: 3.6rem;
  padding: 0;
  border-radius: var(--ui-border-radius);
  border: 1px solid var(--spk-line);
  background: rgba(255, 255, 255, 0.03);
  color: var(--spk-text);
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}

.spk-themebtn:hover {
  background: var(--ui-background-hover);
  border-color: var(--spk-line-strong);
}

.spk-themebtn[aria-expanded="true"] {
  background: var(--ui-background-active);
  border-color: var(--spk-accent-solid);
}

.spk-themebtn .svg-icon {
  width: 1.9rem;
  height: 1.9rem;
}

/* Fixed, and a sibling of the header rather than a child - Fantasy and The
   End clip the header with overflow:hidden for their runes and motes, which
   would erase this popover. z-index clears the header (1500) and every Leaflet
   control (1000). */
.spk-themes {
  position: fixed;
  z-index: 1600;
  top: calc(var(--spk-header-h) + 0.8rem);
  right: 1.8rem;
  width: 23rem;
  max-height: calc(100vh - var(--spk-header-h) - 3rem);
  overflow-y: auto;
  padding: 1rem;
  border-radius: var(--ui-border-radius);
  border: var(--ui-border);
  background: var(--ui-background);
  box-shadow: var(--ui-box-shadow), 0 12px 34px rgba(0, 0, 0, 0.55);
}

.spk-themes__heading {
  margin: 1.2rem 0 0.4rem;
  padding: 0 0.5rem;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--spk-text-faint);
}

.spk-themes__row {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.55rem 0.6rem;
  border-radius: 0.55rem;
  cursor: pointer;
  transition: background 110ms ease-out;
}

.spk-themes__row:hover {
  background: rgba(255, 255, 255, 0.08);
}

.spk-themes__row input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.spk-themes__row:has(input:checked) {
  background: rgba(255, 255, 255, 0.12);
  box-shadow: inset 0 0 0 1px var(--spk-accent-solid);
}

/* Focus must stay visible even though the input itself is hidden. */
.spk-themes__row:has(input:focus-visible) {
  outline: 2px solid var(--spk-accent-solid);
  outline-offset: 1px;
}

/* Locked rows stay visible rather than hidden: seeing that there is more to
   come is the point, and hovering still explains why it is shut. */
.spk-themes__row.is-locked {
  cursor: not-allowed;
  opacity: 0.45;
}

.spk-themes__row.is-locked:hover {
  background: transparent;
}

/* A single clear colour per theme. The previous version used split-gradient
   rectangles which, at that size, just read as muddy blocks. */
.spk-themes__dot {
  flex: 0 0 auto;
  width: 1.5rem;
  height: 1.5rem;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.22);
}

.spk-themes__label {
  flex: 1 1 auto;
  font-size: 1.3rem;
  font-weight: 600;
  white-space: nowrap;
}

.spk-tile__lock {
  flex: 0 0 auto;
  width: 1.4rem;
  height: 1.4rem;
  opacity: 0.85;
}

/* Dots show each theme's OWN accent rather than the active one, so the list
   reads as a set of choices. Duplicated from the theme blocks on purpose: a
   dot must show a theme that is not currently applied, so it cannot use
   var(--spk-accent). */
.spk-themes__dot[data-swatch="auto"] {
  background: conic-gradient(#e6e6e6, #22304a, #e9b949, #1c4fa3, #e6e6e6);
}
.spk-themes__dot[data-swatch="dark"] {
  background: #e6e6e6;
}
/* Night's accent is near-white like the default's, so its dot shows the map
   instead: a deep night blue. */
.spk-themes__dot[data-swatch="night"] {
  background: #22304a;
}
.spk-themes__dot[data-swatch="toussaint"] {
  background: #e9b949;
}
.spk-themes__dot[data-swatch="blueprint"] {
  background: #1c4fa3;
}
.spk-themes__dot[data-swatch="bees"] {
  background: #f5b301;
}
.spk-themes__dot[data-swatch="theend"] {
  background: #cba6ff;
}
.spk-themes__dot[data-swatch="halloween"] {
  background: #ff8c2b;
}
.spk-themes__dot[data-swatch="winter"] {
  background: #7dd3fc;
}
.spk-themes__dot[data-swatch="bloom"] {
  background: #f9a8d4;
}

/* The header hides the wordmark below 640px; keep the popover inside the
   viewport rather than letting it hang off the right edge. */
@media (max-width: 480px) {
  .spk-themes {
    right: 1.1rem;
    left: 1.1rem;
    width: auto;
  }
}

.spk-place-tip {
  display: flex;
  flex-direction: column;
  gap: 0.2rem;
}

.spk-place-tip strong {
  font-size: 1.25rem;
  font-weight: 700;
  color: var(--spk-white);
}

.spk-place-tip span {
  font-size: 1.1rem;
  color: var(--spk-g1);
  font-weight: 500;
}

.spk-places__error {
  color: #ff8f8f;
  font-style: normal;
  font-family: var(--ui-mono);
  font-size: 1.2rem;
  line-height: 1.7rem;
}




/* ---- wordmark shimmer ---------------------------------------------------
   A highlight sweeps across the SPEKTRAL lettering every few seconds, tinted
   by whichever theme is active.

   The wordmark is an SVG IMAGE, not text, so background-clip:text is not
   available. Instead an overlay carries the moving gradient and is MASKED by
   the same SVG - the mask confines the highlight to the letterforms, so it
   lights up the glyphs rather than sweeping a band across the whole box.

   The gradient runs accent -> white -> accent rather than plain white: the
   bright core reads as a shine, and the coloured edges carry the theme, so
   this animates differently in Fantasy gold than in End violet without any
   per-theme code.
   ---------------------------------------------------------------------- */
.spk-word__mark {
  position: relative;
}

.spk-word__mark::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(
    105deg,
    transparent 38%,
    var(--spk-accent-solid) 46%,
    #ffffff 50%,
    var(--spk-accent-solid) 54%,
    transparent 62%
  );
  background-size: 300% 100%;
  background-repeat: no-repeat;
  -webkit-mask: url("images/spektral-wordmark.svg") no-repeat left center / contain;
  mask: url("images/spektral-wordmark.svg") no-repeat left center / contain;
  animation: spk-shine 7s ease-in-out infinite;
}

/* The sweep takes about a third of the cycle and the rest is still - a
   highlight that never stops reads as a loading bar rather than a shine. */
@keyframes spk-shine {
  0% {
    background-position: 210% 0;
  }
  34% {
    background-position: -110% 0;
  }
  100% {
    background-position: -110% 0;
  }
}

/* ---- header atmosphere --------------------------------------------------
   The header is where a theme's personality lives: it is small, so an effect
   there costs almost nothing, unlike one across the whole map. Default and
   Night keep a clean bar - the effect is worth more where it is rarer.

     Toussaint    - a band of afternoon light, drifting very slowly
     Blueprint    - drafting grid, still
     Bees of Ra   - honeycomb and a sun, two bees crossing
     Halloween    - a moon, bats crossing
     Winter       - snow falling, icicles hanging under the bar
     Cherry Bloom - a branch in flower, petals drifting
     Cyberpunk    - faint scanlines, a neon rule that breathes
     The End      - void motes rising

   All are drawn on the header's pseudo-elements, so the brand and buttons are
   lifted to z-index 1 to stay above them. Winter is left out of the overflow
   clip on purpose: its icicles hang below the bar.
   ---------------------------------------------------------------------- */
:root[data-theme="toussaint"] #spk-header,
:root[data-theme="blueprint"] #spk-header,
:root[data-theme="bees"] #spk-header,
:root[data-theme="halloween"] #spk-header,
:root[data-theme="bloom"] #spk-header,
:root[data-theme="theend"] #spk-header {
  overflow: hidden;
}

:root[data-theme="toussaint"] #spk-header > *,
:root[data-theme="blueprint"] #spk-header > *,
:root[data-theme="bees"] #spk-header > *,
:root[data-theme="halloween"] #spk-header > *,
:root[data-theme="winter"] #spk-header > *,
:root[data-theme="bloom"] #spk-header > *,
:root[data-theme="theend"] #spk-header > * {
  position: relative;
  z-index: 1;
}

/* ---- Toussaint: afternoon light ------------------------------------------
   No runes. A warm band of light lies across the bar and drifts very slowly
   from one end to the other and back, the way sun through a tall window moves
   over an afternoon. Gold in the middle, a breath of lavender at the trailing
   edge. Slow enough that nobody catches it moving.
   ---------------------------------------------------------------------- */
:root[data-theme="toussaint"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: linear-gradient(
    100deg,
    transparent 0%,
    rgba(233, 185, 73, 0.14) 35%,
    rgba(255, 240, 200, 0.24) 50%,
    rgba(181, 154, 214, 0.12) 66%,
    transparent 100%
  );
  background-size: 55% 100%;
  background-repeat: no-repeat;
  animation: spk-sunband 60s ease-in-out infinite alternate;
}

@keyframes spk-sunband {
  from {
    background-position: -30% 0;
  }
  to {
    background-position: 130% 0;
  }
}

/* ---- Blueprint: drafting paper -------------------------------------------
   The same faint grid the map carries, on the bar, and nothing that moves. */
:root[data-theme="blueprint"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(234, 242, 255, 0.08) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(234, 242, 255, 0.08) 1px, transparent 1px);
  background-size: 32px 32px;
}

/* ---- Halloween: a low moon and bats ---------------------------------------
   The moon hangs still in the empty middle of the bar with an orange glow
   along the bottom edge; two flights of bats cross it right to left, the
   nearer ones larger and faster. Each flight drifts by a whole number of tile
   widths per cycle, so the loop never jumps. Masked to the stretch between
   the brand and the buttons so nothing flies through the wordmark. */
:root[data-theme="halloween"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 44% 42%, rgba(255, 228, 160, 0.95) 0 7px, rgba(255, 200, 100, 0.35) 8px, rgba(255, 160, 60, 0.12) 15px, transparent 28px),
    linear-gradient(to top, rgba(255, 117, 24, 0.2), transparent 55%);
}

:root[data-theme="halloween"] #spk-header::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 150 60'%3E%3Cg fill='%23dcb3ff' fill-opacity='0.85'%3E%3Cpath transform='translate(22 34) scale(1.25)' d='M0 8C3 2 7 3 9 5L10 3.5 11 5 13 5 14 3.5 15 5C17 3 21 2 24 8C21 6 19 7 18 9.5C16.5 7.5 14.5 8 13 10.5C12.5 11 11.5 11 11 10.5C9.5 8 7.5 7.5 6 9.5C5 7 3 6 0 8Z'/%3E%3Cpath transform='translate(96 12) scale(1.05)' d='M0 8C3 2 7 3 9 5L10 3.5 11 5 13 5 14 3.5 15 5C17 3 21 2 24 8C21 6 19 7 18 9.5C16.5 7.5 14.5 8 13 10.5C12.5 11 11.5 11 11 10.5C9.5 8 7.5 7.5 6 9.5C5 7 3 6 0 8Z'/%3E%3C/g%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 210 60'%3E%3Cg fill='%23b14aed' fill-opacity='0.6'%3E%3Cpath transform='translate(16 14) scale(0.85)' d='M0 8C3 2 7 3 9 5L10 3.5 11 5 13 5 14 3.5 15 5C17 3 21 2 24 8C21 6 19 7 18 9.5C16.5 7.5 14.5 8 13 10.5C12.5 11 11.5 11 11 10.5C9.5 8 7.5 7.5 6 9.5C5 7 3 6 0 8Z'/%3E%3Cpath transform='translate(112 40) scale(0.7)' d='M0 8C3 2 7 3 9 5L10 3.5 11 5 13 5 14 3.5 15 5C17 3 21 2 24 8C21 6 19 7 18 9.5C16.5 7.5 14.5 8 13 10.5C12.5 11 11.5 11 11 10.5C9.5 8 7.5 7.5 6 9.5C5 7 3 6 0 8Z'/%3E%3Cpath transform='translate(168 24) scale(0.6)' d='M0 8C3 2 7 3 9 5L10 3.5 11 5 13 5 14 3.5 15 5C17 3 21 2 24 8C21 6 19 7 18 9.5C16.5 7.5 14.5 8 13 10.5C12.5 11 11.5 11 11 10.5C9.5 8 7.5 7.5 6 9.5C5 7 3 6 0 8Z'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 150px 60px, 210px 60px;
  animation: spk-bats 24s linear infinite;
  -webkit-mask-image: linear-gradient(to right, transparent 20%, #000 28%, #000 54%, transparent 62%);
  mask-image: linear-gradient(to right, transparent 20%, #000 28%, #000 54%, transparent 62%);
}

@keyframes spk-bats {
  to {
    background-position: -300px 0, -210px 0;
  }
}

/* ---- Winter: snowfall and icicles -----------------------------------------
   Snow falls across the whole bar, three sizes at three speeds, fading in at
   the top edge. Icicles hang from the bar's bottom edge OVER the map: the
   header is not overflow-hidden on this theme so they can. */
:root[data-theme="winter"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.75;
  background-image:
    radial-gradient(circle at 22% 22%, rgba(255, 255, 255, 0.95) 0 1.7px, transparent 2.2px),
    radial-gradient(circle at 68% 48%, rgba(255, 255, 255, 0.85) 0 1.2px, transparent 1.7px),
    radial-gradient(circle at 45% 78%, rgba(255, 255, 255, 0.7) 0 0.9px, transparent 1.3px);
  background-size: 96px 60px, 64px 46px, 44px 34px;
  animation: spk-snowfall 16s linear infinite;
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 35%);
  mask-image: linear-gradient(to bottom, transparent 0, #000 35%);
}

@keyframes spk-snowfall {
  to {
    background-position: 10px 60px, -8px 46px, 6px 34px;
  }
}

:root[data-theme="winter"] #spk-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: 100%;
  height: 22px;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(ellipse 5px 18px at 50% 0, rgba(220, 242, 255, 0.85) 0 55%, transparent 60%),
    radial-gradient(ellipse 3.5px 11px at 50% 0, rgba(220, 242, 255, 0.7) 0 55%, transparent 60%),
    linear-gradient(to bottom, rgba(190, 230, 255, 0.6), transparent 5px);
  background-size: 26px 22px, 15px 22px, 100% 22px;
  background-position: 0 0, 9px 0, 0 0;
  background-repeat: repeat-x, repeat-x, no-repeat;
}

/* ---- Cherry Bloom: a branch in flower, petals drifting --------------------
   The branch lies still across the empty middle of the bar; petals drift down
   and to the right over it and everything else. */
:root[data-theme="bloom"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.95;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 260 52'%3E%3Cg fill='none' stroke='%233b1f1a' stroke-linecap='round'%3E%3Cpath d='M0 46C50 36 90 42 130 24 160 11 200 16 260 6' stroke-width='3'/%3E%3Cpath d='M72 39C80 30 88 27 98 25' stroke-width='2'/%3E%3Cpath d='M150 17C158 26 166 30 176 32' stroke-width='2'/%3E%3Cpath d='M204 15C208 8 214 4 224 3' stroke-width='2'/%3E%3C/g%3E%3Cg fill='%23f9a8d4'%3E%3Ccircle cx='40' cy='38' r='4.5'/%3E%3Ccircle cx='62' cy='40' r='3.5'/%3E%3Ccircle cx='97' cy='25' r='4'/%3E%3Ccircle cx='118' cy='30' r='3'/%3E%3Ccircle cx='146' cy='16' r='4.5'/%3E%3Ccircle cx='176' cy='32' r='3.5'/%3E%3Ccircle cx='190' cy='14' r='4'/%3E%3Ccircle cx='224' cy='4' r='3.5'/%3E%3Ccircle cx='240' cy='10' r='3'/%3E%3C/g%3E%3Cg fill='%23f472b6'%3E%3Ccircle cx='84' cy='30' r='3'/%3E%3Ccircle cx='132' cy='22' r='3.2'/%3E%3Ccircle cx='166' cy='26' r='2.6'/%3E%3Ccircle cx='210' cy='12' r='3'/%3E%3C/g%3E%3Cg fill='%23fff0f7'%3E%3Ccircle cx='40' cy='38' r='1.3'/%3E%3Ccircle cx='97' cy='25' r='1.2'/%3E%3Ccircle cx='146' cy='16' r='1.3'/%3E%3Ccircle cx='190' cy='14' r='1.2'/%3E%3C/g%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 40% 58%;
  background-size: 250px 50px;
}

:root[data-theme="bloom"] #spk-header::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.7;
  background-image:
    radial-gradient(ellipse 3px 2px at 30% 30%, rgba(249, 168, 212, 0.95) 0 60%, transparent 75%),
    radial-gradient(ellipse 2.4px 1.6px at 70% 60%, rgba(251, 207, 232, 0.9) 0 60%, transparent 75%),
    radial-gradient(ellipse 2px 1.4px at 50% 85%, rgba(240, 171, 252, 0.85) 0 60%, transparent 75%);
  background-size: 110px 60px, 80px 46px, 60px 34px;
  animation: spk-petals 20s linear infinite;
}

@keyframes spk-petals {
  to {
    background-position: 44px 60px, 32px 46px, 24px 34px;
  }
}

/* ---- Bees of Ra: honeycomb, the sun, and two bees -------------------------
   Honeycomb lies across the whole bar behind everything, brightest in the
   middle, with a sun glowing at its centre. Two bees cross the empty stretch
   left to right, slowly - one lap every forty seconds. */
:root[data-theme="bees"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    radial-gradient(circle at 42% 50%, rgba(255, 214, 80, 0.5) 0 9px, rgba(255, 190, 30, 0.22) 16px, rgba(255, 170, 0, 0.08) 30px, transparent 70px),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cpath fill='%23f5b301' fill-opacity='0.16' d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/svg%3E");
  background-size: auto, 28px 49px;
  -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.3) 0%, #000 38%, #000 62%, rgba(0, 0, 0, 0.3) 100%);
  mask-image: linear-gradient(to right, rgba(0, 0, 0, 0.3) 0%, #000 38%, #000 62%, rgba(0, 0, 0, 0.3) 100%);
}

:root[data-theme="bees"] #spk-header::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 60'%3E%3Cg transform='translate(48 22)'%3E%3Cellipse cx='2' cy='-6' rx='5' ry='2.6' fill='%23ffffff' fill-opacity='0.55' transform='rotate(-25 2 -6)'/%3E%3Cellipse cx='-3' cy='-6' rx='4' ry='2.2' fill='%23ffffff' fill-opacity='0.45' transform='rotate(-35 -3 -6)'/%3E%3Cellipse cx='0' cy='0' rx='8' ry='4.8' fill='%23f5b301'/%3E%3Crect x='-4' y='-4' width='2.4' height='8' fill='%23141000'/%3E%3Crect x='0.5' y='-4.4' width='2.4' height='8.8' fill='%23141000'/%3E%3Ccircle cx='-9' cy='0' r='3.2' fill='%23141000'/%3E%3C/g%3E%3Cg transform='translate(230 40) scale(0.75)'%3E%3Cellipse cx='2' cy='-6' rx='5' ry='2.6' fill='%23ffffff' fill-opacity='0.55' transform='rotate(-25 2 -6)'/%3E%3Cellipse cx='-3' cy='-6' rx='4' ry='2.2' fill='%23ffffff' fill-opacity='0.45' transform='rotate(-35 -3 -6)'/%3E%3Cellipse cx='0' cy='0' rx='8' ry='4.8' fill='%23f5b301'/%3E%3Crect x='-4' y='-4' width='2.4' height='8' fill='%23141000'/%3E%3Crect x='0.5' y='-4.4' width='2.4' height='8.8' fill='%23141000'/%3E%3Ccircle cx='-9' cy='0' r='3.2' fill='%23141000'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 320px 60px;
  animation: spk-bees 40s linear infinite;
  -webkit-mask-image: linear-gradient(to right, transparent 20%, #000 28%, #000 54%, transparent 62%);
  mask-image: linear-gradient(to right, transparent 20%, #000 28%, #000 54%, transparent 62%);
}

@keyframes spk-bees {
  to {
    background-position: 320px 0;
  }
}

/* ---- The End: rising motes ---------------------------------------------- */
:root[data-theme="theend"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: 0.6;
  background-image:
    radial-gradient(circle at 22% 78%, var(--spk-ember) 0 1.5px, transparent 1.9px),
    radial-gradient(circle at 68% 58%, var(--spk-ember) 0 1.1px, transparent 1.5px),
    radial-gradient(circle at 45% 88%, var(--spk-ember) 0 0.8px, transparent 1.2px);
  background-size: 96px 60px, 64px 46px, 44px 34px;
  animation: spk-embers 9s linear infinite;
  /* Fade out towards the top so motes dissolve instead of hitting the edge. */
  -webkit-mask-image: linear-gradient(to top, #000 8%, transparent 92%);
  mask-image: linear-gradient(to top, #000 8%, transparent 92%);
}

@keyframes spk-embers {
  from {
    background-position: 0 0, 0 0, 0 0;
  }
  to {
    background-position: 14px -60px, -11px -46px, 7px -34px;
  }
}

@media (prefers-reduced-motion: reduce) {
  .spk-word__mark::after,
  :root[data-theme="toussaint"] #spk-header::before,
  :root[data-theme="halloween"] #spk-header::after,
  :root[data-theme="winter"] #spk-header::before,
  :root[data-theme="bloom"] #spk-header::after,
  :root[data-theme="bees"] #spk-header::after,
  :root[data-theme="theend"] #spk-header::before {
    display: none;
  }
}

/* ---- attribution ---------------------------------------------------------
   Pl3xMap's on-screen credit, hidden at the site owner's request.

   Note this only removes the rendered badge. The plugin jar, its source and
   its copyright headers are untouched, which is what an open-source licence
   actually asks for - the notice has to travel with the software, not
   necessarily be painted on the page. I could not find a LICENSE file inside
   the jar to confirm Pl3xMap's exact terms, so if you ever redistribute the
   plugin or a modified build, check them.

   Delete this rule to put the credit back.
   ---------------------------------------------------------------------- */
/* !important is required, not lazy: Pl3xMap sets display:inline-block as an
   INLINE style on this element from JavaScript, and an inline style beats any
   normal stylesheet rule. This is the one thing that can win. */
.leaflet-control-attribution {
  display: none !important;
}

/* ---- header refinements --------------------------------------------------
   The actions row carries two different kinds of thing: STATUS (how many are
   on, what the address is) and the CALL TO ACTION (join the Discord). A hair
   rule separates them so the eye groups them correctly instead of reading four
   equally-weighted chips.
   ---------------------------------------------------------------------- */
.spk-discord {
  position: relative;
  margin-left: 0.5rem;
}

.spk-discord::before {
  content: "";
  position: absolute;
  left: -0.9rem;
  top: 22%;
  bottom: 22%;
  width: 1px;
  background: var(--spk-line-strong);
}

/* The dot is real information - somebody is actually on the server right now -
   so it earns a slow pulse. It stops entirely when the server is empty, which
   makes the difference between 0 and 1 player visible at a glance. */
.spk-online:not(.is-empty) .spk-online__dot {
  animation: spk-pulse 2.6s ease-in-out infinite;
}

@keyframes spk-pulse {
  0%,
  100% {
    box-shadow: 0 0 5px rgba(59, 165, 93, 0.55);
    transform: scale(0.9);
  }
  50% {
    box-shadow: 0 0 13px rgba(59, 165, 93, 1);
    transform: scale(1.15);
  }
}

.spk-online.is-empty .spk-online__dot {
  background: var(--spk-g4);
  box-shadow: none;
}

@media (prefers-reduced-motion: reduce) {
  .spk-online:not(.is-empty) .spk-online__dot {
    animation: none;
  }
}

/* ---- 500 Discord Members: header -----------------------------------------
   The first version crawled scanlines down the bar and swept a bright band
   across it every nine seconds, and it gave the site owner a headache. Nothing
   in the header moves now. The CRT texture stays as a static, much fainter set
   of lines, and the neon lives in one place: a thin cyan-to-magenta rule along
   the bottom edge that breathes slowly, like a sign warming up. The map itself
   is untouched - that is the part that looked great.
   ---------------------------------------------------------------------- */
:root[data-theme="cyber"] #spk-header {
  overflow: hidden;
}

:root[data-theme="cyber"] #spk-header > * {
  position: relative;
  z-index: 1;
}

:root[data-theme="cyber"] #spk-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(124, 140, 255, 0.06) 0 1px,
    transparent 1px 4px
  );
}

:root[data-theme="cyber"] #spk-header::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  z-index: 0;
  pointer-events: none;
  background: linear-gradient(90deg, #00e5ff 0%, #5865f2 50%, #ff2e88 100%);
  box-shadow: 0 0 14px rgba(0, 229, 255, 0.45), 0 0 4px rgba(255, 46, 136, 0.5);
  animation: spk-neon-breathe 7s ease-in-out infinite;
}

@keyframes spk-neon-breathe {
  0%,
  100% {
    opacity: 0.55;
  }
  50% {
    opacity: 1;
  }
}

.spk-themes__dot[data-swatch="cyber"] {
  background: linear-gradient(135deg, #00e5ff 0%, #5865f2 50%, #ff2e88 100%);
}

@media (prefers-reduced-motion: reduce) {
  :root[data-theme="cyber"] #spk-header::after {
    animation: none;
  }
}


/* Cyan in the middle, magenta bleeding in at the edges. That duality is the
   cyberpunk signature; a single hue is just a tint. */
:root[data-theme="cyber"] .map::after {
  background-image: radial-gradient(
    120% 80% at 50% 45%,
    rgba(0, 229, 255, 0.08) 0%,
    rgba(6, 6, 20, 0.5) 50%,
    rgba(255, 46, 136, 0.16) 86%,
    rgba(120, 10, 60, 0.32) 100%
  );
}

/* Everything that draws on the map glows, because in this world everything is
   a light source. */
:root[data-theme="cyber"] .spk-region {
  filter: drop-shadow(0 0 5px var(--rc, var(--spk-accent-solid)));
}

:root[data-theme="cyber"] .spk-worldborder {
  filter: drop-shadow(0 0 6px var(--spk-edge));
}

/* ---- Blueprint: drafting grid over the map --------------------------------
   Two grids, a fine one and a heavier one every fifth line, in white ink at
   low opacity. STATIC - nothing drawn over the map body animates on any
   theme, by the site owner's rule (2026-09-04: the falling weather that used
   to live on this layer is gone; header effects carry the seasons instead).

   ON THE MAP PANE, NOT THE CONTAINER. Leaflet's map pane is its own stacking
   context (z-index 400 plus a transform), so a pseudo-element of .map can only
   ever sit under every tile or over every popup, never in between. Drawing on
   the pane itself puts z-index 250 above the tiles and below the outlines,
   markers and cards. The pane is a zero-size box that slides as you drag, so
   the layer is made huge and centred instead of inset; only the visible part
   is painted. The vignette goes to a deep paper blue so unrendered areas read
   as the sheet, not as a hole. */
:root[data-theme="blueprint"] .leaflet-map-pane::before {
  content: "";
  position: absolute;
  left: -10000px; /* see the weather layer for why this is not inset: 0 */
  top: -10000px;
  width: 20000px;
  height: 20000px;
  z-index: 250;
  pointer-events: none;
  background-image:
    linear-gradient(to right, rgba(234, 242, 255, 0.17) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(234, 242, 255, 0.17) 1px, transparent 1px),
    linear-gradient(to right, rgba(234, 242, 255, 0.075) 1px, transparent 1px),
    linear-gradient(to bottom, rgba(234, 242, 255, 0.075) 1px, transparent 1px);
  background-size: 160px 160px, 160px 160px, 32px 32px, 32px 32px;
}

:root[data-theme="blueprint"] .map::after {
  background-image: radial-gradient(
    120% 85% at 50% 45%,
    rgba(30, 80, 160, 0.1) 0%,
    rgba(6, 26, 58, 0.55) 60%,
    rgba(3, 14, 34, 0.9) 100%
  );
}

/* ---- picker rows: the requirement lives in the row ------------------------
   Locked rows carry their unlock condition as a permanent subtitle. The
   shared description line this replaces was never in the right place - top or
   bottom, it meant reading one part of the panel while pointing at another.
   ---------------------------------------------------------------------- */
.spk-themes__text {
  display: flex;
  flex: 1 1 auto;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}

.spk-themes__req {
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1.3;
  color: var(--spk-text-faint);
  white-space: normal;
}

/* Locked rows are taller, so they need a little more room to breathe. */
.spk-themes__row.is-locked {
  align-items: flex-start;
  padding-top: 0.7rem;
  padding-bottom: 0.7rem;
}

.spk-themes__row.is-locked .spk-themes__dot,
.spk-themes__row.is-locked .spk-tile__lock {
  margin-top: 0.15rem;
}

/* ---- map control positions ----------------------------------------------
   Zoom moves to the bottom right, where the cursor already is when you are
   scroll-zooming, and where it no longer competes with the sidebar for the
   top edge. Leaflet parks it inside its own top-left control container, so
   this is a fixed-position override rather than a reparent - reparenting
   would fight Leaflet's own layout on every resize.

   z-index 900 keeps it under the header (1500) and the theme popover (1600)
   but above the map panes.
   ---------------------------------------------------------------------- */
.leaflet-control-zoom {
  position: fixed !important;
  right: 1.1rem;
  bottom: 1.1rem;
  top: auto;
  left: auto;
  margin: 0 !important;
  z-index: 900;
}

/* The share-link button: hidden. It duplicated what the address bar already
   holds, since Pl3xMap keeps the map state in the URL anyway. */
.leaflet-control-link {
  display: none !important;
}

/* ---- online roster -------------------------------------------------------
   Who is on, down the left edge. Hidden entirely when nobody is - see the
   note in spektral.js for why that is the whole point rather than an edge
   case.

   Fixed rather than a Leaflet control: it is site chrome, not a map tool, and
   it should not participate in Leaflet's control layout or move with the map.
   z-index 900 puts it above the map panes but below the header and popover.
   ---------------------------------------------------------------------- */
.spk-roster {
  position: fixed;
  left: 1.1rem;
  top: calc(var(--spk-header-h) + 1.1rem);
  z-index: 900;
  width: 17rem;
  max-height: calc(100vh - var(--spk-header-h) - 9rem);
  overflow-y: auto;
  padding: 0.9rem;
  border-radius: var(--ui-border-radius);
  border: var(--ui-border);
  background: var(--ui-background);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: var(--ui-box-shadow);
  scrollbar-width: thin;
  scrollbar-color: var(--spk-line-strong) transparent;
}

.spk-roster__title {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  margin: 0 0 0.7rem;
  padding: 0 0.3rem;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--spk-text-faint);
}

.spk-roster__count {
  padding: 0.1rem 0.5rem;
  border-radius: 0.4rem;
  background: rgba(255, 255, 255, 0.08);
  color: var(--spk-text);
  font-size: 1rem;
  letter-spacing: 0;
}

.spk-roster__row {
  display: flex;
  align-items: center;
  gap: 0.8rem;
  width: 100%;
  padding: 0.45rem 0.5rem;
  border: none;
  border-radius: 0.5rem;
  background: transparent;
  color: var(--spk-text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 110ms ease-out;
}

.spk-roster__row:hover {
  background: rgba(255, 255, 255, 0.09);
}

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

/* Skin renders are 8x8 pixel art; smoothing them turns faces to mush. */
.spk-roster__head {
  flex: 0 0 auto;
  width: 2.2rem;
  height: 2.2rem;
  border-radius: 0.35rem;
  border: 1px solid var(--spk-line);
  image-rendering: pixelated;
}

.spk-roster__name {
  flex: 1 1 auto;
  min-width: 0;
  font-size: 1.25rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* On a phone the roster would cover most of the map, and the header badge
   already reports the count. */
@media (max-width: 640px) {
  .spk-roster {
    display: none;
  }
}

/* ---- sidebar: drop the Players tab ---------------------------------------
   The roster on the left now shows who is on, and the header badge shows the
   count, so this tab was the third thing reporting one fact.

   Targeted by aria-controls rather than position: upstream labels this button
   "Worlds" by mistake (the aria-label is wrong on their side), so the label is
   useless as a hook and :nth-child would break the moment a tab is added.
   ---------------------------------------------------------------------- */
#sidebar #sidebar__buttons button[aria-controls="sidebar__players"] {
  display: none;
}

/* ---- readouts: coordinates joined to block info --------------------------
   These were reporting the same thing - what is under your cursor - from two
   different corners, so the eye had to travel between them. Stacked into one
   card at the bottom left: coordinates on top as the primary readout, block
   and biome beneath.

   The adjoining corners are squared and the seam border removed so they read
   as a single panel rather than two that happen to touch. Offsets use
   --ui-button-size, which is what sets .leaflet-control-panel's height, so
   the stack stays aligned if that token ever changes.
   ---------------------------------------------------------------------- */
.leaflet-control-coordinates,
.leaflet-control-blockinfo {
  position: fixed !important;
  left: 1.1rem;
  margin: 0 !important;
  /* A FIXED width, not min-width. With min-width the coordinate readout grew
     past the block info by however wide its text happened to be, so the two
     edges never lined up and the card looked broken at the seam. */
  width: 21rem;
  /* Both centred. They were inheriting different alignments - coordinates
     centred, block info left - which is the other half of why the join did
     not read as one panel. */
  text-align: center;
  z-index: 900;
}

.leaflet-control-coordinates {
  bottom: calc(1.1rem + var(--ui-button-size));
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  border-bottom: none;
}

.leaflet-control-blockinfo {
  bottom: 1.1rem;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

/* The roster sits above them on the same edge, so it must stop short of the
   pair rather than running underneath. */
.spk-roster {
  max-height: calc(100vh - var(--spk-header-h) - (var(--ui-button-size) * 2) - 4rem);
}
