/* The map view. Written against the tokens in tokens.css and nothing else, so the palette change that
   repaints the board repaints this too.

   THE SHEET'S ONE OPINION: the graph is DRAWN, the reasons are TEXT. The canvas carries geometry — lanes,
   boxes, edges, the four status colours — and every sentence an operator has to read (what holds this, what
   waits on it, where the last session stopped) lives in the detail panel beside it as ordinary HTML. Labels
   inside SVG cannot wrap, cannot be selected comfortably, and do not resize with the browser's own text
   settings; putting the prose there is how a diagram becomes a picture of text nobody can read. */

.map-wrap {
  display: grid;
  gap: var(--s-3);
  grid-template-columns: 1fr;
}
@media (min-width: 1024px) {
  /* The detail panel earns a column only where there is one to spare. Below that it stacks under the canvas,
     which is also the order a screen reader meets them in. */
  .map-wrap { grid-template-columns: minmax(0, 1fr) 340px; align-items: start; }
  .map-head { grid-column: 1 / -1; }
}
@media (min-width: 1600px) {
  /* The extra width goes mostly to the canvas, but the detail panel takes some of it too: at 340px a claim
     wraps every five or six words, and this panel is where the prose lives. */
  .map-wrap { grid-template-columns: minmax(0, 1fr) 420px; }
}

/* FULL BLEED, and it is the one panel that gets it. `main` centres its content inside `--measure` (1080px),
   which is a MEASURE — a line length chosen so prose stays readable — and a dependency graph is not prose.
   Capping it there wastes every pixel past 1080 on a desktop and forces a pan across work that would have
   fitted. So this section escapes the cap and keeps only the page gutter.

   `50% - 50vw` is the standard escape and needs no knowledge of the parent's padding, which is computed from
   `100% - var(--measure)` and cannot be re-derived correctly from inside a child (its `100%` resolves against
   a different box). The companion `overflow-x: clip` on the page is not optional: `100vw` INCLUDES the
   vertical scrollbar, so without it this section overflows by exactly the scrollbar's width and the whole
   page scrolls sideways. `clip` rather than `hidden` because `hidden` would make the page a scroll container
   and break the sticky header and footer. */
#mapSection {
  width: calc(100vw - 2 * var(--gutter));
  max-width: none;
  margin-inline: calc(50% - 50vw + var(--gutter));
}

.map-head { display: flex; flex-wrap: wrap; gap: var(--s-2) var(--s-4); align-items: center; }
.map-counts { display: flex; flex-wrap: wrap; gap: var(--s-3); }
.map-stat { color: var(--muted); font-size: var(--t-meta); }
.map-stat b { color: var(--fg); font-size: var(--t-body); font-variant-numeric: tabular-nums; }
.map-stat.is-good b { color: var(--ready); }

.map-notes {
  flex-basis: 100%;
  margin: 0; padding-left: 1.1em;
  color: var(--muted); font-size: var(--t-meta);
}
.map-notes li { margin: 2px 0; }

.map-lenses { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-left: auto; }
/* The camera's controls sit apart from the lenses, on their own line at narrow widths: a lens changes WHAT is
   drawn and these change where it is seen from, and putting them in one run of identical chips invites the
   reading that "Flat" is another filter. */
.map-camera {
  display: flex; flex-wrap: wrap; gap: var(--s-2);
  padding-left: var(--s-3);
  border-left: 1px solid var(--line);
}
.map-camera .chip[data-testid^="map-zoom"] {
  min-width: 2.1em;
  font-variant-numeric: tabular-nums;
}

.map-canvas {
  width: 100%;
  height: min(82vh, 1040px);
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--lift);
  touch-action: none;              /* the canvas owns the gesture: a two-finger scroll here is a pan, not a page scroll */
  cursor: grab;
}
.map-canvas.is-panning { cursor: grabbing; }
/* Turning the scene is a different act from sliding it, and the pointer says which one is in progress —
   otherwise a drag that rotates when the operator expected a pan reads as the graph having jumped. */
.map-canvas.is-turning { cursor: move; }

/* Distance haze lives on this wrapper and the lens/selection fades live on `.m-node` inside it. Two nested
   groups because SVG MULTIPLIES nested opacities: a dimmed node that is also far ends up at the product of
   both, which is the composition the eye expects, and neither value has to know about the other. */
