/* InteliRain Operations Dashboard — motion & glow layer.
 *
 * Loaded AFTER styles.css, so equal-specificity rules here win: this file
 * owns every transition, animation and glow in the app. styles.css keeps
 * structure and resting colors only.
 *
 * Rules of the house:
 *  - Animate transform/opacity (GPU). Never top/left/width/height.
 *  - Entrance keyframes END at the natural state and use fill-mode
 *    `backwards` — a `forwards` fill would pin the element's transform
 *    forever and silently kill every hover lift below.
 *  - Colors derive from the styles.css tokens through color-mix, so light
 *    and dark themes stay correct from one definition.
 *  - Everything degrades through the prefers-reduced-motion block at the
 *    end of this file.
 */

/* ------------------------------------------------------------------ */
/* 1. Motion tokens                                                    */
/* ------------------------------------------------------------------ */

:root {
  /* durations */
  --motion-fast: 150ms;    /* focus rings, button press, row hover */
  --motion-hover: 250ms;   /* card glow + lift */
  --motion-modal: 200ms;   /* dialog open/close */
  --motion-view: 300ms;    /* tab switch */
  --motion-stagger: 50ms;  /* per-child cascade step */
  --motion-pulse: 600ms;   /* one-shot data-change flash */

  /* easings */
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);

  /* accent glow, derived from the theme's blue */
  --glow-strong: color-mix(in srgb, var(--blue) 50%, transparent);
  --glow-soft: color-mix(in srgb, var(--blue) 22%, transparent);
  --glow-faint: color-mix(in srgb, var(--blue) 12%, transparent);

  /* composed shadows */
  --shadow-glow: 0 12px 30px -10px var(--glow-soft), 0 0 22px -4px var(--glow-faint);
  --shadow-glow-compact: 0 4px 12px -3px var(--glow-soft);
  --focus-ring: 0 0 0 3px var(--glow-soft);

  --backdrop-blur: 5px;
}

/* Dark surfaces swallow a low-opacity glow, so the accent runs hotter. */
[data-theme="dark"] {
  --glow-strong: color-mix(in srgb, var(--blue) 62%, transparent);
  --glow-soft: color-mix(in srgb, var(--blue) 30%, transparent);
  --glow-faint: color-mix(in srgb, var(--blue) 16%, transparent);
}

/* ------------------------------------------------------------------ */
/* 2. Keyframes                                                        */
/* ------------------------------------------------------------------ */

/* Tab switch: the view slides in from the nav side while fading up. */
@keyframes view-enter {
  from { opacity: 0; transform: translateX(14px); }
  to { opacity: 1; transform: none; }
}

/* Staggered child reveal inside a freshly shown view. */
@keyframes child-rise {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

@keyframes modal-pop {
  from { opacity: 0; transform: scale(0.95); }
  to { opacity: 1; transform: none; }
}

@keyframes modal-drop {
  from { opacity: 1; transform: none; }
  to { opacity: 0; transform: scale(0.95); }
}

@keyframes backdrop-in {
  from { opacity: 0; }
  to { opacity: 1; }
}

@keyframes backdrop-out {
  from { opacity: 1; }
  to { opacity: 0; }
}

/* One-shot flash when a rendered value actually changed. */
@keyframes value-pulse {
  0% { transform: none; }
  30% { transform: scale(1.06); color: var(--blue); text-shadow: 0 0 14px var(--glow-soft); }
  100% { transform: none; }
}

/* Expanding ring on live status dots — transform/opacity, not box-shadow. */
@keyframes status-ping {
  0% { transform: scale(1); opacity: 0.9; }
  70% { transform: scale(1.9); opacity: 0; }
  100% { transform: scale(1.9); opacity: 0; }
}

@keyframes shimmer-sweep {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}

@keyframes sheen-sweep {
  from { transform: translateX(-120%); }
  to { transform: translateX(120%); }
}

/* Placeholder pulse for containers that must not gain a position or a
 * pseudo-element (see .world-map:empty). */
@keyframes skeleton-breathe {
  0%, 100% { opacity: 0.55; }
  50% { opacity: 1; }
}

/* The live-map loader: a lateral sweeping a circular field, like the
 * pivots this dashboard watches. Pure rotation, so it stays on the
 * compositor. */
@keyframes pivot-sweep {
  to { transform: rotate(360deg); }
}

@keyframes pivot-arm-sweep {
  to { transform: translateY(-50%) rotate(360deg); }
}

/* ------------------------------------------------------------------ */
/* 3. Page depth + scrolling                                           */
/* ------------------------------------------------------------------ */

html {
  scroll-behavior: smooth;
}

/* Two faint fixed washes above the page background. Content scrolls over
 * them while they stay put — that relative motion is the depth cue, at no
 * runtime cost (no scroll listener, no repaint). */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background:
    radial-gradient(560px 420px at 12% -4%, color-mix(in srgb, var(--blue) 7%, transparent), transparent 70%),
    radial-gradient(640px 480px at 96% 8%, color-mix(in srgb, var(--purple) 5%, transparent), transparent 70%);
}

