/* vkstack — a cross-section of the vSphere stack.
   The page is deliberately quiet so that one thing can be loud: the lit load path
   through the layers. Everything else is hairlines and cool greys. */

:root {
  --surface: #eaeef3;
  --panel: #ffffff;
  --ink: #161c25;
  --muted: #67748a;
  --faint: #97a2b4;
  --hair: #ccd4e0;

  /* The only saturated colours on the page. Indigo marks the load path; green marks
     the single recommended stack. Nothing else is allowed to compete. */
  --signal: #3d5afe;
  --signal-soft: #e6eaff;
  --pick: #0b8a5a;
  --pick-soft: #e0f4ec;
  --warn: #b3400f;
  /* Support lifecycle. Amber for Technical Guidance, muted red for End of Support —
     both quieter than the indigo path so they qualify a node without competing with it. */
  --tg: #a3690a;
  --tg-soft: #fbf0dc;
  --eos: #9c2b30;
  --eos-soft: #f7e4e5;

  --radius: 7px;
  --mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
  --step: 180ms;
}

@media (prefers-color-scheme: dark) {
  :root {
    --surface: #10141b;
    --panel: #171d26;
    --ink: #e6eaf1;
    --muted: #8f9cb0;
    --faint: #5f6b7e;
    --hair: #2a3441;

    --signal: #7d92ff;
    --signal-soft: #1d2544;
    --pick: #3ecf96;
    --pick-soft: #102c22;
    --warn: #f0906a;
    --tg: #dda94a;
    --tg-soft: #33290f;
    --eos: #e8848a;
    --eos-soft: #331a1c;
  }
}

@media (prefers-reduced-motion: reduce) {
  :root { --step: 0ms; }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

/* ---- masthead ---------------------------------------------------------- */

.masthead {
  background: var(--panel);
  border-bottom: 1px solid var(--hair);
  padding: 0.9rem 1.5rem 0.7rem;
}

.masthead-row {
  display: flex;
  align-items: baseline;
  gap: 1.75rem;
  flex-wrap: wrap;
}

.masthead h1 {
  margin: 0;
  font-size: 0.95rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

#tabs { display: flex; gap: 1.1rem; }

#tabs button {
  background: none;
  border: 0;
  padding: 0.15rem 0;
  font: inherit;
  font-size: 0.88rem;
  color: var(--muted);
  cursor: pointer;
  border-bottom: 2px solid transparent;
}

#tabs button:hover { color: var(--ink); }
#tabs button.active { color: var(--ink); border-bottom-color: var(--signal); }

.meta {
  margin: 0.6rem 0 0;
  font-size: 0.76rem;
  color: var(--faint);
  font-family: var(--mono);
}

main { padding: 1.5rem; max-width: 1240px; }
.view.hidden { display: none; }

.lede { margin: 0; color: var(--muted); max-width: 60ch; }
h2 { font-size: 0.9rem; margin: 1.6rem 0 0.6rem; }

.stack-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 1.5rem;
  flex-wrap: wrap;
  margin-bottom: 1.3rem;
}

.stack-actions { display: flex; align-items: center; gap: 0.7rem; flex-wrap: wrap; }

/* The filter sits with the list it narrows, not in the header: it is about finding a
   version among dozens, not about what the map is showing. */
.section-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
}

.filter {
  font: inherit;
  font-size: 0.82rem;
  font-family: var(--mono);
  padding: 0.32rem 0.6rem;
  min-width: 14rem;
  border-radius: var(--radius);
  border: 1px solid var(--hair);
  background: var(--panel);
  color: var(--ink);
}

.filter:focus-visible { outline: 2px solid var(--signal); outline-offset: 1px; }

/* Whether the data is still moving, not just how old it is. A failing background refresh
   used to be visible only to whoever could read the logs on the box. */
.stale-flag,
.refresh-flag {
  font: inherit;
  font-size: 0.72rem;
  font-family: var(--mono);
  margin-left: 0.7rem;
  padding: 0 0.35rem;
  border-radius: 3px;
  border: 1px solid currentColor;
  background: none;
  cursor: pointer;
}

.stale-flag { color: var(--eos); background: var(--eos-soft); }
.refresh-flag { color: var(--faint); border-color: var(--hair); }