.m-depth { transition: none; }

.map-empty {
  padding: var(--s-5);
  color: var(--muted);
  background: var(--panel);
  border: 1px dashed var(--line);
  border-radius: var(--radius);
}

/* ---- lanes / planes ------------------------------------------------------------------------------------
   One per epic. Flat on the sheet they are bands; in depth they are quadrilaterals seen at an angle, which is
   why the element is a polygon and not a rect. The outline is what the depth arrangement needs and the flat one
   did not: several translucent fills stacked in a projection blend into one wash, and the edge of a plane is
   the thing that says where one feature ends. */
.m-lane-bg {
  fill: var(--line-soft);
  fill-opacity: .34;
  stroke: var(--line);
  stroke-width: 1;
}
.m-lane-label {
  fill: var(--muted);
  font-size: var(--t-micro);
  letter-spacing: .06em;
  text-transform: uppercase;
  /* The halo. An epic's name identifies a whole band and was the label most reliably covered by the issue
     boxes; it is now drawn in a layer above them, and this keeps it legible where it crosses one. `paint-order`
     puts the stroke UNDER the glyphs, so the stroke reads as a background rather than as an outline. */
  paint-order: stroke;
  stroke: var(--panel);
  stroke-width: 4px;
  stroke-linejoin: round;
  pointer-events: none;
}
/* An epic the operator unticked, on screen only because the selection still waits on it. */
.m-lane-label.is-held { fill: var(--limbo); }
.m-lane.is-held .m-lane-bg { fill: var(--limbo-soft); stroke: var(--limbo); stroke-dasharray: 6 4; }
.m-node.is-held { opacity: .55; }
.m-node.is-held .m-box { stroke-dasharray: 5 3; }

/* ---- edges ------------------------------------------------------------------------------------------- */
.m-edge { fill: none; stroke-width: 1.6; }
/* Binding — the blocker is still open. This is the only edge that means "you cannot start". */
.m-edge.is-binding { stroke: var(--block); opacity: .8; }
/* Satisfied — kept on screen because the sequencing is a record, drawn faint because it holds nothing. */
.m-edge.is-done { stroke: var(--faint); opacity: .38; stroke-dasharray: 3 4; }
.m-edge.is-dim { opacity: .07; }
.m-arrow { fill: var(--block); }
.m-arrow-done { fill: var(--faint); }

/* ---- nodes ------------------------------------------------------------------------------------------- */
.m-node { cursor: pointer; }
.m-box { fill: var(--panel); stroke: var(--line); stroke-width: 1; }
.m-stripe { fill: var(--faint); }
.m-ref { fill: var(--fg); font-size: 12px; font-weight: 600; }
.m-title { fill: var(--muted); font-size: 11.5px; }
.m-count { fill: var(--block); font-size: 11px; font-weight: 600; }
.m-live { fill: var(--live); }

.m-ready .m-stripe   { fill: var(--ready); }
.m-run .m-stripe     { fill: var(--run); }
.m-todo .m-stripe    { fill: var(--accent); }
.m-blocked .m-stripe { fill: var(--block); }

/* Startable now — nothing open is in front of it. The ring is the view's whole answer to "what can I pick
   up", so it is a ring and not another tint: it must be findable at a glance from across the lane strip. */
.m-node.is-startable .m-box { stroke: var(--ready); stroke-width: 2; }
.m-node.is-focus .m-box { stroke: var(--accent); stroke-width: 2.5; }
.m-node.is-cycle .m-box { stroke: var(--limbo); stroke-dasharray: 5 3; stroke-width: 2; }

/* A ghost is an edge's other end that the board does not carry — closed, or in a repository that did not
   answer. Muted rather than absent: dropping it would draw sequenced work as free-standing. */
.m-node.is-ghost .m-box { fill: transparent; stroke-dasharray: 4 4; }
.m-node.is-ghost .m-ref { fill: var(--faint); }

.m-node.is-dim { opacity: .16; }
/* Outside the current lens: still drawn, because a chain with holes in it is not the chain. Faded only. */
.m-node.is-outside { opacity: .34; }
.m-node:focus-visible .m-box { stroke: var(--accent); stroke-width: 2.5; }
.m-node:focus { outline: none; }