/* ------------------------------------------------------------------ */
/* 4. View switching, stagger and first paint                          */
/* ------------------------------------------------------------------ */

/* These bind to .page-view.active and its DIRECT children only. Those
 * children are the static wrappers from index.html — render() rewrites
 * their innerHTML but never replaces them, and re-applying an already
 * present .active class mutates nothing. So the animation restarts only
 * when display flips none -> grid: a real tab switch, or the first paint
 * when enterApp() unhides #appShell. Polling never replays it.
 * Do NOT move these selectors inside re-rendered content. */
.page-view.active {
  animation: view-enter var(--motion-view) var(--ease-out) backwards;
}

.page-view.active > * {
  animation: child-rise var(--motion-view) var(--ease-out) backwards;
}

.page-view.active > :nth-child(2) { animation-delay: var(--motion-stagger); }
.page-view.active > :nth-child(3) { animation-delay: calc(2 * var(--motion-stagger)); }
.page-view.active > :nth-child(4) { animation-delay: calc(3 * var(--motion-stagger)); }
/* Cap the cascade so a long view never feels like it is loading slowly. */
.page-view.active > :nth-child(n + 5) { animation-delay: calc(4 * var(--motion-stagger)); }

/* The shell chrome rises with the first view on login. */
.app-shell:not([hidden]) > .sidebar {
  animation: child-rise var(--motion-view) var(--ease-out) backwards;
}

.app-shell:not([hidden]) .topbar {
  animation: child-rise var(--motion-view) var(--ease-out) backwards;
  animation-delay: var(--motion-stagger);
}

/* ------------------------------------------------------------------ */
/* 5. Cards and containers — hover glow                                */
/* ------------------------------------------------------------------ */

/* Group A: cards with room to breathe. box-shadow follows the border box,
 * so one recipe scales the glow to any card size. */
.panel,
.metric-card,
.setting-item,
.tower-chip,
.tp-item,
.summary-tile,
.employee-card,
.admin-locked,
.history-group,
.sync-block,
.avatar-block,
.search-field,
.elevation-legend,
.geofence-legend,
.maps-thumb img,
.map-figure img {
  transition:
    transform var(--motion-hover) var(--ease-out),
    box-shadow var(--motion-hover) var(--ease-out),
    border-color var(--motion-hover) var(--ease-out);
}

.panel:hover,
.metric-card:hover,
.setting-item:hover,
.tower-chip:hover,
.tp-item:hover,
.summary-tile:hover,
.employee-card:hover,
.admin-locked:hover,
.history-group:hover,
.sync-block:hover,
.avatar-block:hover,
.search-field:hover,
.elevation-legend:hover,
.geofence-legend:hover,
.maps-thumb img:hover,
.map-figure img:hover {
  transform: translateY(-2px);
  border-color: var(--blue-line);
  box-shadow: var(--shadow-soft), var(--shadow-glow);
}

