/* pulse.css — the /pulse network dashboard, extending "The Origin Plot"
   identity (network-plot.css). Loads after it alphabetically, so equal-
   specificity rules here win. Everything is `.np-pulse-` prefixed; the
   per-app `--np-accent` vars come from the shared `.np-k-<key>` classes. */

/* ---------- hero strip: graph-paper backdrop, same recipe as .np-hero ---------- */
.np-pulse-hero {
    position: relative;
    padding: 20px 24px;
    border-radius: var(--mantine-radius-md);
    border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
    background:
        linear-gradient(light-dark(rgba(255, 255, 255, 0.88), rgba(26, 27, 30, 0.88)),
                        light-dark(rgba(255, 255, 255, 0.88), rgba(26, 27, 30, 0.88))),
        repeating-linear-gradient(0deg,
            light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)) 0 1px,
            transparent 1px 22px),
        repeating-linear-gradient(90deg,
            light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4)) 0 1px,
            transparent 1px 22px);
}

/* ---------- satellite cards ---------- */
.np-pulse-card {
    position: relative;
    padding: 12px 14px;
    border-radius: var(--mantine-radius-md);
    border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
    border-left: 3px solid var(--np-accent, var(--mantine-color-gray-5));
    background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-6));
}

.np-pulse-dot {
    display: inline-block;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    flex-shrink: 0;
}

.np-pulse-dot--up {
    background: light-dark(var(--mantine-color-teal-6), var(--mantine-color-teal-4));
    box-shadow: 0 0 0 3px light-dark(rgba(9, 146, 104, 0.15), rgba(9, 146, 104, 0.3));
    animation: np-pulse-beat 2.4s ease-in-out infinite;
}

.np-pulse-dot--cold {
    /* free-tier host that answered only on the long cold-boot retry —
       awake now, but a visitor would have waited. Not an outage. */
    background: light-dark(var(--mantine-color-yellow-6), var(--mantine-color-yellow-4));
    box-shadow: 0 0 0 3px light-dark(rgba(250, 176, 5, 0.18), rgba(250, 176, 5, 0.3));
}

.np-pulse-dot--down {
    background: light-dark(var(--mantine-color-red-6), var(--mantine-color-red-4));
    box-shadow: 0 0 0 3px light-dark(rgba(224, 49, 49, 0.15), rgba(224, 49, 49, 0.3));
}

.np-pulse-dot--unknown {
    background: light-dark(var(--mantine-color-gray-5), var(--mantine-color-dark-3));
}

@keyframes np-pulse-beat {
    0%, 100% { box-shadow: 0 0 0 2px light-dark(rgba(9, 146, 104, 0.12), rgba(9, 146, 104, 0.25)); }
    50%      { box-shadow: 0 0 0 5px light-dark(rgba(9, 146, 104, 0.06), rgba(9, 146, 104, 0.12)); }
}

/* ---------- event feed rows ---------- */
.np-pulse-row {
    display: flex;
    align-items: baseline;
    gap: 10px;
    padding: 2px 6px;
    border-radius: 4px;
    font-size: 12px;
    line-height: 1.5;
    overflow: hidden;
    white-space: nowrap;
}

.np-pulse-row:hover {
    background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-5));
}

.np-pulse-row--error {
    background: light-dark(var(--mantine-color-red-0), rgba(224, 49, 49, 0.12));
}

.np-pulse-ts {
    font-family: 'Spline Sans Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
    color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
    flex-shrink: 0;
}

.np-pulse-task {
    font-family: 'Spline Sans Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
    color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-4));
    flex-shrink: 0;
    min-width: 110px;
}

.np-pulse-msg {
    color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-4));
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ---------- nav clock + activity cockpit (components/nav_pulse.py) ---------- */
.np-nav-clock {
    color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-4));
}

/* Below the mobile breakpoint the bottom Drawer takes over — hide the
   hovercard dropdown entirely so a stray hover can't open both. */
@media (max-width: 767px) {
    .np-nav-act-hc-dropdown { display: none !important; }
}

.np-nav-act-drawer-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
}
#nav-act-drawer-panel {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
}

/* ---------- HeartbeatPulse dial (components/heartbeat_pulse.py) ----------
   Ported from the SailsBaord dial. The component sets --hb-color-1/2/3
   inline from the phase's Mantine color (teal / red / yellow). */
