/* ═══════════════════════════════════════════════════════════════════════
   HA FABRIC — canvas-first cluster workspace
   ───────────────────────────────────────────────────────────────────────
   Tokens only. The palette is the product's existing gold/navy set from
   design-system.css; the identity of this surface comes from the canvas
   and the information design, not from a new colour story.

   The one rule worth restating here because it lives half in CSS: the
   edge animation IS the measurement. `--haf-dash` (particle spacing) and
   `--haf-speed` (traversal time) are written by fabric-canvas.js from the
   lag it actually read. An edge with no reading gets .haf-flow-unknown,
   which is deliberately NOT animated — a moving edge asserts data is
   flowing, and that is the one claim an unmeasured edge cannot make.
   ═══════════════════════════════════════════════════════════════════════ */

#ha-fabric {
  display: flex;
  flex-direction: column;
  gap: var(--sp-4);
  min-height: 520px;
  /* Container queries, not viewport ones. This panel's width is the
     viewport MINUS the sidebar and the page padding — measured 1165px
     inside a 1600px window — so a viewport breakpoint fires at the wrong
     moment every time. At 1600px a six-across strip fitted numerically
     and truncated every value to "idbe-h..." and "Async...". Same reason
     the Connections detail pane is container-based. */
  container-type: inline-size;
  container-name: hafabric;
}

/* ── top bar ─────────────────────────────────────────────────────────── */
.haf-bar {
  display: flex; align-items: center; justify-content: space-between;
  gap: var(--sp-3); flex-wrap: wrap;
  padding-bottom: var(--sp-3);
  border-bottom: 1px solid var(--border);
}
.haf-bar-left, .haf-bar-right {
  display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap;
}
.haf-title { margin: 0; font-size: 20px; font-weight: 680; color: var(--text-1); letter-spacing: -0.02em; }
.haf-tagline { margin: 3px 0 0; font-size: 12.5px; color: var(--text-3); }
.haf-dl-wide { grid-template-columns: minmax(0, 44%) minmax(0, 1fr); }

/* ── the decider ─────────────────────────────────────────────────────────
   One row per Patroni agent: can it still reach the store that arbitrates
   who is primary, and how long since it did.

   THREE dot states, not two. `is-unknown` is its own colour because an
   agent whose clock disagrees with ours, or that never answered, has NOT
   been shown to be stale — painting it as a failure would report a
   monitoring problem as a cluster one. */
.haf-dcs-agents { list-style: none; margin: var(--sp-3) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 5px; }
.haf-dcs-agent { display: flex; align-items: baseline; gap: 7px;
  font-size: 12px; min-width: 0; }
.haf-dcs-dot { flex: 0 0 auto; width: 7px; height: 7px; border-radius: 50%;
  background: var(--text-4); }
.haf-dcs-agent.is-ok .haf-dcs-dot { background: var(--success); }
.haf-dcs-agent.is-bad .haf-dcs-dot { background: var(--danger); }
.haf-dcs-agent.is-unknown .haf-dcs-dot {
  background: transparent; border: 1px solid var(--text-4); }
/* An inline span ignores text-overflow, so the name needs a block display
   or a long member name pushes the age out of the card. */
.haf-dcs-name { display: block; flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-2); }
.haf-dcs-age { flex: 0 0 auto; color: var(--text-3);
  font-variant-numeric: tabular-nums; }
.haf-dcs-agent.is-bad .haf-dcs-age { color: var(--danger); }
.haf-disclose { margin-top: var(--sp-3); font-size: 11.5px; }
.haf-disclose > summary { cursor: pointer; color: var(--text-3);
  list-style-position: outside; }
.haf-disclose > summary:hover { color: var(--text-2); }
.haf-disclose > summary:focus-visible { outline: 2px solid var(--accent);
  outline-offset: 2px; border-radius: 3px; }