/* ---- detail ------------------------------------------------------------------------------------------ */
.map-detail {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--card-pad);
  box-shadow: var(--lift);
  max-height: min(82vh, 1040px);
  overflow: auto;
}
.map-detail-head { display: flex; align-items: center; gap: var(--s-2); }
.map-detail-ref { font-size: var(--t-meta); color: var(--muted); font-weight: 600; }
.map-detail-head .chip { margin-left: auto; }
.map-detail-title { display: block; margin: var(--s-2) 0; color: var(--fg); font-weight: 600; text-decoration: none; }
a.map-detail-title:hover { text-decoration: underline; }
.map-detail-badges { display: flex; flex-wrap: wrap; gap: var(--s-1); margin-bottom: var(--s-3); }
.map-detail-summary { font-size: var(--t-meta); color: var(--fg); }
.map-detail-list { margin-top: var(--s-3); }
.map-detail h4 {
  margin: 0 0 var(--s-1);
  font-size: var(--t-micro); letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}
.map-detail .muted { color: var(--muted); font-size: var(--t-meta); margin: 0; }
.map-warn {
  font-size: var(--t-meta); color: var(--limbo);
  background: var(--limbo-soft); border-radius: var(--radius-xs); padding: var(--s-2);
}
.map-ref {
  display: block; width: 100%; text-align: left;
  background: none; border: 0; border-radius: var(--radius-xs);
  padding: 3px var(--s-1); margin: 1px 0;
  color: var(--muted); font: inherit; font-size: var(--t-meta); cursor: pointer;
}
.map-ref:hover { background: var(--accent-soft); color: var(--fg); }
.map-ref b { color: var(--fg); }
.map-ref.is-ghost b { color: var(--faint); font-weight: 500; }
.map-detail-pr { font-size: var(--t-meta); margin-top: var(--s-1); }
.map-detail-pr a { color: var(--accent); }

/* ---- chantier notes ------------------------------------------------------------------------------------
   The reasoning behind an issue, beside the blockers in front of it. Bordered per note rather than separated
   by rules: several of these are long, and a rule between two paragraphs of prose does not say where one
   claim ends. */
.map-note {
  border: 1px solid var(--line);
  border-left-width: 3px;
  border-radius: var(--radius-xs);
  padding: var(--s-2);
  margin: var(--s-2) 0;
}
/* Closed — superseded or refuted. Kept on screen because a rejected option is the most expensive thing in the
   store to rediscover, and faded because it is not the current answer. */
.map-note.is-closed { opacity: .6; border-left-color: var(--line); }
.map-note-head { display: flex; align-items: center; gap: var(--s-1); }
.map-note-kind {
  font-size: var(--t-micro); letter-spacing: .05em; text-transform: uppercase; font-weight: 600;
  padding: 1px 6px; border-radius: var(--radius-pill); background: var(--line-soft); color: var(--muted);
}
/* The four kinds an operator returning after weeks is looking for get a colour; the rest stay neutral, so the
   colour means "this needs you" rather than "this is a node". */
.map-note-kind.k-question  { background: var(--accent-soft); color: var(--accent); }
.map-note-kind.k-risk      { background: var(--block-soft); color: var(--block); }
.map-note-kind.k-decision  { background: var(--ready-soft); color: var(--ready); }
.map-note-kind.k-rejection { background: var(--limbo-soft); color: var(--limbo); }
.map-note-closed { font-size: var(--t-micro); color: var(--faint); text-transform: uppercase; }
.map-note-date { margin-left: auto; font-size: var(--t-micro); color: var(--faint); }
.map-note-claim { margin: var(--s-1) 0 0; font-size: var(--t-meta); color: var(--fg); }
.map-note-body { margin: var(--s-1) 0 0; font-size: var(--t-meta); color: var(--muted); white-space: pre-wrap; }
/* The command that re-checks the claim. Selectable and wrapping — it exists to be copied, and a line that
   scrolls sideways inside a 340px panel cannot be. */