.np-hb-pulse {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
}
.np-hb-pulse-ring {
    position: absolute;
    top: 50%;
    left: 50%;
    border-radius: 50%;
    transform: translate(-50%, -50%);
    display: flex;
    align-items: center;
    justify-content: center;
    will-change: transform, opacity, box-shadow;
}
.np-hb-pulse-ring--outer {
    width: 100%;
    height: 100%;
    background: var(--hb-color-1);
    opacity: 0.35;
    animation: np-hb-breathe 3.2s ease-in-out infinite;
}
.np-hb-pulse-ring--mid {
    width: 72%;
    height: 72%;
    background: var(--hb-color-2);
    opacity: 0.55;
    animation: np-hb-breathe 3.2s ease-in-out infinite;
    animation-delay: -1.1s;
}
.np-hb-pulse-ring--core {
    width: 46%;
    height: 46%;
    background: var(--mantine-color-body);
    border: 2px solid var(--hb-color-3);
    box-shadow: 0 0 0 4px var(--hb-color-1), 0 0 24px -4px var(--hb-color-3);
}

@keyframes np-hb-breathe {
    0%, 100% { transform: translate(-50%, -50%) scale(0.82); opacity: 0.28; }
    50%      { transform: translate(-50%, -50%) scale(1.00); opacity: 0.55; }
}

.np-hb-pulse--working .np-hb-pulse-ring--outer,
.np-hb-pulse--working .np-hb-pulse-ring--mid {
    animation-duration: 0.9s;
}
.np-hb-pulse--working .np-hb-pulse-ring--core {
    animation: np-hb-core-glow 0.9s ease-in-out infinite alternate;
}
@keyframes np-hb-core-glow {
    from { box-shadow: 0 0 0 4px var(--hb-color-1), 0 0 18px -4px var(--hb-color-3); }
    to   { box-shadow: 0 0 0 6px var(--hb-color-2), 0 0 36px -2px var(--hb-color-3); }
}

.np-hb-pulse--error .np-hb-pulse-ring--outer,
.np-hb-pulse--error .np-hb-pulse-ring--mid {
    animation: np-hb-panic 0.55s ease-in-out infinite;
}
@keyframes np-hb-panic {
    0%, 40%, 100% { transform: translate(-50%, -50%) scale(0.92); opacity: 0.55; }
    20%, 60%      { transform: translate(-50%, -50%) scale(1.05); opacity: 0.9;  }
}

.np-hb-pulse--cancelled .np-hb-pulse-ring--outer,
.np-hb-pulse--cancelled .np-hb-pulse-ring--mid,
.np-hb-pulse--scheduled .np-hb-pulse-ring--outer,
.np-hb-pulse--scheduled .np-hb-pulse-ring--mid {
    animation-duration: 4.2s;
    animation-timing-function: ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
    .np-hb-pulse-ring--outer,
    .np-hb-pulse-ring--mid,
    .np-hb-pulse-ring--core {
        animation: none !important;
    }
}

/* ---------- RhythmBar (components/rhythm_bar.py) ----------
   Class names stay hb-rhythm-* — assets/heartbeat_rhythm_click.js and
   the data-tooltip rule below key on them. */
.hb-rhythm-cell--active:hover {
    transform: translateY(-2px);
    filter: brightness(1.08);
}
.hb-rhythm-cell--active:active {
    transform: translateY(0);
    filter: brightness(0.95);
}

.hb-rhythm-cell-wrap {
    flex: 1 1 0;
    min-width: 2px;
}

/* Rhythm cells are plain html.Divs — no Mantine Tooltip wrapper. The
   hover popup is a ::after pseudo-element so we don't pay for 120
   React portals churning every fast-tick. `display: none` (not
   visibility) keeps the 280px popup out of layout until hover, so it
   can never push the page's scrollWidth past the viewport. */
.hb-rhythm-cell--active::after {
    content: attr(data-tooltip);
    display: none;
    position: absolute;
    bottom: calc(100% + 8px);
    left: 50%;
    transform: translateX(-50%);
    background: var(--mantine-color-dark-7);
    color: var(--mantine-color-gray-0);
    border: 1px solid var(--mantine-color-dark-4);
    border-radius: 6px;
    box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.45);
    padding: 8px 10px;
    font-family: 'Spline Sans Mono', ui-monospace, Menlo, monospace;
    font-size: 11px;
    line-height: 1.45;
    white-space: pre;
    pointer-events: none;
    z-index: 200;
    max-width: 280px;
}

.hb-rhythm-cell--active:hover::after {
    display: block;
}

/* Let the tooltip escape the cell + bar on hover. */
.hb-rhythm-cell-wrap,
.hb-rhythm-cell-bar {
    overflow: visible;
}

