/* Live Map — update feedback.
   Counts change under the viewer while polling, so a changed number is briefly
   tinted and nudged rather than silently swapping. Everything here is suppressed
   under prefers-reduced-motion: the information is in the number, not the
   movement. */
@keyframes live-map-bump {
  0%   { transform: translateY(0);     background-color: transparent; }
  25%  { transform: translateY(-2px);  background-color: rgb(219 234 254); }
  100% { transform: translateY(0);     background-color: transparent; }
}
@keyframes live-map-bump-alarm {
  0%   { transform: translateY(0);     background-color: transparent; }
  25%  { transform: translateY(-2px);  background-color: rgb(255 237 213); }
  100% { transform: translateY(0);     background-color: transparent; }
}
.lm-changed       { animation: live-map-bump .9s ease-out; border-radius: .25rem; }
.lm-changed-alarm { animation: live-map-bump-alarm 1.1s ease-out; border-radius: .25rem; }

/* A pharmacy that just gained an unread critical alert pulses once on the map. */
@keyframes live-map-pulse {
  0%   { opacity: .55; transform: scale(.6); }
  100% { opacity: 0;   transform: scale(2.6); }
}
.lm-pulse {
  position: absolute; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border-radius: 9999px; background: rgb(235 104 52); pointer-events: none;
  animation: live-map-pulse 1.4s ease-out forwards;
}

/* The live indicator breathes only while polling is actually running. */
@keyframes live-map-breathe { 0%,100% { opacity: 1 } 50% { opacity: .35 } }
.lm-live-dot { animation: live-map-breathe 2s ease-in-out infinite; }

@media (prefers-reduced-motion: reduce) {
  .lm-changed, .lm-changed-alarm, .lm-pulse, .lm-live-dot { animation: none; }
  .lm-pulse { display: none; }
}

/* --- News ticker ---------------------------------------------------------
   Two identical tracks side by side, the pair translated by exactly half its
   width, so the loop closes with no gap or jump. Duration is set inline from
   the content length, so a long run of headlines scrolls at the same speed as
   a short one rather than racing. */
/* Clipping belongs to the tape's own viewport, NOT the bar. On the bar it also
   clipped the Facts menu, which opens upward out of it — the menu was cut off,
   not hidden behind the map. The bar is raised above the map for good measure,
   since the menu overlaps it. */
.lm-ticker { position: sticky; z-index: 45; }
.lm-ticker-viewport { overflow: hidden; }
.lm-ticker-tape {
  display: inline-flex;
  white-space: nowrap;
  will-change: transform;
  animation: lm-ticker-scroll linear infinite;
}
@keyframes lm-ticker-scroll {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}
/* Reading a headline should not be a race — hovering stops the tape. */
.lm-ticker:hover .lm-ticker-tape,
.lm-ticker:focus-within .lm-ticker-tape { animation-play-state: paused; }

/* Explicitly paused by the viewer. Same mechanism as the hover pause, but it
   survives the pointer leaving, and it is remembered per browser. */
.lm-ticker-paused .lm-ticker-tape { animation-play-state: paused; }

/* Every fact type switched off. The tape is empty, so the bar collapses to its
   label and controls rather than scrolling nothing forever. */
.lm-ticker-empty .lm-ticker-tape { display: none; }

.lm-ticker-item { display: inline-flex; align-items: center; gap: .5rem; padding: 0 1.25rem; }
.lm-ticker-sep  { opacity: .35; }

@media (prefers-reduced-motion: reduce) {
  /* No scrolling at all: the tape becomes a normal scrollable strip, so the
     headlines are still all reachable. On the viewport, not the bar, or the
     Facts menu is clipped again. */
  .lm-ticker-viewport { overflow-x: auto; }
  .lm-ticker-tape { animation: none; }
  .lm-ticker-tape > *:nth-child(2) { display: none; }
}

/* --- Floating event marks ------------------------------------------------
   A mark rises from the pharmacy and fades out. Short and small on purpose:
   at a busy moment several play at once, and anything heavier would read as
   clutter rather than activity. */
