/* Graph page: full-viewport canvas with a fixed-width detail panel. */

.body--graph { overflow: hidden; }

.graph-page {
  height: calc(100vh - var(--topbar-height));
  /* dvh tracks the mobile browser's collapsing address bar; vh does not, and
     the difference is a strip of graph hidden under the toolbar on a phone. */
  height: calc(100dvh - var(--topbar-height));
  display: grid;
  grid-template-rows: auto 1fr auto;
}

/* Fullscreen: everything that is not the graph steps out of the way. Driven by
   a class rather than the Fullscreen API because iOS Safari only grants real
   fullscreen to <video>, which would make the feature fail on exactly the
   devices that need it most. */
.body--graph.is-fullscreen .topbar,
.body--graph.is-fullscreen .demo-banner,
.body--graph.is-fullscreen .graph-toolbar,
.body--graph.is-fullscreen .graph-legend {
  display: none;
}

.body--graph.is-fullscreen .graph-page {
  height: 100vh;
  height: 100dvh;
  /* The hidden toolbar and legend stop being grid items altogether, so without
     this the canvas slides up into the `auto` row the toolbar was using and is
     sized by its own content rather than by the screen. */
  grid-template-rows: 1fr;
}

/* ------------------------------------------------------------- toolbar */

.graph-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 0.75rem 1.25rem;
  border-bottom: 1px solid var(--border);
  background: var(--bg-elevated);
  flex-wrap: wrap;
}

.graph-toolbar__identity {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  min-width: 0;
}

.graph-toolbar__art {
  width: 46px;
  height: 46px;
  border-radius: var(--radius);
  object-fit: cover;
}

.graph-toolbar__title {
  font-size: 1.05rem;
  margin: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 34ch;
}