/* ---------- MUI chart theming (dash-mui-charts) ----------
   The chart components don't follow data-mantine-color-scheme — series
   and colorScale colors are re-rendered server-side per scheme, but the
   legend/tooltip TEXT is SVG/DOM the package styles itself. Scoped
   overrides keep it readable in dark mode. */
[data-mantine-color-scheme="dark"] .MuiChartsLegend-root,
[data-mantine-color-scheme="dark"] .MuiChartsLegend-label,
[data-mantine-color-scheme="dark"] .MuiChartsLegend-series text {
    color: #C1C2C5 !important;
    fill: #C1C2C5 !important;
}

[data-mantine-color-scheme="dark"] .MuiChartsTooltip-root,
[data-mantine-color-scheme="dark"] .MuiChartsTooltip-paper {
    background: var(--mantine-color-dark-6) !important;
    color: var(--mantine-color-gray-2) !important;
}

/* Axis tick + label text (SVG <text fill>). Themed here via CSS rather than
   per-chart `tickLabelStyle.fill` / `labelStyle.fill` props — passing those
   style objects into ScatterChart/BarChart under dmc 2.8 tripped Mantine's
   color parser ("Expected color to be a string, instead got object"), which
   spammed render errors and destabilized the page. CSS is scheme-reactive
   with no server round trip and can never hand an object to a color parser. */