@keyframes lm-float-up {
  0%   { opacity: 0;   transform: translate(-50%, -50%) scale(.6); }
  15%  { opacity: 1;   transform: translate(-50%, -110%) scale(1); }
  100% { opacity: 0;   transform: translate(calc(-50% + var(--drift, 0px)), -320%) scale(1); }
}
.lm-float {
  position: absolute;
  z-index: 5;
  /* 26px is the old 17px plus half again — the marks were small enough that on
     a wall screen across the room they read as specks rather than as anything
     happening. The multiplier on top is the viewer's own, from the settings
     menu, and goes up to 5x for a screen being watched from further back. */
  font-size: calc(26px * var(--lm-float-scale, 1));
  line-height: 1;
  pointer-events: none;
  will-change: transform, opacity;
  /* 2.4s is the original. The multiplier is the viewer's, up to 4x, for a map
     being watched rather than glanced at — at 1x a mark is gone before someone
     across the room has finished looking up. */
  animation: lm-float-up calc(2.4s * var(--lm-float-duration, 1)) ease-out forwards;
}
/* The pharmacy's own logo, rising with the mark. Sized independently of the
   emoji — a logo has to be legible to be worth showing, and the two rarely want
   the same size. The white plate keeps a transparent PNG from dissolving into
   the map underneath it. */
.lm-float-logo {
  position: absolute;
  z-index: 5;
  width: calc(22px * var(--lm-logo-scale, 1));
  height: calc(22px * var(--lm-logo-scale, 1));
  /* Capped so a 10x logo on a small map cannot cover the thing it came from. */
  max-width: 45%;
  max-height: 45%;
  object-fit: contain;
  background: #fff;
  border-radius: 15%;
  padding: 2px;
  box-shadow: 0 2px 8px rgb(0 0 0 / 18%);
  pointer-events: none;
  will-change: transform, opacity, filter;
  animation: lm-logo-pop calc(2.4s * var(--lm-logo-duration, 1)) cubic-bezier(.34, 1.05, .64, 1) forwards;
}

/* The logo rises, swells, and bursts.
 
   Deliberately a different ending from the emoji beside it. The emoji is a
   count — one heart is one reading — so it fades and the eye lets it go. The
   logo is an identity, and it is the thing worth noticing, so it earns a
   moment: a slow swell like a balloon filling, then a burst outward where it
   blurs and dissolves into the map rather than blinking out. The blur is what
   makes it read as dissolving rather than merely getting bigger. */
@keyframes lm-logo-pop {
  0%   { opacity: 0;   filter: blur(0px); transform: translate(-50%, -50%) scale(.4); }
  12%  { opacity: 1;   filter: blur(0px); transform: translate(-50%, -95%) scale(1); }
  55%  { opacity: 1;   filter: blur(0px); transform: translate(calc(-50% + var(--drift, 0px)), -190%) scale(1.05); }
  /* the stretch just before it goes */
  80%  { opacity: .92; filter: blur(0px); transform: translate(calc(-50% + var(--drift, 0px)), -240%) scale(1.3); }
  /* the burst */
  100% { opacity: 0;   filter: blur(7px); transform: translate(calc(-50% + var(--drift, 0px)), -262%) scale(2.5); }
}

@media (prefers-reduced-motion: reduce) {
  .lm-float, .lm-float-logo { display: none; }
}

/* --- Presentation mode ---------------------------------------------------
   For a screen someone leaves running: the portal chrome steps back and the
   map takes the room. Not full-screen API — the page stays navigable and the
   session keeps polling. */
body.lm-presenting .sidebar-rail,
body.lm-presenting header { display: none !important; }
/* Was 0.75rem all round, which reads as a wide border once the map fills the
   screen. Just enough to keep the map off the glass. */
body.lm-presenting main { padding: 0.25rem !important; }
body.lm-presenting .lm-ticker { font-size: 13px; }

/* The page title is chrome too: on a wall screen the map is the point, and the
   heading is a header the room has already read. */
body.lm-presenting .lm-page-head { display: none; }

/* The shell keeps its 540px in the page and is measured to the viewport in
   presentation mode (see sizeMap). Rounding and a light border read as a frame
   when the map is edge to edge, so both go. */
body.lm-presenting .lm-map-shell {
  border-radius: 0;
  border-color: transparent;
}