.refresh-log {
  margin: 0.7rem 0 0;
  padding: 0.7rem 0.9rem;
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: 0.76rem;
}

.refresh-log.hidden { display: none; }
.refresh-log h2 { margin: 0 0 0.4rem; font-size: 0.72rem; }
.refresh-log ul { margin: 0; padding-left: 1rem; font-family: var(--mono); }
.refresh-log li.failed { color: var(--eos); }
.refresh-log li.ok { color: var(--muted); }
.refresh-log .when { margin-right: 0.6rem; color: var(--faint); }
.refresh-log .note { margin: 0.5rem 0 0; color: var(--muted); font-family: inherit; }

.offscreen { position: fixed; left: -9999px; top: 0; opacity: 0; }

.pin-label {
  font-family: var(--mono);
  font-size: 0.82rem;
  color: var(--signal);
}

button.ghost {
  font: inherit;
  font-size: 0.82rem;
  padding: 0.3rem 0.7rem;
  border-radius: var(--radius);
  border: 1px solid var(--hair);
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
}

button.ghost:hover { color: var(--ink); border-color: var(--muted); }

/* ---- the strata -------------------------------------------------------- */

.strata {
  display: flex;
  flex-direction: column-reverse; /* bottom-up: vCenter is the foundation */
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--panel);
}

.layer {
  display: grid;
  grid-template-columns: 8.5rem 1fr;
  gap: 1rem;
  padding: 0.85rem 1rem;
  border-top: 1px solid var(--hair);
}

.strata > .layer:last-child { border-top: 0; } /* visually the bottom row */

.layer-spine { padding-top: 0.15rem; }