.graph-toolbar__stats {
  margin: 0;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.graph-toolbar__controls {
  display: flex;
  align-items: center;
  gap: 0.85rem;
  flex-wrap: wrap;
}

/* -------------------------------------------------------------- canvas */

.graph-body {
  display: grid;
  grid-template-columns: 1fr 380px;
  min-height: 0; /* lets the SVG shrink instead of overflowing the grid row */
  position: relative; /* anchors the mobile detail sheet */
}

/* Dismissing the panel hands its column to the canvas rather than leaving a
   380px gap where it used to be. */
.graph-body:has(> .detail-panel.is-closed) { grid-template-columns: 1fr; }

.graph-canvas {
  position: relative;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 40%, #1a1a1a 0%, var(--bg) 70%);
}

.graph-canvas svg { display: block; width: 100%; height: 100%; }

.graph-canvas .loader {
  position: absolute;
  inset: 0;
  place-content: center;
  background: var(--bg);
  z-index: 5;
}

.graph-canvas .loader[hidden] { display: none; }

/* Floating canvas control. Lives inside .graph-canvas so it survives the
   fullscreen class hiding the toolbar -- otherwise the only way out of
   fullscreen would be the Escape key, which a phone does not have. */
.graph-canvas__control {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  z-index: 4;
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.45rem 0.7rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: rgba(24, 24, 24, 0.86);
  backdrop-filter: blur(6px);
  color: var(--text-muted);
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.graph-canvas__control:hover { color: var(--text); border-color: var(--text-muted); }

/* Node / link styling. Transitions are on opacity only -- animating
   stroke-width during a force tick is expensive at a few hundred edges. */

.link {
  stroke: #4a4a4a;
  stroke-opacity: 0.55;
  cursor: pointer;
  transition: stroke-opacity 0.15s, stroke 0.15s;
}

.link:hover { stroke: var(--accent); stroke-opacity: 1; }
.link.is-selected { stroke: var(--accent); stroke-opacity: 1; }
.link.is-dimmed { stroke-opacity: 0.06; }

.node { cursor: pointer; }

/* The artist's picture arrives square and is cropped to the node's circle
   here rather than through an SVG <clipPath>, which would need one element and
   one id per node. The reference box is the image's own bounding box. */
.node__face { clip-path: circle(50%); }

.node__ring {
  fill: none;
  stroke: transparent;
  stroke-width: 2.5;
}

.node.is-primary .node__ring { stroke: var(--accent); }
.node.is-selected .node__ring { stroke: #fff; stroke-width: 3; }
.node.is-dimmed { opacity: 0.15; }
.node.is-match .node__ring { stroke: #ffd166; stroke-width: 3; }

.node__label {
  fill: var(--text);
  font-size: 10px;
  font-weight: 600;
  text-anchor: middle;
  pointer-events: none;
  paint-order: stroke;
  stroke: rgba(0, 0, 0, 0.85);
  stroke-width: 3px;
  stroke-linejoin: round;
}

.node.is-dimmed .node__label { opacity: 0; }

/* --------------------------------------------------------- detail panel */

.detail-panel {
  position: relative;
  border-left: 1px solid var(--border);
  background: var(--bg-elevated);
  overflow-y: auto;
  padding: 1.25rem;
}

.detail-panel.is-closed { display: none; }

/* Built by DetailPanel rather than written into the three page shells, so the
   button cannot be lost when the panel replaces its contents on every click. */
.detail-panel__close {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  z-index: 2;
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  border: 1px solid var(--border);
  border-radius: 50%;
  background: var(--bg);
  color: var(--text-muted);
  font-size: 1rem;
  line-height: 1;
  cursor: pointer;
}

.detail-panel__close:hover { color: var(--text); border-color: var(--text-muted); }

.detail-panel__empty {
  color: var(--text-muted);
  font-size: 0.9rem;
}

.detail-panel__empty h2 {
  font-size: 1rem;
  color: var(--text);
  margin: 0 0 0.75rem;
  padding-right: 2rem; /* clears the close button */
}

.detail-head {
  display: flex;
  gap: 0.85rem;
  align-items: center;
  padding-bottom: 1rem;
  margin-bottom: 1rem;
  padding-right: 2rem; /* clears the close button */
  border-bottom: 1px solid var(--border);
}

.detail-head__avatar {
  width: 56px;
  height: 56px;
  border-radius: 50%;
  object-fit: cover;
  flex-shrink: 0;
}

/* Two overlapping avatars for a link selection (artist A + artist B). */
.detail-head__pair { display: flex; flex-shrink: 0; }
.detail-head__pair img {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  object-fit: cover;
  border: 2px solid var(--bg-elevated);
}
.detail-head__pair img:last-child { margin-left: -16px; }

.detail-head__title { font-size: 1.05rem; margin: 0 0 0.2rem; }
.detail-head__meta { font-size: 0.78rem; color: var(--text-muted); margin: 0; }

.detail-head__genres {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  margin-top: 0.4rem;
}

.genre-chip {
  font-size: 0.68rem;
  background: var(--bg-hover);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 0.1rem 0.5rem;
  color: var(--text-muted);
}

/* A count, not a label: the dashed edge stops "+30" from reading as a genre
   actually named "+30" sitting alongside the real ones. */
.genre-chip--more {
  background: none;
  border-style: dashed;
}

.detail-section__title {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--text-muted);
  margin: 0 0 0.6rem;
}

.track-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

/* Art, text, duration, play, queue. The two button columns are a fixed width
   rather than auto so the durations in a list still line up under each other. */
.track-row {
  display: grid;
  grid-template-columns: 36px 1fr auto 28px 28px;
  gap: 0.7rem;
  align-items: center;
  padding: 0.45rem;
  border-radius: 6px;
  transition: background 0.12s;
}

.track-row:hover { background: var(--bg-hover); }
.track-row.is-playing { background: rgba(29, 185, 84, 0.14); }

.track-row__art {
  width: 36px;
  height: 36px;
  border-radius: 4px;
  object-fit: cover;
  background: var(--bg-hover);
}

.track-row__text { min-width: 0; }

.track-row__name {
  font-size: 0.86rem;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.track-row__sub {
  font-size: 0.73rem;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.track-row__duration {
  font-size: 0.75rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Dim at rest so a long list reads as songs rather than as a column of green
   buttons, full strength once the row is under the pointer. They keep their box
   either way -- fading is what stops the row reflowing on hover. */
.track-row__play,
.track-row__queue {
  display: grid;
  place-items: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0.45;
  transition: opacity 0.12s, background 0.12s, color 0.12s;
}

.track-row__play svg,
.track-row__queue svg { width: 16px; height: 16px; fill: currentColor; }

.track-row:hover .track-row__play,
.track-row:hover .track-row__queue,
.track-row__play:focus-visible,
.track-row__queue:focus-visible { opacity: 1; }

/* Only play takes the filled accent. Queue stays an outline so the pair reads
   as one primary action and one secondary, rather than two equal green pills. */
.track-row:hover .track-row__play,
.track-row__play:focus-visible {
  background: var(--accent);
  color: #05240f;
}

.track-row:hover .track-row__queue:hover,
.track-row__queue:focus-visible {
  background: var(--bg-hover);
  color: var(--text);
}

.track-row.is-playing .track-row__play { opacity: 1; color: var(--accent); }
.track-row.is-playing:hover .track-row__play { color: #05240f; }

/* Confirmation that the track went somewhere. Queueing is otherwise invisible:
   nothing starts, and Spotify itself gives no feedback. */
.track-row__queue.is-queued { opacity: 1; color: var(--accent); }

.detail-link {
  display: inline-block;
  margin-top: 1rem;
  font-size: 0.8rem;
  color: var(--accent);
}

/* A row in the track list, so it lands directly under the song it explains. */
.detail-toast {
  margin: 0.15rem 0 0.4rem;
  padding: 0 0.45rem 0 calc(36px + 0.7rem + 0.45rem);
  font-size: 0.78rem;
  color: #ffb3ac;
}

.detail-toast .detail-link { margin-top: 0.15rem; }

/* Confirmation, not a warning: the accent rather than the error red, so the two
   are told apart by colour before either is read. */
.detail-toast--ok {
  color: var(--accent);
  opacity: 1;
  transition: opacity 0.45s ease;
}

.detail-toast--ok.is-leaving { opacity: 0; }

/* -------------------------------------------------------------- legend */

/* A <details> so the collapse is the browser's, not ours. On desktop the
   summary is hidden and the items always show; on a phone it becomes the
   tappable row that reveals them. */
.graph-legend {
  padding: 0.55rem 1.25rem;
  border-top: 1px solid var(--border);
  background: var(--bg-elevated);
  font-size: 0.75rem;
  color: var(--text-muted);
}

.graph-legend__summary {
  display: none;
  cursor: pointer;
  list-style: none;
  align-items: center;
  gap: 0.4rem;
  font-weight: 600;
  color: var(--text-muted);
}

.graph-legend__summary::-webkit-details-marker { display: none; }

.graph-legend__items {
  display: flex;
  align-items: center;
  gap: 1.5rem;
  flex-wrap: wrap;
}

.graph-legend span { display: inline-flex; align-items: center; gap: 0.4rem; }
.graph-legend__hint { margin-left: auto; opacity: 0.7; }

.legend-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: #555;
  display: inline-block;
}

.legend-dot--primary { background: transparent; border: 2px solid var(--accent); }

.legend-line {
  width: 18px;
  height: 0;
  border-top: 2px solid #4a4a4a;
  display: inline-block;
}

/* -------------------------------------------------------------- narrow */

/* Tablet: the panel drops under the canvas but still takes layout space. */
@media (max-width: 960px) {
  .graph-body { grid-template-columns: 1fr; grid-template-rows: 1fr auto; }
  .detail-panel {
    border-left: 0;
    border-top: 1px solid var(--border);
    max-height: 45vh;
  }
  .graph-body:has(> .detail-panel.is-closed) { grid-template-rows: 1fr; }
}

/* Phone: the panel stops being a layout row and becomes a sheet floating over
   the canvas. Sharing the height was the real problem -- a 45vh panel plus a
   two-row toolbar plus the legend left the graph about a third of the screen,
   which is why the nodes read as tiny. The canvas now owns the whole area and
   the sheet covers part of it only while something is selected. */
@media (max-width: 700px) {
  .graph-toolbar {
    gap: 0.6rem;
    padding: 0.5rem 0.9rem;
  }

  .graph-toolbar__title { font-size: 0.95rem; max-width: 100%; }
  .graph-toolbar__art { width: 34px; height: 34px; }

  /* "Hide unconnected" is a power-user toggle and does not earn a row on a
     390px screen. Search does, so it stays and takes the width it gave up. */
  .graph-toolbar__controls .toggle { display: none; }

  .graph-toolbar__controls {
    flex: 1 1 100%;
    gap: 0.5rem;
  }

  .graph-toolbar__controls .search { flex: 1 1 auto; }
  .graph-toolbar__controls .search input { width: 100%; }

  .graph-body { grid-template-rows: 1fr; }

  .detail-panel {
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 6;
    max-height: 55%;
    border-top: 1px solid var(--border);
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
    padding: 1rem 1rem 1.25rem;
    box-shadow: 0 -12px 28px rgba(0, 0, 0, 0.45);
    animation: detail-sheet-in 0.18s ease-out;
  }

  /* The sheet is a layer over the canvas now, so the canvas must not resize
     when it opens or closes -- otherwise every selection restarts the force
     simulation from a new centre. */
  .graph-body:has(> .detail-panel.is-closed) { grid-template-rows: 1fr; }

  .detail-panel__close { width: 34px; height: 34px; }

  /* There is no hover on a touch screen, so the buttons have to be legible at
     rest. Widening the columns to 40px is what gets the tap targets close to the
     44px guideline without making the row taller than the artwork in it. The gap
     between them is load-bearing too: two 40px targets flush together is how you
     queue a song you meant to play. */
  .track-row {
    grid-template-columns: 36px 1fr auto 40px 40px;
    gap: 0.5rem;
  }
  .track-row__play,
  .track-row__queue {
    width: 40px;
    height: 40px;
    opacity: 1;
    background: var(--bg-hover);
    color: var(--text);
  }
  .track-row__play:active { background: var(--accent); color: #05240f; }
  .track-row__queue:active { background: var(--bg-hover); color: var(--accent); }

  .graph-legend { padding: 0.5rem 0.9rem; }
  .graph-legend__summary { display: flex; }
  .graph-legend__summary::after {
    content: "▾";
    font-size: 0.7rem;
    transition: transform 0.15s;
  }
  .graph-legend[open] .graph-legend__summary::after { transform: rotate(180deg); }

  .graph-legend__items {
    gap: 0.5rem 1.1rem;
    padding-top: 0.55rem;
  }

  .graph-legend__hint { margin-left: 0; }
}

@keyframes detail-sheet-in {
  from { transform: translateY(12px); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

@media (prefers-reduced-motion: reduce) {
  .detail-panel { animation: none; }
  /* The confirmation still disappears, it just stops fading -- _showToast falls
     back to removing it outright when the transition is off. */
  .detail-toast--ok { transition: none; }
}