.map-note-cmd {
  margin: var(--s-2) 0 0;
  padding: var(--s-2);
  border-radius: var(--radius-xs);
  background: var(--bg);
  border: 1px solid var(--line-soft);
  font: 11.5px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--fg);
  white-space: pre-wrap;
  word-break: break-word;
  user-select: all;
}


/* ---- the right-hand column ------------------------------------------------------------------------------
   Two cards in one grid cell: which bands are on the canvas, then the record of the selected node. One
   element, because `.map-wrap` places its children in order and a fourth child of the host would land back
   under the canvas. */
.map-side { display: grid; gap: var(--s-3); align-content: start; min-width: 0; }

.map-epics {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: var(--card-pad);
  box-shadow: var(--lift);
  max-height: min(36vh, 420px);
  overflow: auto;
}
.map-epics-head { display: flex; align-items: center; gap: var(--s-1); margin-bottom: var(--s-2); }
.map-epics-head h4 {
  margin: 0 auto 0 0;
  font-size: var(--t-micro); letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}

/* The repository a run of epic rows belongs to. STICKY, because this list scrolls inside its own card: an
   operator dragged three projects down it would otherwise be reading a column of `#327`-style references with
   nothing on screen saying which estate they are in — which is the exact confusion the grouping was asked to
   fix.

   TWO CONTROLS, and they are siblings rather than one nested in the other: a checkbox inside a button is not a
   thing a browser can honour, and the two mean different things anyway — the tick decides what the CANVAS
   draws, the fold decides how much of this list is on screen. */
.map-epic-repo {
  position: sticky;
  top: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--s-2);
  margin: var(--s-2) 0 2px;
  padding: 4px var(--s-1);
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  font-size: var(--t-micro); letter-spacing: .06em; text-transform: uppercase; color: var(--faint);
}
.map-epic-repo:first-of-type { margin-top: 0; }

/* The disclosure. Transparent and borderless — it is a heading the operator can press, not a button parked in
   a heading — but it keeps a real focus ring, because it is the keyboard's only way to fold a group. */
.map-epic-repo-fold {
  display: flex; align-items: center; gap: var(--s-2);
  width: 100%;
  padding: 0;
  background: none; border: 0;
  font: inherit; letter-spacing: inherit; text-transform: inherit; color: inherit;
  /* A button centres its text by default, and this one is a heading: without this the repository's name floats
     in the middle of the row while every epic reference under it is flush left. */
  text-align: left;
  cursor: pointer;
}
.map-epic-repo-fold:hover .map-epic-repo-name { color: var(--fg); }
.map-epic-repo-name { margin-right: auto; color: var(--muted); letter-spacing: .04em; }
/* How many of this project's bands are OFF, said on the header that hid the rows which would have said it. */
.map-epic-repo-off { color: var(--limbo); letter-spacing: .04em; }
/* The same glyph as the board's group headers, turning the same way — the two lists are read on one page, and a
   disclosure that pointed the other way would read as a different control. Spelled here rather than shared with
   `.repo .caret`: that selector is the BOARD's row, and widening it to match both would put one rule in charge
   of two components that have no reason to change together. */
.map-epic-repo .caret {
  width: 0; height: 0; flex: none;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent; border-bottom: 4px solid transparent;
  transform: rotate(90deg);
  transition: transform var(--dur-1) var(--ease);
}
.map-epic-repo.is-collapsed .caret { transform: none; }
@media (prefers-reduced-motion: reduce) { .map-epic-repo .caret { transition: none; } }

.map-epic {
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--s-1) var(--s-2);
  padding: 4px var(--s-1);
  border-radius: var(--radius-xs);
  font-size: var(--t-meta);
  cursor: pointer;
}
.map-epic:hover { background: var(--accent-soft); }
.map-epic b { color: var(--fg); }
.map-epic-count { color: var(--faint); font-variant-numeric: tabular-nums; }
/* The title and the held note both take the full width under the reference: at this column width a title
   sharing the row with a checkbox and a count wraps every three words. */
.map-epic-title { grid-column: 2 / -1; color: var(--muted); }
.map-epic-held { grid-column: 2 / -1; color: var(--limbo); }
/* Unticked rows stay legible — the list is how the operator gets an epic BACK, so it is not a place to hide
   what is currently off. */
.map-epic[data-on="false"] b { color: var(--muted); font-weight: 500; }