.MuiChartsAxis-tickLabel,
.MuiChartsAxis-tickLabel tspan,
.MuiChartsAxis-label,
.MuiChartsAxis-label tspan {
    fill: light-dark(#495057, #A6A7AB) !important;
}
:root[data-mantine-color-scheme="light"] .MuiChartsAxis-tickLabel,
:root[data-mantine-color-scheme="light"] .MuiChartsAxis-tickLabel tspan,
:root[data-mantine-color-scheme="light"] .MuiChartsAxis-label,
:root[data-mantine-color-scheme="light"] .MuiChartsAxis-label tspan {
    fill: #495057 !important;
}
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-tickLabel,
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-tickLabel tspan,
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-label,
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-label tspan {
    fill: #A6A7AB !important;
}
/* Axis lines/ticks a touch dimmer in dark so the plot doesn't glare. */
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-line,
:root[data-mantine-color-scheme="dark"] .MuiChartsAxis-tick {
    stroke: var(--mantine-color-dark-3) !important;
}

/* ---------- mobile bottom-sheet drawers (rhythm bucket) ---------- */
.np-hb-mobile-drawer .mantine-Drawer-content {
    border-top-left-radius: 16px;
    border-top-right-radius: 16px;
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    padding-bottom: env(safe-area-inset-bottom);
}
.np-hb-mobile-drawer .mantine-Drawer-body {
    overflow-y: auto;
}
.np-hb-mobile-drawer .mantine-Drawer-header {
    position: relative;
    padding-top: 18px;
}
.np-hb-mobile-drawer .mantine-Drawer-header::before {
    content: "";
    position: absolute;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px;
    height: 4px;
    border-radius: 2px;
    background: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
}

/* ---------------------------------------------------------------------------
   Admin-only nav section (Operations).

   Hidden by default and revealed by an attribute on the document root, rather
   than by a callback stamping display on each element it can find. The mobile
   drawer mounts its children lazily — on first open — so element-by-element
   toggling silently worked on desktop only, and admin links never showed on a
   phone. A root-keyed rule applies to markup whenever it appears.

   `.nav-owner-gated` is added only when Clerk auth is on; with auth off the
   pages are open anyway and the section ships visible.
   --------------------------------------------------------------------------- */
.nav-owner-gated { display: none; }
html[data-nav-admin] .nav-owner-gated { display: revert; }

/* ---------------------------------------------------------------------------
   dash-flows canvas height.

   DashFlows does not put its `style` prop on the element that carries its id —
   that div renders with NO inline style at all. So the usual trick of styling
   the component and letting React Flow's `height: 100%` canvas resolve against
   it cannot work: the id element is display:block/height:auto, and every
   percentage below it collapses to zero. Eighteen nodes and seventeen edges
   render correctly into a canvas nobody can see.

   Fixing it from outside instead: the wrapper is a flex column, and the
   unstyled element between it and the canvas is forced to fill. Independent of
   where the component chooses to put its own style.
   --------------------------------------------------------------------------- */
.np-flow-canvas {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
/* TWO unnamed divs sit between the wrapper and the canvas — the element
   carrying the Dash id, and one more inside it. Both have to be told to fill
   or the chain breaks at whichever one is missed. */
.np-flow-canvas > div,
.np-flow-canvas > div > div {
  height: 100%;
  min-height: 0;
  flex: 1 1 auto;
}
.np-flow-canvas .react-flow {
  height: 100%;
  min-height: 0;
}

/* DashFlows clamps node text to a single line by default. The wrapping itself
   is now the component's own `multiline` flag rather than a CSS override —
   but its label/content boxes still carry `overflow: hidden`, which clips the
   descenders on a wrapped line. Only the clipping needs undoing. */
.np-flow-canvas .df-node-content,
.np-flow-canvas .df-node-label,
.np-flow-canvas .df-node-body {
  overflow: visible;
  text-overflow: clip;
}

/* ---------------------------------------------------------------------------
   The home page network map.

   Same DashFlows canvas as the crawler board (so it inherits .np-flow-canvas
   for its height), but styled as part of the Origin Plot identity rather than
   as an operator board: each node wears its app's own --np-accent, the one
   already driving its card, and the whole box is a link.
   --------------------------------------------------------------------------- */
.np-home-map {
  --np-map-edge: light-dark(rgba(0, 0, 0, 0.18), rgba(255, 255, 255, 0.16));
  height: clamp(360px, 52vh, 560px);
  border-radius: var(--mantine-radius-md);
  overflow: hidden;
}

/* React Flow paints its own box on every default node; the look here comes
   from .np-map-node instead, so strip the base chrome rather than fight it. */
.np-home-map .react-flow__node-default {
  background: transparent;
  border: none;
  padding: 0;
  box-shadow: none;
}

.np-map-node {
  position: relative;
  background: light-dark(rgba(255, 255, 255, 0.82), rgba(255, 255, 255, 0.04));
  border: 1px solid color-mix(in srgb, var(--np-accent) 45%, transparent);
  border-left: 3px solid var(--np-accent);
  border-radius: var(--mantine-radius-md);
  padding: 8px 10px;
  font-size: 11px;
  transition: border-color 140ms ease, transform 140ms ease,
              box-shadow 140ms ease;
}

/* The hub is where the visitor already is, so it reads as a statement rather
   than an invitation — no hover lift, no pointer. */
.np-map-node--here { cursor: default; }
.np-map-node--focus {
  border-color: var(--np-accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--np-accent) 22%, transparent);
}

.np-map-node:not(.np-map-node--here):hover {
  border-color: var(--np-accent);
  transform: translateY(-1px);
  box-shadow: 0 6px 18px light-dark(rgba(0, 0, 0, 0.12), rgba(0, 0, 0, 0.45));
}

/* The hit target. The anchor IS the node's title text — it cannot wrap the
   node, because DashFlows resolves only the outermost component in a node
   slot and anything nested stays an unrenderable serialised dict. So the
   anchor sits in the text flow and its ::after does the stretching, covering
   the whole node box (.react-flow__node is absolutely positioned, so it is
   the containing block). A real link either way: keyboard-reachable, and
   right-clicks into a new tab. */
.np-map-hit {
  color: inherit;
  text-decoration: none;
  font-weight: 600;
}
.np-map-hit::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  border-radius: inherit;
}
.np-map-hit:focus-visible::after {
  outline: 2px solid var(--np-accent);
  outline-offset: 2px;
}

/* Panning is mouse-only, so say so with the cursor — but never over a node,
   which is a link and must keep the pointer. The control panel is gone (the
   wheel zooms instead), so there is nothing else to style here. */
.np-home-map .react-flow__pane { cursor: grab; }
.np-home-map .react-flow__pane.dragging { cursor: grabbing; }
.np-home-map .np-map-hit { cursor: pointer; }

/* ---------------------------------------------------------------------------
   Network-map node marks, theme-aware.

   A background-image cannot carry a media query inline, so lib/network_map
   puts BOTH urls on the element as custom properties and this picks one.
   Most apps set the two identically. 2plot.dev cannot: its mark is drawn in
   near-white pills, which disappear on a light page, so it ships a near-black
   counterpart. Keyed off data-mantine-color-scheme rather than
   prefers-color-scheme so it follows the app's OWN toggle — a visitor who
   forces light on a dark OS must still see the mark meant for the page they
   are looking at.
   --------------------------------------------------------------------------- */
.np-node-icon { background-image: var(--np-icon); }
[data-mantine-color-scheme="light"] .np-node-icon {
  background-image: var(--np-icon-light, var(--np-icon));
}