/* The employee block sits on navy, where the blue glow disappears; give
 * it a light halo instead so the hover still registers. */
.avatar-block:hover {
  border-color: rgba(122, 165, 255, 0.45);
  box-shadow: 0 10px 26px -12px rgba(122, 165, 255, 0.55);
}

/* Group B: cards inside overflow containers. .system-list carries a 7px
 * gutter (styles.css) so a compact glow fits; a full-size one would still
 * be sheared off at the scroll edges. */
.system-card {
  transition:
    transform var(--motion-hover) var(--ease-out),
    box-shadow var(--motion-hover) var(--ease-out),
    border-color var(--motion-hover) var(--ease-out),
    background-color var(--motion-hover) var(--ease-out);
}

.system-card:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-glow-compact), inset 0 0 0 1px var(--glow-faint);
}

/* Selected keeps its inset accent bar; add the glow without losing it. */
.system-card.selected:hover {
  box-shadow: inset 3px 0 0 var(--blue), var(--shadow-glow-compact);
}

/* ------------------------------------------------------------------ */
/* 6. Buttons and interactive controls                                 */
/* ------------------------------------------------------------------ */

.primary-button,
.ghost-button,
.link-button,
.icon-close,
.media-modal-close,
.segment,
.category-tab,
.row-action,
.day-chip,
.theme-toggle,
.lane-chart-btn,
.copy-btn,
.nav-link,
.map-search-item,
.history-picker-item,
.maps-download-btn {
  transition:
    transform var(--motion-fast) var(--ease-out),
    box-shadow var(--motion-fast) var(--ease-out),
    border-color var(--motion-fast) var(--ease-out),
    background-color var(--motion-fast) var(--ease-out),
    color var(--motion-fast) var(--ease-out);
}

/* Press feedback — the app had no :active state anywhere before. */
.primary-button:active,
.ghost-button:active,
.icon-close:active,
.media-modal-close:active,
.segment:active,
.category-tab:active,
.row-action:active,
.day-chip:active,
.theme-toggle:active,
.lane-chart-btn:active,
.copy-btn:active,
.nav-link:active,
.login-submit:active,
.system-card:active {
  transform: scale(0.98);
}

/* Disabled controls must not appear to respond. */
.row-action:disabled,
.row-action:disabled:active {
  transform: none;
  box-shadow: none;
}

.primary-button:hover {
  transform: translateY(-1px);
  box-shadow: 0 12px 26px rgba(37, 99, 235, 0.42);
}

.primary-button:active {
  transform: scale(0.98);
}

.ghost-button:hover,
.theme-toggle:hover,
.copy-btn:hover {
  box-shadow: var(--shadow-glow-compact);
}

/* Sheen: a diagonal highlight swept across on hover. Two buttons only —
 * on every control it reads as glitter, not polish. */
.primary-button,
.lane-chart-btn {
  position: relative;
  overflow: hidden;
}

.primary-button::after,
.lane-chart-btn::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  transform: translateX(-120%);
  background: linear-gradient(
    105deg,
    transparent 40%,
    color-mix(in srgb, #ffffff 35%, transparent) 50%,
    transparent 60%
  );
}

.primary-button:hover::after,
.lane-chart-btn:hover::after {
  animation: sheen-sweep var(--motion-pulse) var(--ease-out);
}

/* The graph buttons sit in a quiet column of small icons and were easy to
 * miss, so they carry a permanent accent ring. This is a resting box-shadow
 * only — no animation: a slow glint sweep was tried here and removed
 * (operator decision 2026-08-05), the ring alone reads well enough. The
 * hover sheen above still fires on pointer-over. */
.lane-chart-btn {
  box-shadow: 0 0 0 1px var(--glow-faint), var(--shadow-glow-compact);
}

.lane-chart-btn:hover,
.lane-chart-btn:focus-visible {
  box-shadow: 0 0 0 1px var(--glow-strong), var(--shadow-glow);
}