.haf-disclose .haf-note { margin-top: 5px; }
.haf-dl-wide dd .haf-sub { display: block; font-size: 11px; margin-top: 1px; }
.haf-select {
  background: var(--bg-input); color: var(--text-1);
  border: 1px solid var(--border-mid); border-radius: var(--r-md);
  padding: 6px 10px; font-size: 13.5px; font-weight: 600;
  min-width: 190px; cursor: pointer;
}
.haf-select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.haf-sub { font-size: 12.5px; color: var(--text-3); }
.haf-btn {
  background: var(--bg-elevated); color: var(--text-2);
  border: 1px solid var(--border-mid); border-radius: var(--r-md);
  padding: 5px 11px; font-size: 13px; cursor: pointer;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.haf-btn:hover { color: var(--text-1); border-color: var(--border-strong); background: var(--bg-card-hover); }
.haf-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── chips ───────────────────────────────────────────────────────────── */
.haf-chip {
  display: inline-flex; align-items: center; gap: 6px;
  font-size: 11.5px; font-weight: 650; letter-spacing: 0.02em;
  padding: 3px 9px; border-radius: var(--r-full);
  border: 1px solid var(--border-mid); color: var(--text-2);
  background: var(--bg-elevated);
}
.haf-chip .haf-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.haf-chip-ok     { color: var(--success); border-color: var(--success-border); background: var(--success-bg); }
.haf-chip-warn   { color: var(--warning); border-color: var(--warning-border); background: var(--warning-bg); }
.haf-chip-danger { color: var(--danger);  border-color: var(--danger-border);  background: var(--danger-bg); }
.haf-chip-muted  { color: var(--text-3); }
.haf-count {
  font-size: 11px; font-weight: 700; color: var(--text-3);
  background: var(--bg-elevated); border-radius: var(--r-full);
  padding: 1px 7px; margin-left: 6px;
}

/* ── body: canvas over a masonry of panels ───────────────────────────── */
/* Two shapes were tried and both were ragged, which is what a column of
 * cards beside a card of a different height always is.
 *
 * A tall rail next to the canvas left the whole left side of the page
 * empty below the graph. A three-column grid fixed that but produced a
 * new kind of ragged: a grid row is as tall as its tallest member, so
 * every shorter card sat above a gap and the last row ended on an empty
 * cell. Levelling those with `align-items: stretch` only moved the
 * emptiness inside the short cards, where it looks deliberate and reads
 * worse — Findings stretched to 550px around 200px of content.
 *
 * So there is no pairing at all now. The canvas takes a full row at its
 * own natural height and every panel goes into one masonry beneath it.
 */
.haf-body { display: flex; flex-direction: column; gap: var(--sp-4); }

/* ── headline strip ──────────────────────────────────────────────────── */
/* Explicit column counts, NOT auto-fit.
 *
 * auto-fit decides how many fit and leaves whatever is left over on a
 * short final row — measured at 1165px it produced 4 + 2, so the strip
 * ended in a half-empty row, which is the same raggedness the panels
 * below were just rebuilt to avoid. There are six stats, so every count
 * used here divides six exactly and the last row is always full.
 */
.haf-kpis {
  display: grid;
  /* Two, three or six — every count divides six exactly, so the last row
     is always full whichever one applies. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--sp-3);
}
@container hafabric (min-width: 640px) {
  .haf-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@container hafabric (min-width: 1340px) {
  .haf-kpis { grid-template-columns: repeat(6, minmax(0, 1fr)); }
}
.haf-kpi {
  display: flex; align-items: flex-start; gap: 10px;
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-top: 2px solid var(--text-4);
  border-radius: var(--r-md);
  padding: 14px;
  min-width: 0;
}
.haf-kpi[data-status="ok"]   { border-top-color: var(--success); }
.haf-kpi[data-status="warn"] { border-top-color: var(--warning); }
.haf-kpi[data-status="bad"]  { border-top-color: var(--danger); }
.haf-kpi[data-status="none"] { border-top-color: var(--border-mid); }
.haf-kpi-label {
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--text-3);
}
.haf-kpi-ico {
  flex: 0 0 auto; width: 34px; height: 34px; border-radius: var(--r-md);
  display: grid; place-items: center;
}
.haf-kpi-ico svg { width: 19px; height: 19px; }
.haf-tint-ok     { background: var(--success-bg); color: var(--success); }
.haf-tint-info   { background: var(--accent-dim); color: var(--accent); }
.haf-tint-warn   { background: var(--warning-bg); color: var(--warning); }
.haf-tint-danger { background: var(--danger-bg);  color: var(--danger); }
.haf-tint-accent { background: var(--accent-dim); color: var(--accent); }
.haf-kpi-body { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.haf-kpi-value {
  font-size: 17px; font-weight: 680; color: var(--text-1);
  letter-spacing: -0.015em; line-height: 1.2;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.haf-kpi-sub {
  font-size: 11.5px; color: var(--text-3);
  display: flex; align-items: center; gap: 5px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.haf-kpi-sub code { font-size: 11px; }
.haf-dot-ok { width: 6px; height: 6px; border-radius: 50%; background: var(--success); flex: 0 0 auto; }

/* ── main row: topology + aside ──────────────────────────────────────── */
/* Nothing stretches. Measured on a healthy three-node cluster, the void
   BELOW the last content inside each card when the grid default applied:

       Replication slots   1200px card, 1003px void  (beside Operations)
       Timelines            248px card,  157px void  (beside WAL)
       Findings             321px card,  139px void  (beside Cortex)

   `stretch` is right when two cards are close in natural height and
   absurd at 6:1. The dashboard pass already established that the answer
   is neither `margin-top: auto` nor `space-between` inside the card --
   both only relocate the gap. Leftover belongs BETWEEN cards, where it
   reads as layout. */
.haf-main {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--sp-4);
  align-items: start;
}
@container hafabric (min-width: 880px) {
  .haf-main { grid-template-columns: minmax(0, 2.1fr) minmax(300px, 1fr); }
}
.haf-aside { display: flex; flex-direction: column; gap: var(--sp-4); }
/* The details card sizes to content and Quick Actions absorbs whatever is
   left, so the aside always ends level with the topology instead of
   floating above a gap. Its content is a button grid, which fills. */
.haf-aside > .haf-panel:last-child { flex: 1 1 auto; }
.haf-canvas-card { display: flex; flex-direction: column; }
.haf-canvas { border-radius: var(--r-md); }
/* The canvas holds its own fixed height now, so it must NOT absorb
   whatever the row hands the card -- that absorption is what turned a
   taller aside into empty canvas. */
.haf-canvas-card > .haf-canvas { flex: 0 0 auto; }

/* ── legend ──────────────────────────────────────────────────────────── */
.haf-legend {
  display: flex; flex-wrap: wrap; gap: var(--sp-3) var(--sp-5);
  padding-top: var(--sp-3); margin-top: var(--sp-3);
  border-top: 1px solid var(--border);
}
.haf-leg { display: inline-flex; align-items: center; gap: 7px; font-size: 11.5px; color: var(--text-3); }
.haf-leg-dot { width: 8px; height: 8px; border-radius: 50%; }
.haf-tintdot-ok     { background: var(--success); }
.haf-tintdot-accent { background: var(--accent); }
.haf-tintdot-muted  { background: var(--text-3); }
.haf-tintdot-danger { background: var(--danger); }
.haf-leg-flow { width: 22px; height: 0; border-top: 2px dashed var(--success); }
.haf-leg-unknown { width: 22px; height: 0; border-top: 2px dotted var(--text-4); }

/* ── quick actions ───────────────────────────────────────────────────── */
.haf-qa-grid { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.haf-qa {
  font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
  background: var(--bg-elevated); color: var(--text-2);
  border: 1px solid var(--border-mid); border-radius: var(--r-md);
  padding: 9px 10px; text-align: center;
  transition: color var(--t-fast), border-color var(--t-fast), background var(--t-fast);
}
.haf-qa:hover:not(:disabled) { color: var(--text-1); border-color: var(--border-strong); background: var(--bg-card-hover); }
.haf-qa:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
/* Disabled carries the reason in its title rather than vanishing — an
   absent control teaches nothing about why it is absent. */
.haf-qa:disabled { opacity: 0.42; cursor: not-allowed; }
.haf-qa-primary {
  grid-column: 1 / -1;
  background: var(--accent); border-color: var(--accent);
  color: var(--text-inverse, #1a1206);
}
.haf-qa-primary:hover:not(:disabled) { background: var(--accent-light); border-color: var(--accent-light); color: var(--text-inverse, #1a1206); }

/* ── nodes table ─────────────────────────────────────────────────────── */
.haf-panel-wide { width: 100%; }
/* Wide content scrolls inside its own box; the page body must never
   scroll horizontally. */
.haf-tablewrap { overflow-x: auto; }
.haf-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
.haf-table th {
  text-align: left; font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--text-3);
  padding: 0 var(--sp-3) var(--sp-2); white-space: nowrap;
}
.haf-table td {
  padding: var(--sp-3); border-top: 1px solid var(--border);
  color: var(--text-1); white-space: nowrap; vertical-align: middle;
}
.haf-table tr.is-down td { opacity: 0.7; }
.haf-th-right { text-align: right; }
.haf-td-node { display: inline-flex; align-items: center; gap: 8px; }
.haf-td-node code { font-size: 12px; }
.haf-node-dot.is-down { background: var(--danger); }
.haf-role {
  display: inline-block; font-size: 10.5px; font-weight: 700;
  letter-spacing: 0.04em; text-transform: uppercase;
  padding: 2px 8px; border-radius: var(--r-full);
  border: 1px solid var(--border-mid); color: var(--text-2);
}
.haf-role-primary { color: var(--success); border-color: var(--success-border); background: var(--success-bg); }
.haf-role-sync_replica, .haf-role-quorum_standby { color: var(--accent); border-color: var(--border-accent); background: var(--accent-dim); }
.haf-btn-sm { padding: 3px 9px; font-size: 12px; }

/* Six cards, three across, every card in a row the same height.
 *
 * Masonry packed them tightly but left the columns ending at different
 * depths, which reads as unfinished. A grid levels them — and levelling
 * only works without leaving a card stretched around a void because the
 * set was trimmed to SIX and the two dropped panels (Membership, Patroni
 * operations) had controls that already existed in Quick Actions. Two
 * launchers for one action is worse than none.
 *
 * Three divides six exactly, so the final row is always full.
 */
/* MASONRY, not a grid. Grid was the wrong tool here.
   -------------------------------------------------
   These cards differ wildly in natural height -- Operations lists every
   action ever proposed on the cluster (~750px) while Timelines is a
   single bar (~112px). A grid ROW is as tall as its tallest cell, so
   `align-items: start` stopped the short cards STRETCHING but left them
   parked at the top of a row Operations had made 750px tall. The card no
   longer contained the void; the row did. Same emptiness, one level out.

   Multi-column packs by height instead: each card flows into the column
   with the most room, so a tall card is simply a tall card and the short
   ones stack beside it. No JS, no measurement, and it degrades to a
   single column on its own at narrow widths.

   `break-inside: avoid` is what keeps a card whole -- without it a panel
   splits across the column boundary mid-content. */
.haf-grid {
  columns: 1;
  column-gap: var(--sp-4);
}
/* TWO columns at every width above the phone breakpoint, and three at
   none of them. Measured on this cluster's eleven status cards:

       columns   ragged bottom   wasted
          3          214px        368px
          2            3px          3px

   A masonry balances better the more items each column has to work with,
   and at three the odd card sizes cannot be made to come out level. Two
   also gives the prose cards -- Where reads go, the failover forecast --
   the measure they want instead of squeezing them into a third. */
@container hafabric (min-width: 680px) {
  .haf-grid { columns: 2; }
}
.haf-grid > .haf-panel {
  break-inside: avoid;
  /* The column flow has no row gap of its own. */
  margin-bottom: var(--sp-4);
}
.haf-grid > .haf-panel:last-child { margin-bottom: 0; }
/* `margin-top: auto` pushed a trailing note to the bottom of a STRETCHED
   card, to make the leftover read as padding. Nothing stretches now, so
   there is no leftover to push into -- and inside a multi-column flow an
   auto margin on a flex child is a no-op anyway. */
/* Inside the dialog it is content, not a card. */
.haf-panel-bare { border: none; padding: 0; background: none; }

/* The canvas is a WORKSPACE, not a box that grows to fit its graph.
   ---------------------------------------------------------------------
   It was content-sized and stretched by the grid to match a much taller
   aside, so a three-node cluster drew 323px of card into a 573px box and
   the rest read as a rendering fault. Sizing it to the content instead
   only moved the emptiness outside the card.

   A workspace answers this differently, and it is what the Connections
   topology already does: a FIXED viewport you look through, with the
   graph centred inside it and zoom and pan moving the VIEW. The height
   never changes, so nothing downstream reflows when the operator zooms.

   The dotted grid is load-bearing, not decoration. Empty space on a plain
   surface reads as something missing; the same space over a grid reads as
   room to pan into. The reference has just as much of it. */
.haf-canvas {
  position: relative;
  height: var(--haf-canvas-h, 520px);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background:
    radial-gradient(circle at 50% 14%, var(--accent-dim), transparent 62%),
    radial-gradient(circle, var(--border-mid) 1px, transparent 1.4px) 0 0 / 18px 18px,
    var(--bg-card);
  overflow: hidden;
}
/* The dots are a surface texture, not information: hold them well below
   the node cards so they never compete with a host:port. */
.haf-canvas::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--bg-card);
  opacity: 0.55;
  pointer-events: none;
}
.haf-canvas > * { position: relative; }
.haf-stage { position: absolute; inset: 0; }
.haf-edges { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.haf-nodes { position: absolute; inset: 0; }
.haf-entries { position: absolute; inset: 0; }

/* Layer toggles live in the legend, because they answer the same question
   it does: what am I looking at. */
.haf-leg-sep {
  width: 1px; height: 12px; background: var(--border-mid); opacity: 0.7;
  margin: 0 2px;
}
.haf-leg-toggle {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 2px 8px 2px 6px;
  font: inherit; font-size: 11.5px; line-height: 1.6;
  color: var(--text-3);
  background: none; border: 1px solid transparent; border-radius: 999px;
  cursor: pointer;
}
.haf-leg-toggle:hover { background: var(--bg-input); }
.haf-leg-toggle.is-on { color: var(--text-2); }
.haf-leg-box {
  width: 11px; height: 11px; border-radius: 3px;
  border: 1.5px solid var(--border-mid);
  background: transparent;
}
.haf-leg-toggle.is-on .haf-leg-box {
  background: var(--accent); border-color: var(--accent);
}
.haf-leg-toggle:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 2px;
}

/* ── entrypoint cards ────────────────────────────────────────────────── */
/* The tier CLIENTS reach: a pooler, a VIP, a proxy. Deliberately a
   different shape from a node card -- squarer corners, a lighter ground,
   no status rail -- because it is NOT a database and reading it as one is
   how an operator concludes the cluster has four members. Height is FIXED
   for the same reason the node card's is: the layout module places edge
   endpoints from a constant. */
.haf-entry {
  position: absolute;
  box-sizing: border-box;
  height: 74px;
  display: flex; flex-direction: column; gap: 2px; justify-content: center;
  padding: 8px 12px;
  background: var(--bg-card);
  border: 1px dashed var(--border-mid);
  border-radius: var(--r-md);
  overflow: hidden;
}
.haf-entry.is-down { border-color: var(--danger); opacity: 0.85; }
.haf-entry-head {
  display: flex; align-items: center; gap: 6px; justify-content: space-between;
}
.haf-entry-name {
  /* A span ignores text-overflow without an explicit block display -- the
     rail item and the node card both paid for this already. */
  display: block;
  font-weight: 600; font-size: 12.5px; color: var(--text-1);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.haf-entry-tag {
  flex: 0 0 auto;
  font-size: 10px; letter-spacing: 0.02em; text-transform: uppercase;
  padding: 1px 6px; border-radius: 999px;
  background: var(--bg-input); color: var(--text-3);
}
.haf-entry-sub {
  display: block;
  font-family: var(--font-mono, monospace); font-size: 10.5px; color: var(--text-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.haf-entry-route {
  display: block;
  font-size: 11px; color: var(--text-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.haf-entry-role { color: var(--text-3); }
.haf-entry-muted { color: var(--text-3); font-style: italic; }
.haf-entry-bad { color: var(--danger); }
/* What a client reaching through this doorway can DO -- measured through it,
   never read off its configuration. It sits on the route line rather than
   getting a line of its own because `.haf-entry`'s height is pinned in the
   layout (edge endpoints are placed from that constant), so a fourth line
   would detach every route edge. */
.haf-entry-serves {
  flex: 0 0 auto;
  font-size: 9.5px; letter-spacing: 0.03em; text-transform: uppercase;
  font-weight: 600;
  padding: 0 5px; margin-right: 5px; border-radius: 3px;
  vertical-align: 1px;
}
.haf-entry-serves-reads  { background: var(--info-bg, var(--bg-input)); color: var(--info, var(--text-2)); }
.haf-entry-serves-writes { background: var(--accent-soft, var(--bg-input)); color: var(--accent); }
/* A hairline down the leading edge, so read and write doorways are
   distinguishable across the whole tier at a glance without either of them
   reading as an alert -- both are healthy states. */
.haf-entry.is-reads  { box-shadow: inset 3px 0 0 var(--info, var(--text-3)); }
.haf-entry.is-writes { box-shadow: inset 3px 0 0 var(--accent); }

/* A doorway whose backend is not a member of this cluster. Bordered rather
   than filled: it is a real entrypoint that real clients are using, so it
   must stay legible -- the colour says "not what you think", not "broken". */
.haf-entry.is-misrouted { border-color: var(--warning-border); }
.haf-entry.is-misrouted .haf-entry-route { color: var(--warning); }

/* The client route. Dashed and STILL: a replication edge's motion encodes
   a measured lag, and a client route has no such quantity -- animating it
   would imply one. */
.haf-route {
  fill: none;
  stroke: var(--text-3);
  stroke-width: 1.5;
  stroke-dasharray: 2 5;
  opacity: 0.55;
}

/* ── node cards ──────────────────────────────────────────────────────── */
.haf-node {
  position: absolute;
  /* Height is FIXED, not content-derived. The layout module places edge
     endpoints from a constant node height, so a long hostname wrapping to
     a second line would silently detach every edge from its card. */
  box-sizing: border-box;
  height: var(--haf-node-h, 92px);
  display: flex; flex-direction: column; gap: 5px; justify-content: center;
  padding: 11px 13px;
  text-align: left; font: inherit; cursor: pointer;
  background: var(--bg-elevated);
  border: 1px solid var(--border-mid);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-md);
  /* left/top are transitioned so a PROMOTED node visibly travels to the
     primary band instead of teleporting there on the next repaint. */
  transition: left var(--t-slow) var(--ease-spring),
              top var(--t-slow) var(--ease-spring),
              border-color var(--t-base), box-shadow var(--t-base),
              transform var(--t-fast);
}
.haf-node:hover { transform: translateY(-2px); border-color: var(--border-strong); }
.haf-node:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
.haf-node.is-selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent), var(--shadow-lg); }
.haf-node.is-down { opacity: 0.72; border-style: dashed; }

.haf-node-top { display: flex; align-items: center; gap: 7px; }
.haf-node-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-4); flex: 0 0 auto; }
.haf-node-primary       .haf-node-dot { background: var(--success); box-shadow: 0 0 0 3px var(--success-bg); }
.haf-node-sync_replica  .haf-node-dot,
.haf-node-quorum_standby .haf-node-dot { background: var(--accent); box-shadow: 0 0 0 3px var(--accent-dim); }
.haf-node-async_replica .haf-node-dot { background: var(--text-3); }
.haf-node.is-down       .haf-node-dot { background: var(--danger); }

.haf-node-name {
  font-size: 13.5px; font-weight: 650; color: var(--text-1);
  display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  min-width: 0; flex: 1 1 auto;
}
.haf-node-tli {
  font-size: 10.5px; font-weight: 700; color: var(--text-3);
  background: var(--bg-base); border-radius: var(--r-sm);
  padding: 1px 5px; flex: 0 0 auto;
}
/* An inline span ignores text-overflow entirely — it needs a block
   display or the host string simply overflows the card. */
.haf-node-host {
  display: block; font-size: 11.5px; color: var(--text-3);
  font-family: var(--font-mono, ui-monospace, monospace);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* One line, always. The card height is a constant the layout module places
   edge endpoints from, so a foot that wraps to two lines pushes the whole
   content past the box and clips the host:port above it — measured at a
   390px viewport, where the fitted card is 164px and "SYNC STANDBY" plus
   "0 B · 2 ms" needs 158px of a 138px content width.

   The ROLE is what truncates and the LAG is what does not: the role is
   also carried by the dot colour, the card's band, the node's aria-label
   and the Nodes table, while the lag is a MEASUREMENT that appears
   nowhere else on the canvas. */
.haf-node-foot {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 8px; flex-wrap: nowrap; min-width: 0;
}
.haf-node-role {
  font-size: 11px; font-weight: 600; color: var(--text-2);
  text-transform: uppercase; letter-spacing: 0.04em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.haf-node-stat {
  font-size: 11.5px; color: var(--text-3);
  flex: 0 0 auto; white-space: nowrap;
}
.haf-node.is-drifted { border-color: var(--warning-border); }

/* Other connection rows that resolve to this same node. Deliberately quiet:
   the fold is correct and routine, and this exists so an operator can see
   that their second registration was absorbed rather than lost. */
.haf-node-alias {
  display: inline-block; margin-left: 4px; padding: 0 4px;
  border: 1px solid var(--border-mid); border-radius: 4px;
  font-size: 10px; line-height: 1.5; color: var(--text-3);
  font-family: var(--font-sans, inherit); vertical-align: 1px;
}

/* Patroni node tags — a capability the operator has REMOVED from this node.

   Absolutely positioned, and that is load-bearing rather than cosmetic: the
   card height is a constant the layout module places edge endpoints from
   (see .haf-node), so a chip row in normal flow would push the foot down
   and silently detach every rail from its card. Out of flow it cannot.

   Clipped rather than wrapped for the same reason. Each chip carries its
   own title, and the two that change what an operator can DO — nofailover
   and replicatefrom — are also in the card's aria-label, so a clipped chip
   costs a hint and never the fact. */
/* The card keeps its fixed height and RESERVES the strip's band instead.
   Measured before this: the absolutely-positioned chips overlapped the
   role/lag row by 11px, so "SYNC STANDBY" and "NO-FAILOVER" were drawn on
   top of each other. Padding is inside the border-box, so the height the
   layout module places edges from does not move. */
.haf-node:has(.haf-node-tags) { padding-bottom: 22px; }
.haf-node-tags {
  position: absolute; left: 13px; right: 13px; bottom: 6px;
  display: flex; gap: 4px; align-items: center;
  overflow: hidden; white-space: nowrap; pointer-events: none;
}
.haf-node-tag {
  flex: 0 0 auto; padding: 0 4px; border-radius: 3px;
  font-size: 9.5px; line-height: 14px; font-weight: 700;
  letter-spacing: 0.02em; text-transform: uppercase;
  color: var(--text-3); background: var(--bg-base);
  border: 1px solid var(--border-mid);
}
/* A node that cannot be promoted is the one tag worth a border, because it
   changes what the whole cluster can do rather than what one node does. */
.haf-node.is-barred { border-style: dashed; }
.haf-node.is-barred .haf-node-tag:first-child {
  color: var(--warning); border-color: var(--warning-border);
}

/* ── edges ───────────────────────────────────────────────────────────── */
.haf-rail { fill: none; stroke: var(--border-mid); stroke-width: 2; }
.haf-rail-sync { stroke-width: 3.5; }
.haf-rail-ok     { stroke: var(--border-mid); }
.haf-rail-warn   { stroke: var(--warning-border); }
.haf-rail-danger { stroke: var(--danger-border); }
.haf-rail-muted  { stroke: var(--border); }
.haf-rail-severed { stroke-dasharray: 5 9; stroke: var(--danger); opacity: 0.55; }
/* A stream that hangs off ANOTHER STANDBY. Drawn thinner and dotted so the
   chain reads as a chain: the primary is not what keeps this node fed. */
.haf-rail-cascaded { stroke-dasharray: 2 4; stroke-width: 2; }
.haf-sever { stroke: var(--danger); stroke-width: 2.5; stroke-linecap: round; fill: none; }

.haf-flow { fill: none; stroke-width: 3; stroke-linecap: round; }
.haf-flow-ok     { stroke: var(--success); }
.haf-flow-warn   { stroke: var(--warning); }
.haf-flow-danger { stroke: var(--danger); }
/* Unmeasured: static, muted, dashed. Never animated. */
.haf-flow-unknown {
  stroke: var(--text-4); stroke-width: 2;
  stroke-dasharray: 2 7; opacity: 0.8;
}

@keyframes haf-stream { to { stroke-dashoffset: calc(var(--haf-dash, 20) * -1px); } }

@media (prefers-reduced-motion: no-preference) {
  .haf-flow-ok, .haf-flow-warn, .haf-flow-danger {
    animation: haf-stream var(--haf-speed, 3s) linear infinite;
  }
}
.haf-flow.is-still { opacity: 0.85; }

/* ── entrance ────────────────────────────────────────────────────────── */
@keyframes haf-rise { from { opacity: 0; transform: translateY(10px) scale(0.98); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: no-preference) {
  .haf-node.is-entering {
    animation: haf-rise 420ms var(--ease-out) backwards;
    animation-delay: calc(var(--haf-i, 0) * 60ms);
  }
}

/* ── rail panels ─────────────────────────────────────────────────────── */
/* Same card grammar as Vantage: a 2px status rail across the top edge,
   --r-md corners, and a small uppercase title. The rail is not decoration
   — it encodes the panel's own worst finding, so the state of the cluster
   is readable from the edges of the cards before a single value is. */
.haf-panel {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-top: 2px solid var(--text-4);
  border-radius: var(--r-md);
  padding: var(--sp-4);
  min-width: 0;
}
.haf-panel[data-status="ok"]   { border-top-color: var(--success); }
.haf-panel[data-status="warn"] { border-top-color: var(--warning); }
.haf-panel[data-status="bad"]  { border-top-color: var(--danger); }
.haf-panel[data-status="none"] { border-top-color: var(--border-mid); }
.haf-panel-title {
  display: flex; align-items: center; gap: var(--sp-2);
  margin: 0 0 var(--sp-3); font-size: 10.5px; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-3);
}
.haf-clean, .haf-unavail { font-size: 12.5px; color: var(--text-3); line-height: 1.55; }
.haf-unavail { color: var(--warning); }
.haf-note { margin: var(--sp-3) 0 0; font-size: 12px; color: var(--text-3); line-height: 1.55; }
.haf-dash { color: var(--text-4); cursor: help; border-bottom: 1px dotted var(--text-4); }
.haf-bad { color: var(--danger); }
.haf-ok { color: var(--success); }

/* The forecast list names only the replicas that did NOT qualify, and why.
   Same dot vocabulary as the decider so a reader learns it once: filled
   danger = disqualified by a stated rule, hollow = not judged. */
.haf-forecast-list { list-style: none; margin: var(--sp-3) 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 6px; }
.haf-forecast-list > li { display: flex; align-items: baseline; gap: 7px;
  font-size: 12px; min-width: 0; flex-wrap: wrap; }
.haf-forecast-list > li.is-bad .haf-dcs-dot { background: var(--danger); }
.haf-forecast-list > li.is-unknown .haf-dcs-dot {
  background: transparent; border: 1px solid var(--text-4); }
.haf-forecast-why { flex: 1 1 100%; margin-left: 14px; color: var(--text-3);
  font-size: 11.5px; }

/* The disclaimer is part of the reading, not a footnote to be styled away:
   an operator deciding on a maintenance window is exactly who would act on
   a number they took for a guarantee. */
.haf-computed { border-top: 1px solid var(--border); margin-top: var(--sp-3);
  padding-top: var(--sp-2); color: var(--text-4); }
.haf-warn { color: var(--warning); }

/* ── findings ────────────────────────────────────────────────────────── */
.haf-findings { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.haf-finding {
  border-left: 3px solid var(--border-mid); padding-left: var(--sp-3);
  display: flex; flex-direction: column; gap: 3px;
}
.haf-finding-title { font-size: 13px; font-weight: 650; color: var(--text-1); }
.haf-finding-detail { font-size: 12px; color: var(--text-3); line-height: 1.55; }
.haf-sev-danger  { border-left-color: var(--danger); }
.haf-sev-warn    { border-left-color: var(--warning); }
.haf-sev-unknown { border-left-color: var(--text-4); }
.haf-sev-info    { border-left-color: var(--accent); }

/* ── timeline ribbon ─────────────────────────────────────────────────── */
.haf-ribbon { display: flex; flex-direction: column; gap: 7px; }
.haf-ribbon-row { display: flex; align-items: center; gap: 9px; }
.haf-ribbon-tli { font-size: 11px; font-weight: 700; color: var(--text-3); min-width: 42px; }
.haf-ribbon-track { flex: 1 1 auto; height: 3px; border-radius: 2px; background: var(--border-mid); }
.haf-ribbon-row.is-current .haf-ribbon-track { background: var(--accent); }
.haf-ribbon-row.is-current .haf-ribbon-tli { color: var(--accent); }
.haf-ribbon-nodes {
  font-size: 11.5px; color: var(--text-3); max-width: 46%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ── slots ───────────────────────────────────────────────────────────── */
.haf-slots { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.haf-slot { display: flex; flex-direction: column; gap: 4px; }
.haf-slot-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.haf-slot-name { font-size: 12.5px; font-weight: 600; color: var(--text-1); font-family: var(--font-mono, ui-monospace, monospace); }
.haf-slot-status { font-size: 11px; color: var(--text-3); }
.haf-slot-bar { display: block; height: 5px; border-radius: 3px; background: var(--bg-base); overflow: hidden; }
.haf-slot-bar > span { display: block; height: 100%; background: var(--success); border-radius: 3px; transition: width var(--t-slow) var(--ease-out); }
.haf-slot.is-warn .haf-slot-bar > span { background: var(--warning); }
.haf-slot.is-lost .haf-slot-bar > span { background: var(--danger); }
.haf-slot.is-lost .haf-slot-name { color: var(--danger); }
.haf-slot-ret { font-size: 11px; color: var(--text-3); }

/* ── definition lists ────────────────────────────────────────────────── */
.haf-dl { display: grid; grid-template-columns: minmax(0, 42%) minmax(0, 1fr); gap: 7px var(--sp-3); margin: 0; font-size: 12.5px; }
.haf-dl dt { color: var(--text-3); }
.haf-dl dd { margin: 0; color: var(--text-1); overflow-wrap: anywhere; }
.haf-dl dd code { font-size: 11.5px; color: var(--text-2); }

/* ── drawer ──────────────────────────────────────────────────────────── */
.haf-drawer {
  position: fixed; top: 0; right: 0; bottom: 0; width: min(420px, 92vw);
  background: var(--bg-surface); border-left: 1px solid var(--border-mid);
  box-shadow: var(--shadow-xl, 0 0 40px rgba(0,0,0,0.4));
  padding: var(--sp-5); overflow-y: auto; z-index: 60;
}
@media (prefers-reduced-motion: no-preference) {
  @keyframes haf-slide { from { transform: translateX(24px); opacity: 0; } to { transform: none; opacity: 1; } }
  .haf-drawer { animation: haf-slide var(--t-base) var(--ease-out); }
}
.haf-drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.haf-drawer-name { font-size: 16px; font-weight: 650; color: var(--text-1); }
.haf-drawer-host { font-size: 12px; color: var(--text-3); font-family: var(--font-mono, ui-monospace, monospace); }
.haf-drawer-down {
  background: var(--danger-bg); border: 1px solid var(--danger-border);
  border-radius: var(--r-md); padding: var(--sp-3); margin-bottom: var(--sp-4);
  font-size: 12.5px; color: var(--text-2); line-height: 1.55;
}
.haf-x { background: none; border: none; color: var(--text-3); font-size: 24px; line-height: 1; cursor: pointer; padding: 0 4px; }
.haf-x:hover { color: var(--text-1); }
.haf-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── empty / failed ──────────────────────────────────────────────────── */
.haf-empty, .haf-fail {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: var(--sp-3); text-align: center; padding: var(--sp-12) var(--sp-6);
  border: 1px dashed var(--border-mid); border-radius: var(--r-lg);
  background: var(--bg-card); min-height: 320px;
}
.haf-empty-title { font-size: 15px; font-weight: 650; color: var(--text-2); }
.haf-empty-sub { font-size: 13px; color: var(--text-3); max-width: 460px; line-height: 1.6; }
/* A failed read renders an error, never an empty state. An empty state
   asserts "you have nothing"; a failed read asserts nothing at all. */
.haf-fail { border-color: var(--danger-border); background: var(--danger-bg); }
.haf-fail strong { color: var(--text-1); font-size: 14.5px; }
.haf-fail span { color: var(--text-2); font-size: 13px; max-width: 520px; }

/* ── membership: actions, notices, attach dialog ─────────────────────── */
.haf-actions { display: flex; gap: var(--sp-2); flex-wrap: wrap; margin-bottom: var(--sp-2); }
.haf-btn-primary { background: var(--accent); color: var(--text-inverse, #1a1206); border-color: var(--accent); font-weight: 650; }
.haf-btn-primary:hover:not(:disabled) { background: var(--accent-light); border-color: var(--accent-light); color: var(--text-inverse, #1a1206); }
.haf-btn-danger { color: var(--danger); border-color: var(--danger-border); background: var(--danger-bg); }
.haf-btn-danger:hover:not(:disabled) { color: var(--danger); border-color: var(--danger); background: var(--danger-bg); }
.haf-btn:disabled { opacity: 0.5; cursor: not-allowed; }

.haf-notice {
  margin-top: var(--sp-3); padding: var(--sp-3);
  border-radius: var(--r-md); border: 1px solid var(--border-mid);
  font-size: 12.5px; line-height: 1.55; color: var(--text-2);
}
.haf-notice-ok     { border-color: var(--success-border); background: var(--success-bg); }
.haf-notice-warn   { border-color: var(--warning-border); background: var(--warning-bg); }
.haf-notice-danger { border-color: var(--danger-border);  background: var(--danger-bg); }

.haf-drawer-foot { margin-top: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--border); }

.haf-modal-back {
  position: fixed; inset: 0; background: var(--bg-overlay);
  display: flex; align-items: center; justify-content: center;
  padding: var(--sp-4); z-index: 80;
}
.haf-modal {
  background: var(--bg-surface); border: 1px solid var(--border-mid);
  border-radius: var(--r-lg); box-shadow: var(--shadow-xl, 0 20px 60px rgba(0,0,0,0.45));
  width: min(560px, 100%); max-height: min(80vh, 720px);
  display: flex; flex-direction: column; padding: var(--sp-5);
}
@media (prefers-reduced-motion: no-preference) {
  @keyframes haf-pop { from { opacity: 0; transform: translateY(10px) scale(0.985); } to { opacity: 1; transform: none; } }
  .haf-modal { animation: haf-pop var(--t-base) var(--ease-out); }
}
/* Head and foot are fixed bands in a flex column; without this the list
   compresses them and the buttons drift under the content at narrow
   heights. Same fix the drill-down header needed. */
.haf-modal-head, .haf-modal-foot { flex-shrink: 0; }
.haf-modal-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); }
.haf-modal-head h3 { margin: 0; font-size: 16px; font-weight: 650; color: var(--text-1); }
.haf-modal-foot { display: flex; justify-content: flex-end; gap: var(--sp-2); margin-top: var(--sp-4); }

.haf-cand { list-style: none; margin: var(--sp-3) 0 0; padding: 0; overflow-y: auto; flex: 1 1 auto; }
.haf-cand-row { padding: var(--sp-3) 0; border-bottom: 1px solid var(--border); }
.haf-cand-row:last-child { border-bottom: none; }
.haf-cand-row label { display: flex; align-items: flex-start; gap: var(--sp-3); cursor: pointer; }
.haf-cand-row.is-blocked label { cursor: not-allowed; opacity: 0.6; }
.haf-cand-row input { margin-top: 3px; flex: 0 0 auto; accent-color: var(--accent); }
.haf-cand-main { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.haf-cand-name { font-size: 13.5px; font-weight: 600; color: var(--text-1); }
.haf-cand-host { font-size: 11.5px; color: var(--text-3); font-family: var(--font-mono, ui-monospace, monospace); }
/* The reason a row is refused is the point of showing it at all, so it
   gets its own line rather than a title attribute nobody hovers. */
.haf-cand-why { display: block; margin: 4px 0 0 30px; font-size: 11.5px; color: var(--warning); line-height: 1.5; }
.haf-fail-inline {
  margin-top: var(--sp-3); padding: var(--sp-3); border-radius: var(--r-md);
  border: 1px solid var(--danger-border); background: var(--danger-bg);
  font-size: 12.5px; color: var(--text-1);
}
.haf-fail-inline span { display: block; margin-top: 4px; color: var(--text-2); }

/* ── Patroni operations dock ─────────────────────────────────────────── */
.haf-verbs { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.haf-verb { display: flex; flex-direction: column; gap: 3px; }
.haf-verb-btn {
  display: flex; align-items: center; gap: 8px; width: 100%;
  text-align: left; font: inherit; cursor: pointer;
  background: var(--bg-elevated); color: var(--text-1);
  border: 1px solid var(--border-mid); border-radius: var(--r-md);
  padding: 8px 11px;
  transition: border-color var(--t-fast), background var(--t-fast);
}
.haf-verb-btn:hover:not(:disabled) { border-color: var(--border-strong); background: var(--bg-card-hover); }
.haf-verb-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.haf-verb-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.haf-verb-label { font-size: 13px; font-weight: 600; flex: 1 1 auto; }
/* The gate is stated on the button, not discovered on click. */
.haf-verb-blurb { font-size: 11.5px; color: var(--text-3); line-height: 1.5; padding-left: 2px; }

.haf-field { display: flex; flex-direction: column; gap: 5px; margin-top: var(--sp-4); }
.haf-field > span { font-size: 12px; font-weight: 600; color: var(--text-2); }
.haf-field select, .haf-field input[type="text"], .haf-field input[type="number"] {
  background: var(--bg-input); color: var(--text-1);
  border: 1px solid var(--border-mid); border-radius: var(--r-md);
  padding: 8px 10px; font: inherit; font-size: 13px; width: 100%;
}
.haf-field select:focus-visible,
.haf-field input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.haf-field-inline { flex-direction: row; align-items: flex-start; gap: 9px; }
.haf-field-inline input { margin-top: 2px; accent-color: var(--accent); }
.haf-two { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-3); }
.haf-field-inline span { font-weight: 400; font-size: 12.5px; color: var(--text-2); }

/* ── responsive ──────────────────────────────────────────────────────── */

@media (max-width: 720px) {
  .haf-qa-grid { grid-template-columns: 1fr; }
  .haf-bar-right { width: 100%; }
  .haf-canvas { min-height: 380px; }
  .haf-bar { align-items: flex-start; }
  .haf-select { min-width: 0; width: 100%; }
  .haf-drawer { width: 100vw; }
  .haf-dl { grid-template-columns: minmax(0, 1fr); gap: 2px; }
  .haf-dl dt { margin-top: 8px; font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
}

/* Light theme: --bg-elevated and --bg-card are BOTH #ffffff there, so a
   card on a card loses its edge. Check a computed paint, not token names. */
[data-theme="light"] .haf-canvas { background: var(--bg-base); }
[data-theme="light"] .haf-node { background: var(--bg-card); }
[data-theme="light"] .haf-node-tli { background: var(--bg-surface); }
[data-theme="light"] .haf-node-tag { background: var(--bg-surface); }
[data-theme="light"] .haf-slot-bar { background: var(--bg-surface); }

/* ── the left column of .haf-main ─────────────────────────────────────── */
/* A stack, so the topology, Findings and Cortex Analysis flow down it and
   the column keeps pace with the five-panel aside rather than ending a
   screenful early. Both of those cards are statements ABOUT this cluster,
   so the reading order improves as well: see the shape, then what we
   found in it, then what Cortex makes of it. */
.haf-col { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* Operations lists every action ever proposed on the cluster and was the
   card that forced a 1200px row. Bounded so the column stays legible; the
   list keeps every row, reachable by scrolling, rather than truncating. */
.haf-panel.cluster-card-expandable > .cluster-details-inner {
  max-height: 540px;
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* The workspace is focusable, so it needs a visible focus ring — and it is
   a gesture surface, so the keyboard hint below it is not optional
   decoration: it is how a keyboard user learns the pan exists. */
/* THE VIEWPORT holds the fixed height, not the surface inside it.
   The surface lives inside the transformed stage and therefore scales with
   the zoom; pinning the height there made the whole card grow and shrink
   as the operator zoomed, which is the behaviour being fixed. The viewport
   sits outside the transform, so it never moves.

   `overflow: hidden`, not `auto`: a zoomed-in workspace is reached by
   DRAGGING, and a scrollbar that appears and disappears as you zoom is
   both noise and an unwanted scrollbar. */
.haf-canvas-vp {
  position: relative;
  height: var(--haf-vp-h, 480px);
  overflow: hidden;
  border-radius: var(--r-lg);
  touch-action: manipulation;
  overscroll-behavior: contain;
  flex: 0 0 auto;
}
.haf-canvas-vp:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
.haf-vp-help {
  margin: var(--sp-2) 0 0;
  font-size: 11.5px;
  color: var(--text-3);
}
.haf-vp-help kbd {
  font: inherit;
  font-variant-numeric: tabular-nums;
  padding: 1px 5px;
  border: 1px solid var(--border);
  border-bottom-width: 2px;
  border-radius: 4px;
  background: var(--bg-elevated);
  color: var(--text-2);
}
