/* 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);
}

/* Everything about a cell that is the SAME for all 120 of them lives
   here, not in an inline style dict repeated 120 times in the payload.
   The bar was 108 KB per rebuild — ~900 bytes a cell — and almost all of
   it was these identical declarations serialised over and over. Only the
   genuinely per-cell values (the fill height, a segment's share and
   colour) stay inline. (2026-08-28.) */
.hb-rhythm-cell-wrap {
    flex: 1 1 0;
    min-width: 2px;
    height: 36px;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
    cursor: default;
    transition: transform 120ms ease;
    position: relative;          /* anchor for the ::after tooltip */
}

.hb-rhythm-cell--active {
    cursor: pointer;
}

/* The filled column inside a cell. Stacks from the bottom so taller
   segments grow upward — "events piling up". */
.hb-rhythm-fill {
    width: 100%;
    border-radius: 1px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    justify-content: flex-end;
}

.hb-rhythm-seg {
    width: 100%;
    opacity: 0.95;
}

/* 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));
}

/* ---- chart slots (v0.21.0) --------------------------------------------
   A chart is mounted ONCE and never swapped: replacing a box's children
   remounts the chart, and a remount measured ~10 s of main thread on
   /admin/heartbeat while a 73.5 KB payload of plain divs applied in 1 ms.
   So the empty state is a sibling toggled by the slot's className — a
   prop update, not a component swap. See pages/heartbeat._chart_slot. */
.hb-slot > .hb-slot-note { display: none; }
.hb-slot--empty > .hb-slot-chart { display: none; }
.hb-slot--empty > .hb-slot-note { display: block; }

/* Countdown counters in UP NEXT. Plain spans written by
   assets/pulse_clocks.js — the Mantine color tokens are reproduced here
   because these are no longer dmc.Text components. */
.hb-countdown {
  font-family: var(--mantine-font-family-monospace);
  font-size: var(--mantine-font-size-xs);
  font-weight: 700;
}
.hb-countdown--blue   { color: var(--mantine-color-blue-text); }
.hb-countdown--yellow { color: var(--mantine-color-yellow-text); }
.hb-countdown--teal   { color: var(--mantine-color-teal-text); }
.hb-countdown--red    { color: var(--mantine-color-red-text); }
.hb-countdown--gray   { color: var(--mantine-color-gray-text); }

/* ---------- Plain-DOM panels (v0.22.0) ----------
   The satellite grid, the recent-event feed and the rhythm bar's legend
   were Mantine component trees; they are plain elements with class names
   now. This is NOT a byte-saving measure — bytes were measured to be the
   wrong target (73.5 KB of divs applied in 1 ms while 22.7 KB of charts
   blocked 10.9 s). It is a NODE count: applying a callback response costs
   in proportion to the subtree the renderer walks, because every Dash node
   is a DashWrapper that re-runs its selector, rebuilds its props and
   renders — and a Mantine component then adds its own useProps/useStyles
   pass that a classed <span> does not.
   Measured 2026-08-29 on the live page: the response carrying the
   satellite grid + event feed (593 nodes) blocked the main thread
   3,836 ms, the largest single block on the page. */

/* A "light variant" badge, the shape dmc.Badge(variant="light") drew.
   --mantine-color-<c>-light[-color] are scheme-aware, so this needs no
   light-dark() of its own. Shared by the satellite state and the event
   kind, which is why it is not named after either. */
.hb-chip {
    display: inline-block;
    padding: 2px 8px;
    border-radius: 999px;
    font-family: 'Spline Sans Mono', ui-monospace, Menlo, monospace;
    font-size: 10px;
    font-weight: 700;
    line-height: 1.6;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
}

.hb-chip--gray   { background: var(--mantine-color-gray-light);   color: var(--mantine-color-gray-light-color); }
.hb-chip--blue   { background: var(--mantine-color-blue-light);   color: var(--mantine-color-blue-light-color); }
.hb-chip--teal   { background: var(--mantine-color-teal-light);   color: var(--mantine-color-teal-light-color); }
.hb-chip--cyan   { background: var(--mantine-color-cyan-light);   color: var(--mantine-color-cyan-light-color); }
.hb-chip--red    { background: var(--mantine-color-red-light);    color: var(--mantine-color-red-light-color); }
.hb-chip--yellow { background: var(--mantine-color-yellow-light); color: var(--mantine-color-yellow-light-color); }

/* ---------- satellite cards ---------- */
.hb-sat-grid {
    display: grid;
    gap: var(--mantine-spacing-sm);
    grid-template-columns: 1fr;
}