/* ------------------------------------------------------------------ */
/* 7. Inputs, selects and focus rings                                  */
/* ------------------------------------------------------------------ */

/* .search-field input and .select-control select set outline:none, so the
 * ring has to live on the wrapper. */
.search-field,
.select-control,
.form-field input,
.form-field textarea,
.form-field select,
.inline-select,
.history-picker-menu input[type="search"],
.history-custom-range input,
.probe-status-filter select,
select {
  transition:
    border-color var(--motion-fast) var(--ease-out),
    box-shadow var(--motion-fast) var(--ease-out),
    background-color var(--motion-fast) var(--ease-out);
}

.search-field:focus-within,
.select-control:focus-within {
  border-color: var(--blue);
  box-shadow: var(--focus-ring);
}

.form-field input:focus,
.form-field textarea:focus,
.form-field select:focus {
  box-shadow: var(--focus-ring);
}

.inline-select:focus-visible,
.history-picker-menu input[type="search"]:focus-visible,
.history-custom-range input:focus-visible,
.probe-status-filter select:focus-visible,
select:focus-visible {
  outline: none;
  border-color: var(--blue);
  box-shadow: var(--focus-ring);
}

/* ------------------------------------------------------------------ */
/* 8. Tables                                                           */
/* ------------------------------------------------------------------ */

/* Rows are never transformed: they live under a sticky thead and inside
 * an overflow-x scroller, where a transform both clips and repaints. The
 * inset shadow draws the left accent with zero layout shift. */
tbody tr {
  transition:
    background-color var(--motion-fast) var(--ease-out),
    box-shadow var(--motion-fast) var(--ease-out);
}

tbody tr:hover {
  background: color-mix(in srgb, var(--blue) 5%, var(--surface));
  box-shadow: inset 3px 0 0 var(--blue);
}

/* ------------------------------------------------------------------ */
/* 9. Live status indicators                                           */
/* ------------------------------------------------------------------ */

/* Exactly three things breathe: the live indicator, online system dots,
 * and issues actively being worked. Everything else stays still — a
 * dashboard where twenty badges pulse communicates nothing. */
.pulse,
.dot.online,
.status-pill.in_progress {
  position: relative;
}

.pulse::after,
.dot.online::after,
.status-pill.in_progress::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  box-shadow: 0 0 0 1.5px var(--pulse-color);
  animation: status-ping 2.4s var(--ease-out) infinite;
}

.dot.online {
  --pulse-color: color-mix(in srgb, var(--green) 55%, transparent);
}

.status-pill.in_progress {
  --pulse-color: color-mix(in srgb, var(--purple) 45%, transparent);
}

/* The live indicator: a recording-light red. The lamp itself breathes
 * (an inner glow that swells and settles) while a ring pings outward, so
 * it reads as "on air" rather than as an error dot. */
.pulse {
  --pulse-color: color-mix(in srgb, var(--red) 60%, transparent);
  animation: live-glow 2.4s var(--ease-out) infinite;
}

.pulse::after {
  animation: status-ping 2.4s var(--ease-out) infinite;
}

@keyframes live-glow {
  0%, 100% {
    box-shadow:
      0 0 0 2px color-mix(in srgb, var(--red) 18%, transparent),
      0 0 8px 1px color-mix(in srgb, var(--red) 45%, transparent);
  }
  50% {
    box-shadow:
      0 0 0 3px color-mix(in srgb, var(--red) 10%, transparent),
      0 0 14px 3px color-mix(in srgb, var(--red) 65%, transparent);
  }
}

/* Stale data is a problem, not a heartbeat: kill the glow and the ring so
 * the amber dot sits still and reads as a fault. */
.pulse.degraded {
  animation: none;
  box-shadow: none;
}

.pulse.degraded::after {
  display: none;
}

/* ------------------------------------------------------------------ */
/* 10. Tooltips                                                        */
/* ------------------------------------------------------------------ */

/* No reveal animation: these follow the cursor and are toggled through
 * [hidden], so a transition would only make them lag. */
