/* Render viewport ownership.
 *
 * The graph surface is an interaction plane, not selectable page content.
 * Browser text/callout selection is disabled across the surface while graph
 * nodes keep their explicit Rust/Leptos activation semantics. Labels and the
 * coloured strands never participate in hit testing: a connection is reached
 * through its single composite hit path, so a pair carrying three relations is
 * still one target rather than three competing ones.
 *
 * On compact screens the heading/status block and renderer form one viewport
 * composition. The viewport container owns exactly one dynamic viewport height
 * and CSS Grid gives the renderer whatever block space remains after the real
 * heading (and an optional runtime error) has laid itself out. There is no
 * measured pixel constant and no JS resize bookkeeping: browser chrome changes
 * update `dvh`, Grid recomputes the remaining track, and the bottom telemetry
 * therefore remains visible in the same viewport as the section heading.
 */

.renderer-surface--wasm,
.renderer-surface--wasm * {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* `touch-action` is decided by the browser when a touch gesture starts; setting
 * it only after Rust crosses the drag threshold is too late on Safari/Chrome.
 * An active graph therefore owns pan gestures from pointer-down so rotating the
 * world can never become document scrolling mid-gesture. Blocking overlays put
 * the surface back under normal page gesture ownership via `data-pointer-blocked`. */
.renderer-surface--wasm[data-pointer-blocked="false"] {
  touch-action: none;
}

.renderer-surface--wasm[data-dragging="true"] {
  cursor: grabbing;
}

/* A blocking runtime error makes the whole render plane observational only.
 * Rust also applies the native `inert` attribute, which removes every node and
 * render control from pointer/keyboard activation; this rule is the visual and
 * pointer-level half of the same contract. Retry stays outside the surface. */
.renderer-surface--wasm[data-renderer-state="inactive"] {
  opacity: 0.42;
  pointer-events: none;
}

.renderer-surface--wasm .wasm-strand,
.renderer-surface--wasm .wasm-labels {
  pointer-events: none;
}

/* Structurally the viewport groups heading + stage, but on tablet/desktop it
 * disappears from layout so the existing graph grid keeps its original
 * heading/stage/details geometry. */
.graph-viewport {
  display: contents;
}

@media (max-width: 719px) {
  .graph-section {
    padding-top: 0;
  }

  .graph-layout {
    display: block;
    margin-top: 0;
  }

  .graph-viewport {
    display: grid;
    width: 100%;
    height: 100vh;
    height: 100dvh;
    min-height: 0;
    grid-template-rows: auto minmax(0, 1fr);
    align-items: stretch;
  }

  .graph-viewport > .section-heading {
    grid-row: 1;
    min-height: 0;
  }

  .graph-viewport > .graph-stage {
    grid-row: 2;
    width: 100%;
    height: 100%;
    min-height: 0;
  }

  /* Runtime failures become a genuine middle row instead of an overlay or a
   hard-coded subtraction. `:has()` lets the stage move down only when that row
   actually exists. */
  .graph-viewport:has(> .runtime-banner) {
    grid-template-rows: auto auto minmax(0, 1fr);
  }

  .graph-viewport > .runtime-banner {
    grid-row: 2;
    margin: 0 0 0.75rem;
  }

  .graph-viewport:has(> .runtime-banner) > .graph-stage {
    grid-row: 3;
  }

  .renderer-surface.renderer-surface--wasm {
    width: 100%;
    height: 100%;
    min-height: 0;
    max-height: none;
  }

  /* The graph gets the stage minus the telemetry line. The overlay stays
     anchored to the surface edge where the control contract puts it; what
     changes is that the render layers stop reaching underneath it, so the
     bottom of the graph is never read through a pill of text. */
  .graph-viewport .renderer-surface--wasm {
    --telemetry-strip: 2.75rem;
  }

  .graph-viewport .renderer-gpu-canvas,
  .graph-viewport .renderer-svg-layer {
    /* Both layers are given the same computed height rather than being left to
       resolve one. `bottom` with `height: auto` sizes a div, but the canvas is
       a replaced element: `auto` resolves through its own 300×150 attribute
       ratio and `bottom` is dropped, so the WebGPU frame occupied a 2:1 box in
       the top half of the surface while the SVG layer kept the whole one. The
       two layers then measured different stages, chose different design
       viewports, and drew one graph as two — bodies above, labels below. The
       fallback keeps the layers coincident if the strip ever goes away. */
    height: calc(100% - var(--telemetry-strip, 0px));
  }

  /* The heading is a caption on a phone, not a billboard: at 10vw it took a
     third of the screen out of the graph it names. */
  .graph-viewport > .section-heading {
    gap: 0.5rem;
    padding-bottom: 0.35rem;
  }

  .graph-viewport > .section-heading h2 {
    margin: 0.15rem 0 0;
    max-width: none;
    font-size: clamp(1.35rem, 6vw, 1.9rem);
    line-height: 1.05;
  }

  .graph-viewport > .section-heading .kicker {
    font-size: 0.58rem;
  }

  .graph-layout > .node-details {
    margin-top: 0.85rem;
  }
}

/* At desktop widths the new structural wrapper is layout-transparent. The
 * explicit columns preserve the existing full-width heading followed by the
 * renderer/details pair. */
@media (min-width: 980px) {
  .graph-layout .section-heading,
  .graph-layout .runtime-banner {
    grid-column: 1 / -1;
  }

  .graph-layout .graph-stage {
    grid-column: 1;
  }

  .graph-layout > .node-details {
    grid-column: 2;
  }
}