@media (min-width: 48em) {
    .hb-sat-grid { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 75em) {
    .hb-sat-grid { grid-template-columns: repeat(3, 1fr); }
}

.hb-sat-head {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: nowrap;
}

.hb-sat-name {
    font-size: var(--mantine-font-size-sm);
    font-weight: 600;
}

/* Pushes the state chip to the trailing edge — dmc.Badge(ml="auto"). */
.hb-sat-head > .hb-chip { margin-left: auto; }

.hb-sat-url {
    margin-top: 6px;
    font-size: 10px;
    color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.hb-sat-cold {
    margin-top: 6px;
    font-size: 10px;
    color: light-dark(var(--mantine-color-yellow-8), var(--mantine-color-yellow-4));
}

.hb-sat-stats {
    display: flex;
    gap: 22px;
    margin-top: 6px;
}

/* Each stat is ONE element: the caption is drawn from data-label rather
   than being a second component stacked above the value. */
.hb-sat-stats > span {
    display: flex;
    flex-direction: column;
    font-weight: 600;
}

.hb-sat-stats > span::before {
    content: attr(data-label);
    font-size: 10px;
    font-weight: 400;
    text-transform: uppercase;
    color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}

/* ---------- recent-events feed ---------- */
.hb-ev-scroll {
    max-height: 360px;
    overflow: auto;
}

.hb-ev-table {
    width: 100%;
    table-layout: fixed;      /* required for the note column's ellipsis */
    border-collapse: collapse;
    font-size: var(--mantine-font-size-xs);
}

/* 88px, not 74: "time (CT)" measures 75px with padding and wrapped to
   two lines at 74, and the monospace 22:29:32 values want ~78. */
.hb-ev-table th:nth-child(1) { width: 88px; }
.hb-ev-table th:nth-child(2) { width: 86px; }
.hb-ev-table th:nth-child(3) { width: 132px; }
.hb-ev-table th:nth-child(4) { width: 92px; }

.hb-ev-table th {
    position: sticky;
    top: 0;
    z-index: 1;
    text-align: left;
    font-weight: 600;
    padding: 7px 10px;
    background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
    border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
}

.hb-ev-table td {
    padding: 7px 10px;
    vertical-align: middle;
    border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
}

.hb-ev-table tbody tr:nth-child(odd) {
    background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}

.hb-ev-table tbody tr:hover {
    background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}

.hb-ev-dim {
    color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}

/* dmc.Text(lineClamp=1) — one line, ellipsised. Works because the table
   is table-layout: fixed above. */
.hb-ev-note {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ---------- rhythm bar: quiet cells and legend ---------- */
.hb-rhythm {
    display: flex;
    flex-direction: column;
    gap: 2px;
}

.hb-rhythm-cell-bar {
    display: flex;
    gap: 1px;
    width: 100%;
    align-items: flex-end;
    padding: 4px 0;
}

/* A work-free minute is a single element: this ::before is the flat stub
   the old markup spent a fill div and a segment div on. 15% is what the
   fill arithmetic produced for every cell with work == 0. */
.hb-rhythm-cell--idle::before,
.hb-rhythm-cell--tick::before {
    content: "";
    width: 100%;
    height: 15%;
    border-radius: 1px;
}

.hb-rhythm-cell--idle::before {
    background: var(--mantine-color-gray-3);
    opacity: 0.22;
}

.hb-rhythm-cell--tick::before {
    background: var(--mantine-color-teal-3);
    opacity: 0.5;
}

.hb-rhythm-legend {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: 8px;
}

.hb-rhythm-keys {
    display: flex;
    flex-wrap: wrap;
    gap: var(--mantine-spacing-md);
}

.hb-rhythm-edge,
.hb-rhythm-key {
    font-family: 'Spline Sans Mono', ui-monospace, Menlo, monospace;
    font-size: var(--mantine-font-size-xs);
    color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-dark-2));
}

.hb-rhythm-key {
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

/* The swatch, as a pseudo-element instead of a styled div + a Text. */
.hb-rhythm-key::before {
    content: "";
    width: 10px;
    height: 10px;
    border-radius: 2px;
    opacity: 0.95;
    background: var(--mantine-color-gray-5);
}

.hb-rhythm-key--error::before      { background: var(--mantine-color-red-6); }
.hb-rhythm-key--task_end::before   { background: var(--mantine-color-teal-7); }
.hb-rhythm-key--task_start::before { background: var(--mantine-color-blue-5); }
.hb-rhythm-key--api_call::before   { background: var(--mantine-color-cyan-4); }
.hb-rhythm-key--note::before       { background: var(--mantine-color-yellow-5); }
.hb-rhythm-key--tick::before       { background: var(--mantine-color-teal-3); opacity: 0.5; }
.hb-rhythm-key--idle::before       { background: var(--mantine-color-gray-3); opacity: 0.22; }