.chart-tooltip,
.probe-tooltip {
  border-color: var(--blue-line);
  box-shadow: var(--shadow-glow-compact);
}

/* ------------------------------------------------------------------ */
/* 11. Modals                                                          */
/* ------------------------------------------------------------------ */

/* .closing keeps the dialog on screen for its exit animation after .show
 * is removed. app.js drives it (animateModalClose) and tears it down on a
 * timer, so .show stays a synchronous, testable signal of "open". */
.media-modal.closing,
.form-modal.closing,
.chart-modal.closing {
  display: grid;
}

.media-modal,
.form-modal,
.chart-modal {
  /* Constant blur, animated opacity: animating backdrop-filter itself
   * repaints the whole viewport every frame. */
  backdrop-filter: blur(var(--backdrop-blur));
  -webkit-backdrop-filter: blur(var(--backdrop-blur));
}

.media-modal.show,
.form-modal.show,
.chart-modal.show {
  animation: backdrop-in var(--motion-modal) var(--ease-out) backwards;
}

.media-modal.show .media-modal-dialog,
.form-modal.show .form-modal-dialog,
.chart-modal.show .chart-modal-dialog {
  animation: modal-pop var(--motion-modal) var(--ease-out) backwards;
}

.media-modal.closing,
.form-modal.closing,
.chart-modal.closing {
  pointer-events: none;
  animation: backdrop-out var(--motion-modal) var(--ease-in) forwards;
}

.media-modal.closing .media-modal-dialog,
.form-modal.closing .form-modal-dialog,
.chart-modal.closing .chart-modal-dialog {
  animation: modal-drop var(--motion-modal) var(--ease-in) forwards;
}

.toast {
  transition:
    opacity var(--motion-hover) var(--ease-out),
    transform var(--motion-hover) var(--ease-out);
}

/* ------------------------------------------------------------------ */
/* 12. Loading skeletons                                               */
/* ------------------------------------------------------------------ */

.skeleton {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-xs);
  background: color-mix(in srgb, var(--text) 7%, transparent);
}

/* The sweep is a transform on a pseudo-element, not an animated
 * background-position — the latter repaints the element every frame. */
.skeleton::after {
  content: "";
  position: absolute;
  inset: 0;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--surface) 70%, transparent),
    transparent
  );
  animation: shimmer-sweep 1.6s ease-in-out infinite;
}

.skeleton-state {
  display: grid;
  gap: 10px;
  width: 100%;
  place-items: stretch;
}

.skeleton-line {
  height: 13px;
}

/* Live map: hold the frame with a shimmer instead of flashing empty while
 * the once-a-minute refresh downloads. */
.map-figure img {
  opacity: 1;
  transition: opacity var(--motion-hover) var(--ease-out);
}

.map-figure.is-loading img {
  opacity: 0;
}

.map-figure {
  position: relative;
}

/* ::before is the static plate, ::after the moving highlight — one
 * element cannot both hold still and sweep. overflow is clipped only
 * while loading, so it never shears the image's hover glow. */
/* A pending <img> has no intrinsic height, so its grid row collapses and
 * the caption drifts into the middle of the plate. Give the image row the
 * free space while loading so the caption stays pinned to the bottom,
 * clear of the centred loader. */
.map-figure.is-loading {
  overflow: hidden;
  border-radius: var(--radius-sm);
  grid-template-rows: 1fr auto;
}

.map-figure.is-loading::before {
  content: "";
  position: absolute;
  inset: 0 0 26px;
  border-radius: var(--radius-sm);
  background: color-mix(in srgb, var(--text) 7%, transparent);
}

.map-figure.is-loading::after {
  content: "";
  position: absolute;
  inset: 0 0 26px;
  transform: translateX(-100%);
  background: linear-gradient(
    90deg,
    transparent,
    color-mix(in srgb, var(--surface) 70%, transparent),
    transparent
  );
  animation: shimmer-sweep 1.6s ease-in-out infinite;
}

/* --- live-map loader ------------------------------------------------ */