.layer-name {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The count is the point of the whole view: watch it narrow as you pin. */
.layer-count {
  font-family: var(--mono);
  font-size: 0.74rem;
  color: var(--faint);
  margin-top: 0.15rem;
}

.layer-count .narrowed { color: var(--signal); font-weight: 600; }

.layer-nodes { display: flex; flex-wrap: wrap; gap: 0.4rem; }

.node {
  font: inherit;
  font-family: var(--mono);
  font-size: 0.8rem;
  padding: 0.32rem 0.6rem;
  border-radius: var(--radius);
  border: 1px solid var(--hair);
  background: var(--panel);
  color: var(--ink);
  cursor: pointer;
  text-align: left;
  transition: opacity var(--step) ease, border-color var(--step) ease,
              background-color var(--step) ease, color var(--step) ease;
}

.node:hover { border-color: var(--muted); }

.node:focus-visible {
  outline: 2px solid var(--signal);
  outline-offset: 2px;
}

.node .detail {
  display: block;
  font-size: 0.68rem;
  color: var(--faint);
  margin-top: 0.1rem;
}

/* Off the path: present but receded, so the shape of the answer is unmistakable. */
.node.dim { opacity: 0.24; }
.node.dim:hover { opacity: 0.6; }

.node.lit {
  border-color: var(--signal);
  background: var(--signal-soft);
  color: var(--ink);
}

.node.pinned {
  border-color: var(--signal);
  border-width: 2px;
  background: var(--signal);
  color: #fff;
}

.node.pinned .detail { color: rgba(255, 255, 255, 0.78); }

.node.picked { border-color: var(--pick); background: var(--pick-soft); }

.node.nodata {
  border-style: dashed;
  color: var(--faint);
}

/* ---- recommended stack ------------------------------------------------- */

.recommended {
  margin-top: 1.1rem;
  border: 1px solid var(--hair);
  border-left: 3px solid var(--pick);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 0.9rem 1.1rem;
}

.recommended h2 { margin: 0 0 0.6rem; }

.recommended dl {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0.6rem 1.2rem;
  margin: 0;
}

.recommended dt {
  font-size: 0.68rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

.recommended dd { margin: 0.1rem 0 0; font-family: var(--mono); font-size: 0.85rem; }

/* The solver reaches past the legacy filter, so a recommendation can name a release the
   map is hiding. It is coloured like the node would have been, and says so in words. */
.recommended dd.phase-technical-guidance { color: var(--tg); }
.recommended dd.phase-end-of-support { color: var(--eos); }

.recommended .note {
  display: block;
  font-family: ui-sans-serif, system-ui, sans-serif;
  font-size: 0.72rem;
  font-style: italic;
  color: var(--faint);
}

.provenance-summary {
  margin: 0.75rem 0 0;
  padding-top: 0.6rem;
  border-top: 1px solid var(--hair);
  font-size: 0.76rem;
  color: var(--muted);
}

.recommended .footnote-line { margin: 0.35rem 0 0; color: var(--warn); font-style: normal; }

/* The decision has to leave the screen: into a change ticket, a maintenance plan, and the
   hands of whoever runs the upgrade weeks later. */
.manifest-actions {
  display: flex;
  gap: 0.5rem;
  flex-wrap: wrap;
  margin-top: 0.8rem;
  padding-top: 0.7rem;
  border-top: 1px solid var(--hair);
}

button.ghost.inline { margin-left: 0.6rem; }

/* Printing is the cheapest export there is: a change board wants a page, not a browser. */
@media print {
  .masthead nav, .stack-actions, .manifest-actions, .legend,
  #strata, #peek, .refresh-log, .section-head .filter { display: none !important; }
  .recommended { border: 1px solid #999; break-inside: avoid; }
  body { color: #000; background: #fff; }
  .map-svg { max-width: 100%; }
}

.footnote {
  margin-top: 1.1rem;
  font-size: 0.78rem;
  color: var(--faint);
  max-width: 78ch;
}

/* ---- peek popover ------------------------------------------------------ */

.peek {
  position: fixed;
  z-index: 20;
  max-width: 22rem;
  padding: 0.55rem 0.7rem;
  border-radius: var(--radius);
  border: 1px solid var(--hair);
  background: var(--panel);
  box-shadow: 0 10px 26px rgba(10, 16, 28, 0.16);
  font-family: var(--mono);
  font-size: 0.75rem;
  line-height: 1.45;
  pointer-events: none;
}

.peek .peek-title {
  font-family: inherit;
  font-weight: 700;
  margin-bottom: 0.25rem;
}

/* Shift-click keeps the popover open. Locked, it takes the pointer back so its contents
   can be read, selected and copied — the exact builds and the upstream caveats are worth
   more than a tooltip that dies when the mouse moves. */
.peek.is-locked {
  pointer-events: auto;
  user-select: text;
  max-height: 70vh;
  overflow-y: auto;
  border-color: var(--signal);
  box-shadow: 0 12px 30px rgba(10, 16, 28, 0.24);
}

.peek-hint {
  float: right;
  margin-left: 0.8rem;
  font-weight: 400;
  font-size: 0.66rem;
  color: var(--faint);
}

.peek-actions { margin-top: 0.5rem; }

/* What a node means, as opposed to which releases it covers. Never truncated: the release
   list is what gets cut when the popover runs out of room. */
.peek .peek-head {
  font-family: ui-sans-serif, system-ui, sans-serif;
  color: var(--muted);
  margin-bottom: 0.2rem;
}

.peek.hidden { display: none; }

/* ---- shared ------------------------------------------------------------ */

.diagram {
  background: var(--panel);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  padding: 1.2rem;
  overflow-x: auto;
}

.diagram svg { max-width: 100%; height: auto; }

.prose { max-width: 78ch; padding-left: 1.1rem; color: var(--ink); }
.prose li { margin-bottom: 0.5rem; }
.prose .unpublished { color: var(--warn); font-style: italic; }

.error {
  border-radius: var(--radius);
  padding: 0.7rem 0.9rem;
  margin-bottom: 1rem;
  font-size: 0.86rem;
  background: var(--panel);
  border: 1px solid var(--warn);
  color: var(--warn);
}

.hidden { display: none; }

@media (max-width: 640px) {
  .layer { grid-template-columns: 1fr; gap: 0.5rem; }
  main { padding: 1rem; }
}

/* ---- the map ----------------------------------------------------------- */

.map-frame {
  margin: 0 0 1.8rem;
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  background: var(--panel);
  padding: 0.5rem 0.5rem 0.75rem;
}

.map { overflow-x: auto; }

.map-svg { display: block; }

.map-empty {
  margin: 0;
  padding: 2.5rem 1rem;
  text-align: center;
  color: var(--faint);
  font-size: 0.88rem;
}

/* "Nothing selected yet" and "this selection has no stack" both used to draw an empty
   frame. The second is an answer and has to look like one. */
.map-empty.is-dead-end {
  color: var(--eos);
  background: var(--eos-soft);
  border-radius: var(--radius);
}

.map-caption {
  padding: 0.4rem 0.7rem 0;
  font-family: var(--mono);
  font-size: 0.72rem;
  color: var(--faint);
}

.map-layer-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  fill: var(--muted);
  font-family: ui-sans-serif, system-ui, sans-serif;
}

.map-link {
  fill: none;
  stroke: var(--signal);
  stroke-width: 1.4;
  opacity: 0.42;
}

/* A connection carries the matrix's own confidence. Solid is a tested yes; dashed is
   "compatible, not tested"; dotted is a pair upstream never evaluated, drawn because a
   stack exists rather than because anything was looked up. Drawing all three the same
   let the picture claim more than the data. */
.map-link.is-untested {
  stroke-dasharray: 6 3;
  opacity: 0.5;
}

.map-link.is-unverified {
  stroke-dasharray: 1.5 3.5;
  stroke: var(--muted);
  opacity: 0.62;
}

/* The map is reconciled rather than rebuilt, so these transitions finally have something
   to animate. Position lives on the group's transform: one animatable property instead of
   a coordinate rewrite on every child. */
.map-node {
  cursor: pointer;
  transition: transform var(--step) ease, opacity var(--step) ease;
}

/* Re-centring jitter of a node nobody touched snaps instead of sliding, so motion on the
   map always means something actually changed. */
.map-node.is-static { transition: opacity var(--step) ease; }

.map-node.is-entering,
.map-link.is-entering { opacity: 0; }

.map-node.is-leaving,
.map-link.is-leaving {
  opacity: 0;
  pointer-events: none;
}

.map-link { transition: opacity var(--step) ease, d var(--step) ease; }

/* A solve is in flight: keep the previous answer legible instead of blanking the frame,
   which reads as a broken tool on a slow connection. */
.map.is-solving .map-svg {
  opacity: 0.45;
  transition: opacity 120ms ease 90ms;
}

.map.is-solving { cursor: progress; }

@media (prefers-reduced-motion: reduce) {
  .map-node, .map-link, .map.is-solving .map-svg { transition: none; }
}

.map-node .map-node-box {
  fill: var(--signal-soft);
  stroke: var(--signal);
  stroke-width: 1.4;
  transition: fill var(--step) ease;
}

.map-node .map-node-text {
  font-family: var(--mono);
  font-size: 12.5px;
  fill: var(--ink);
}

.map-node:hover .map-node-box { fill: var(--panel); }

.map-node:focus-visible { outline: none; }
.map-node:focus-visible .map-node-box { stroke-width: 3; }

.map-node.is-pinned .map-node-box { fill: var(--signal); stroke: var(--signal); }
.map-node.is-pinned .map-node-text { fill: #fff; }

.map-node.is-picked .map-node-box { stroke: var(--pick); stroke-width: 2.2; fill: var(--pick-soft); }

.section-label {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.25rem;
}

.section-note {
  margin: 0 0 0.7rem;
  font-size: 0.8rem;
  color: var(--faint);
}

/* Supervisor ships two trains at the same Kubernetes version and they are not
   interchangeable, so the train is always visible on the node rather than buried. */
.node .train {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0 0.28rem;
  border-radius: 3px;
  border: 1px solid currentColor;
  font-size: 0.62rem;
  letter-spacing: 0.04em;
  vertical-align: 0.08em;
  opacity: 0.85;
}

.map-node-train {
  font-family: var(--mono);
  font-size: 9.5px;
  fill: var(--muted);
  letter-spacing: 0.04em;
}

.map-node.is-pinned .map-node-train { fill: rgba(255, 255, 255, 0.82); }

/* ---- the explainer ----------------------------------------------------- */

/* Every claim shows its provenance, so an asserted rule never reads like a lookup. */
.evidence {
  display: inline-block;
  margin: 0 0.45rem;
  padding: 0.05rem 0.4rem;
  border-radius: 3px;
  font-size: 0.68rem;
  letter-spacing: 0.02em;
  white-space: nowrap;
  border: 1px solid currentColor;
}

.evidence.known { color: var(--pick); }
.evidence.unknown { color: var(--warn); }

.callout {
  margin: 1rem 0;
  padding: 0.85rem 1.05rem;
  border: 1px solid var(--hair);
  border-left: 3px solid var(--signal);
  border-radius: var(--radius);
  background: var(--panel);
}

.callout h3 { margin: 0 0 0.35rem; font-size: 0.9rem; }
.callout p { margin: 0; color: var(--muted); font-size: 0.88rem; }
.callout code { font-family: var(--mono); color: var(--ink); }

.unknown {
  margin-bottom: 0.8rem;
  padding: 0.85rem 1.05rem;
  border: 1px solid var(--hair);
  border-left: 3px solid var(--warn);
  border-radius: var(--radius);
  background: var(--panel);
}

.unknown h3 { margin: 0 0 0.4rem; font-size: 0.9rem; }
.unknown p { margin: 0 0 0.4rem; font-size: 0.88rem; color: var(--muted); max-width: 76ch; }
.unknown p:last-child { margin-bottom: 0; }
.unknown strong { color: var(--ink); }


/* ---- support lifecycle ------------------------------------------------- */

/* The matrix publishes a support phase per release. "Legacy" here means exactly what it
   means on the interop site: nothing left in General Support. */

.node.phase-technical-guidance {
  border-color: var(--tg);
  background: var(--tg-soft);
}

.node.phase-end-of-support {
  border-color: var(--eos);
  background: var(--eos-soft);
  text-decoration: line-through;
  text-decoration-color: color-mix(in srgb, var(--eos) 55%, transparent);
}

.phase-tag {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.26rem;
  border-radius: 3px;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  vertical-align: 0.1em;
  text-decoration: none;
  color: #fff;
}

.phase-tag.phase-technical-guidance { background: var(--tg); }
.phase-tag.phase-end-of-support { background: var(--eos); }

/* A grouped line keeps its best phase as its headline — it is still usable — but when the
   builds behind it disagree, the worst one is named too. Outlined rather than filled, so
   it reads as a qualification of the node and not as the node's own phase. */
.phase-tag.is-mixed {
  background: transparent;
  border: 1px solid currentColor;
}

.phase-tag.phase-technical-guidance.is-mixed { color: var(--tg); }
.phase-tag.phase-end-of-support.is-mixed { color: var(--eos); }

.map-node.is-technical-guidance .map-node-box { stroke: var(--tg); fill: var(--tg-soft); }
.map-node.is-end-of-support .map-node-box { stroke: var(--eos); fill: var(--eos-soft); }

/* Provenance, on the node itself. "not tested" is upstream's own hedge on a yes;
   "inferred" means no upstream result exists for a pair the stack depends on. */
.prov-tag {
  display: inline-block;
  margin-left: 0.35rem;
  padding: 0 0.26rem;
  border-radius: 3px;
  border: 1px dashed currentColor;
  font-size: 0.6rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  vertical-align: 0.1em;
  text-decoration: none;
}

.prov-tag.is-untested { color: var(--tg); }
.prov-tag.is-unverified { color: var(--muted); }
.prov-tag.is-footnote { color: var(--warn); border-style: solid; }

.map-node.is-untested .map-node-box { stroke-dasharray: 6 3; }
.map-node.is-unverified .map-node-box { stroke-dasharray: 1.5 3.5; }

.hidden-count {
  align-self: center;
  font-size: 0.72rem;
  color: var(--faint);
  font-style: italic;
}

.toggle {
  font-size: 0.82rem;
  color: var(--muted);
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  cursor: pointer;
}

.legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.2rem;
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  font-size: 0.76rem;
  color: var(--muted);
}

.legend li { display: flex; align-items: center; gap: 0.4rem; }

.swatch {
  width: 0.85rem;
  height: 0.85rem;
  border-radius: 3px;
  border: 1px solid var(--hair);
  display: inline-block;
}

.sw-general { background: var(--signal-soft); border-color: var(--signal); }
.sw-tg { background: var(--tg-soft); border-color: var(--tg); }
.sw-eos { background: var(--eos-soft); border-color: var(--eos); }

/* Lifecycle is a fill; provenance is a stroke. Keeping the two channels apart is what
   lets a node say "supported" and "not tested" at the same time without ambiguity. */
.sw-mixed { background: transparent; border-color: var(--eos); border-style: dashed; }
.sw-untested { background: transparent; border-color: var(--tg); border-style: dashed; }
.sw-unverified { background: transparent; border-color: var(--muted); border-style: dotted; }