/* --- New-enrolment announcement ------------------------------------------
   Rises into the bottom-left corner of the map and holds for a few seconds.
   Deliberately calmer than the floating marks: this one is meant to be read,
   not glanced at. */
@keyframes lm-enroll-in {
  from { opacity: 0; transform: translateY(8px) scale(.98); }
  to   { opacity: 1; transform: none; }
}
.lm-enroll-card { animation: lm-enroll-in .35s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  .lm-enroll-card { animation: none; }
}

/* --- "Show on map" ---------------------------------------------------------
   Clicking a pharmacy in the roster flies the map to it. A camera that simply
   arrives leaves people hunting for what moved, so the arrival is announced:
   three rings expand from the pin, and the roster row that was clicked is
   tinted for a moment so the two ends of the action stay connected. */
@keyframes lm-spotlight-ring {
  0%   { opacity: .75; transform: translate(-50%, -50%) scale(.25); }
  70%  { opacity: .12; }
  100% { opacity: 0;   transform: translate(-50%, -50%) scale(3.2); }
}
.lm-spotlight,
.lm-spotlight::before,
.lm-spotlight::after {
  position: absolute;
  width: 54px;
  height: 54px;
  margin: 0;
  border-radius: 9999px;
  border: 3px solid rgb(37 99 235);
  pointer-events: none;
  animation: lm-spotlight-ring 2.1s cubic-bezier(.22, .61, .36, 1) forwards;
}
/* The pseudo-elements ride on the parent, so they inherit its centre and only
   need a delay to become the second and third ring. */
.lm-spotlight::before,
.lm-spotlight::after { content: ""; left: 50%; top: 50%; }
.lm-spotlight::before { animation-delay: .28s; }
.lm-spotlight::after  { animation-delay: .56s; }

/* The picked row. Deliberately a different colour from the polling
   change-flash: that one means "this number moved", this one means "this is
   the one you asked for". */
@keyframes lm-picked-tint {
  0%   { background-color: rgb(219 234 254); }
  100% { background-color: transparent; }
}
.lm-picked { animation: lm-picked-tint 2s ease-out forwards; }

/* The roster's pharmacy mark only reads as a control on hover, so it does not
   compete with the name beside it. */
.lm-show-on-map { cursor: pointer; transition: color .12s ease; }

@media (prefers-reduced-motion: reduce) {
  .lm-spotlight { display: none; }
  .lm-picked    { animation: none; }
}

/* --- Info tooltips -------------------------------------------------------
   The KPI notes have been through two shapes that did not work.

   A native title="" was dropped by the browser whenever the element under the
   pointer was replaced, and the KPI row is rebuilt on every poll, so the tip
   frequently never appeared. Replacing it with a CSS ::after fixed that and
   introduced a worse problem: the map's WebGL canvas painted straight over the
   note. No z-index helps — the canvas is its own compositing layer, and a tip
   anchored inside the page cannot be lifted above it.

   So the note is a POPOVER. The browser renders it in the top layer, above
   every stacking context on the page, and it is positioned by the controller
   rather than by an anchor it can be clipped against. `position: fixed` and a
   high z-index are kept as the fallback path for a browser with no popover
   support, where .lm-tip-open does the showing. */
.lm-tip { cursor: help; }

.lm-tip-pop {
  position: fixed;
  /* Placed by the controller; these keep it off-screen until it is. */
  left: 0;
  top: -9999px;
  z-index: 100;
  margin: 0;
  width: 17rem;
  max-width: calc(100vw - 1rem);
  /* The KPI label above it is uppercase, tracked and nowrap; none of that
     should reach the note. */
  white-space: normal;
  text-transform: none;
  letter-spacing: normal;
  font-weight: 400;
  text-align: left;
  font-size: 11px;
  line-height: 1.4;
  color: #fff;
  background: #111827;
  border: 0;
  padding: .5rem .625rem;
  border-radius: .5rem;
  box-shadow: 0 8px 24px rgb(0 0 0 / 18%);
  overflow: visible;
}

/* :popover-open is what hides it the rest of the time. The fallback class is
   only consulted where the API is missing. */
.lm-tip-pop:not(:popover-open):not(.lm-tip-open) { display: none; }
.lm-tip-pop.lm-tip-open { display: block; }
.lm-tip-pop::backdrop { background: transparent; }