/* Absolutely placed so it never adds a row to .map-figure's grid, and
 * inset to match the shimmer plate (the caption keeps the bottom 26px).
 * Marked aria-hidden in the markup: the figcaption already names the
 * region, and this pane re-renders every minute — announcing "loading"
 * that often would be noise on a screen reader. */
.map-loader {
  position: absolute;
  inset: 0 0 26px;
  display: grid;
  place-content: center;
  justify-items: center;
  gap: 12px;
  pointer-events: none;
  /* The figure's ::after shimmer paints last; keep the dial above it. */
  z-index: 1;
}

.map-figure:not(.is-loading) .map-loader {
  display: none;
}

/* The field boundary, with the pivot point at its centre. */
.map-loader-dial {
  position: relative;
  width: 58px;
  height: 58px;
  border-radius: 50%;
  border: 1.5px dashed color-mix(in srgb, var(--blue) 38%, transparent);
  background: radial-gradient(circle at center, var(--blue) 0 3px, transparent 3.5px);
}

/* Wetted trail behind the lateral: a conic wedge masked into a ring. */
.map-loader-dial::before {
  content: "";
  position: absolute;
  inset: -1.5px;
  border-radius: 50%;
  /* from 90deg puts the wedge's leading edge at 3 o'clock, where the arm
   * rests at rotation 0, so the trail follows the lateral instead of
   * running a quarter-turn ahead of it. */
  background: conic-gradient(
    from 90deg,
    transparent 0deg 240deg,
    color-mix(in srgb, var(--blue) 22%, transparent) 320deg,
    color-mix(in srgb, var(--blue) 45%, transparent) 360deg
  );
  -webkit-mask: radial-gradient(circle, transparent 38%, #000 39%);
  mask: radial-gradient(circle, transparent 38%, #000 39%);
  animation: pivot-sweep 1.6s linear infinite;
}

/* The lateral itself, hinged on the pivot point. */
.map-loader-dial::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 46%;
  height: 2px;
  border-radius: 1px;
  transform-origin: 0 50%;
  transform: translateY(-50%);
  background: linear-gradient(90deg, var(--blue), color-mix(in srgb, var(--blue) 15%, transparent));
  animation: pivot-arm-sweep 1.6s linear infinite;
}

.map-loader-text {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--muted);
  letter-spacing: 0.01em;
}

/* Standing still, a dial reads as a diagram rather than progress — let
 * the caption carry the meaning and drop the artwork. */
@media (prefers-reduced-motion: reduce) {
  .map-loader-dial {
    display: none;
  }
}

/* Leaflet injects children on first visit to the Map tab, which drops
 * :empty and removes this with no JS involved.
 *
 * Paint only — no position, no overflow, no pseudo-element. Giving this
 * container a position while it is empty changes the containing block
 * Leaflet's absolutely-positioned panes resolve against; the moment
 * Leaflet fills it the rule stops matching, the panes reflow against a
 * different ancestor, and the tile layer lands far outside the viewport
 * (a ~12,500px horizontal page scroll). A breathing tint is enough. */
.world-map:empty {
  background: color-mix(in srgb, var(--text) 6%, transparent);
  animation: skeleton-breathe 1.8s ease-in-out infinite;
}

/* ------------------------------------------------------------------ */
/* 13. Data-change pulse                                               */
/* ------------------------------------------------------------------ */

/* app.js adds this only when a rendered value differs from the previous
 * one, so a re-render with unchanged data stays still. */
.value-pulse {
  animation: value-pulse var(--motion-pulse) var(--ease-out);
}

/* ------------------------------------------------------------------ */
/* 14. Reduced motion                                                  */
/* ------------------------------------------------------------------ */

/* Near-zero durations rather than `animation: none`: entrance keyframes
 * must still RUN (and land on their visible end state), or content that
 * starts at opacity 0 under a `backwards` fill would never appear.
 * app.js mirrors this for the modal close path via prefersReducedMotion(),
 * because a .closing overlay has to be torn down by script, not by CSS. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
