/* manifold - visual pass: the prototype's tokens + card surface, dark-mode
   aware. markup carries two hooks (.card, .cockpit); the rest is element- and
   structure-scoped, to be promoted to classes when the cockpit markup churns. */

:root {
  --bg-page: #faf9f7;
  --bg-primary: #ffffff;
  --bg-secondary: #f3f2ef;
  --text-primary: #1b1a18;
  --text-secondary: #6b6a65;
  --text-tertiary: #918f89;
  --border-tertiary: rgba(0, 0, 0, 0.10);
  --border-secondary: rgba(0, 0, 0, 0.18);
  --text-success: #2f7d4f;
  --text-danger: #b3261e;
  --text-accent: #9a6515;
  --accent: #185fa5;
  --accent-surface: #e7f0fa;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
  --radius-md: 8px;
  --radius-lg: 12px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg-page: #161513;
    --bg-primary: #201f1d;
    --bg-secondary: #2a2926;
    --text-primary: #f0efec;
    --text-secondary: #a7a6a0;
    --text-tertiary: #7f7d77;
    --border-tertiary: rgba(255, 255, 255, 0.12);
    --border-secondary: rgba(255, 255, 255, 0.22);
    --text-success: #6fce96;
    --text-danger: #e9938c;
    --text-accent: #d8a24a;
    --accent: #6aa8e8;
    --accent-surface: #22405e;
  }
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

/* the whole app scales with the window; the side margins are the only cap */
body {
  font-family: var(--font-sans);
  line-height: 1.5;
  color: var(--text-primary);
  background: var(--bg-page);
  padding: 1.5rem 3rem 5rem;
  -webkit-font-smoothing: antialiased;
}

h1 {
  margin: 0 0 0.25rem;
  font-size: 1.6rem;
}

/* an act of the page: a quiet structural heading, read through rather than at */
.act {
  margin: 1.5rem 0 0.25rem;
  color: var(--text-tertiary);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* the input deck: what you have and what you want, side by side as the peers
   they are - early game you fill the left, late game the right. narrow screens
   stack, have first (the North Star's own reading order). */
.deck {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 1.5rem;
  align-items: start;
}

.deck > div {
  min-width: 0;
}

@media (max-width: 950px) {
  .deck {
    grid-template-columns: 1fr;
  }
}

/* the fork above the deck: the two ways in, named as questions */
.fork {
  margin: 1.25rem 0 0.25rem;
  color: var(--text-secondary);
}

.fork strong {
  color: var(--text-primary);
}

/* the world card: blurb beside the picker as a door, facts beside the way out
   once a world is loaded */
.world-card {
  display: flex;
  align-items: flex-start;
  gap: 1rem 2rem;
  flex-wrap: wrap;
}

.world-card h2 {
  margin-bottom: 0.25rem;
}

.world-blurb {
  flex: 1 1 24rem;
}

.world-blurb p {
  margin: 0;
  color: var(--text-secondary);
}

.world-side {
  flex: 0 1 auto;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

/* the quiet annotation beside a disclosure's title */
.summary-note {
  color: var(--text-secondary);
  font-weight: 400;
  font-size: 0.9rem;
}

/* the catalog census, demoted from the masthead */
.footnote {
  color: var(--text-tertiary);
  font-size: 0.85rem;
}

h2 {
  margin: 0 0 0.75rem;
  font-size: 1.15rem;
}

h3 {
  margin: 1.25rem 0 0.5rem;
  font-size: 0.95rem;
  color: var(--text-secondary);
}

/* a binding raw's name over its own "to make more" rows */
h4 {
  margin: 0.75rem 0 0.25rem;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--text-secondary);
}

/* the card surface */
.card {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 1rem 1.25rem;
  margin: 1rem 0;
}

/* lists inside cards: no browser bullets */
.card ul {
  list-style: none;
  padding-left: 0;
  margin: 0.5rem 0;
}

.card li {
  padding: 0.1rem 0;
}

/* node-editor rows: controls on one line, wrapping when the deck halves the
   width rather than overflowing the card */
ul:has(select) li {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.375rem;
}

/* form controls */
select,
input,
button,
textarea {
  font: inherit;
  color: var(--text-primary);
  padding: 0.25rem 0.5rem;
}

select,
input,
textarea {
  background: var(--bg-primary);
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
}

button {
  cursor: pointer;
  background: transparent;
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-md);
  padding: 0.375rem 0.75rem;
}

button:hover {
  background: var(--bg-secondary);
}

input[type="number"] {
  width: 5.5rem;
}

/* save / load disclosure: a quieter card */
details {
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
  padding: 0.5rem 0.875rem;
  margin: 1rem 0;
  background: var(--bg-primary);
}

summary {
  cursor: pointer;
  font-weight: 600;
}

details[open] summary {
  margin-bottom: 0.625rem;
}

textarea {
  width: 100%;
  font-family: var(--font-mono);
  font-size: 0.875rem;
  margin-bottom: 0.5rem;
}

details button + button {
  margin-left: 0.5rem;
}

/* supply: the node editor beside its resulting per-minute budget; wraps to
   stacked on a narrow screen */
.supply-cols {
  display: flex;
  flex-wrap: wrap;
  gap: 2.5rem;
  align-items: flex-start;
}

.supply-adds {
  display: flex;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.supply-budget {
  min-width: 12rem;
}

/* a row a claim wrote, tied back to the map it came from */
.supply-sited {
  margin-left: 0.5rem;
  color: var(--text-tertiary);
  font-size: 0.8rem;
  font-style: italic;
}

/* alternate recipes: product groups flowed into columns, each a wrap of toggle
   chips - filled when owned, outline when not */
.alt-groups {
  column-width: 15rem;
  column-gap: 1.5rem;
  margin-top: 0.5rem;
}

.alt-group {
  break-inside: avoid;
  margin-bottom: 0.875rem;
}

.alt-product {
  color: var(--text-secondary);
  font-size: 0.8rem;
  margin-bottom: 0.3rem;
}

.alt-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
}

.alt-chip {
  padding: 0.15rem 0.55rem;
  border: 0.5px solid var(--border-secondary);
  border-radius: 999px;
  background: transparent;
  color: var(--text-secondary);
  font-size: 0.8rem;
}

.alt-chip:hover {
  border-color: var(--accent);
  color: var(--text-primary);
  background: transparent;
}

.alt-chip-on {
  border-color: var(--accent);
  background: var(--accent-surface);
  color: var(--accent);
  font-weight: 600;
}

.alt-chip-on:hover {
  background: var(--accent-surface);
  color: var(--accent);
}

/* the cockpit: numbers align like a panel readout */
.cockpit {
  font-variant-numeric: tabular-nums;
}

/* hero - the "Max X: rate · bound" headline (the p in the first child div) */
.cockpit > div:first-child p {
  font-size: 1.25rem;
  margin: 0.25rem 0 0;
}

/* rail / build rows read as secondary data under their headings */
.cockpit li {
  color: var(--text-secondary);
}

/* net-power line (the p closing the build) */
.cockpit > div:last-child p {
  margin-top: 0.75rem;
  color: var(--text-secondary);
}

/* zoom controls above the flow graph; pan is the canvas's own scroll */
.graph-controls {
  display: flex;
  gap: 0.375rem;
  align-items: center;
  margin-bottom: 0.5rem;
}

.graph-controls button {
  min-width: 2.5rem;
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* zoom and belt-tier controls share one row above the graph */
.flow-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 1.5rem;
  align-items: center;
}

/* a label before a control group, e.g. "Belts" */
.control-label {
  color: var(--text-secondary);
  font-size: 0.85rem;
}

/* the selected belt tier wears the accent surface, like other chosen states */
.graph-controls button.active {
  border-color: var(--accent);
  background: var(--accent-surface);
  color: var(--accent);
}

/* the Simplify panel (§6.7): the heading and its toggle share a row */
.simplify-head {
  display: flex;
  align-items: baseline;
  gap: 0.75rem;
}

/* the free simplifications - pure wins - wear the accent */
.simplify-free {
  color: var(--accent);
}

.simplify-list {
  margin: 0.25rem 0 0.75rem;
  padding: 0;
  list-style: none;
}

.simplify-list li {
  margin: 0.2rem 0;
}

.simplify-list button {
  margin-right: 0.4rem;
}

.simplify-none {
  color: var(--text-secondary);
}

/* the flow graph: a scrollable canvas of what you'd build, source -> target. it
   fills the now full-width app; the SVG inside scrolls when it outgrows even
   that. */
.flow-graph {
  margin: 0.5rem 0;
  overflow: auto;
  max-height: 85vh;
  background: var(--bg-page);
  border: 0.5px solid var(--border-tertiary);
  border-radius: var(--radius-lg);
}

.flow-graph svg {
  display: block;
}

/* the wiring legend under the graph; capped so it stays readable when the app
   is full-width */
.graph-note {
  max-width: 55rem;
  margin: 0.625rem 0 0;
  color: var(--text-secondary);
  font-size: 0.875rem;
}

/* every node and edge fades smoothly when a hover dims it */
.gitem {
  transition: opacity 0.12s ease;
  cursor: default;
}

.gitem.dimmed {
  opacity: 0.12;
}

/* nodes: a machine block, an extractor, a bin - each a placeable thing */
.gnode {
  fill: var(--bg-primary);
  stroke: var(--border-secondary);
  stroke-width: 1;
}

.gnode-source {
  fill: var(--bg-secondary);
}

/* the binding source - what caps the plan - wears the amber accent */
.gnode-binding {
  stroke: var(--text-accent);
  stroke-width: 1.75;
}

.gprice {
  fill: var(--text-accent);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
}

/* a belt is a belt, not a machine: a thin bar, quieter border */
.gnode-belt {
  fill: var(--bg-secondary);
  stroke: var(--border-tertiary);
}

/* a bin reads as storage: dashed edge */
.gnode-sink {
  stroke-dasharray: 3 2;
}

.glabel {
  fill: var(--text-primary);
  font-family: var(--font-sans);
  font-size: 11px;
  font-weight: 600;
}

.gdetail,
.gbelt-label {
  fill: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: 10px;
}

/* the belt BOM on a producer node - how many belts its output needs, and the
   machine split - the buildable answer, so it carries the accent, not the muted
   detail colour */
.gbelt {
  fill: var(--accent);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 600;
}

/* material rides the edges (belts); power is a badge, never an edge */
.gedge {
  fill: none;
  stroke: var(--border-secondary);
  stroke-width: 1;
}

/* a chip behind each rate label so it stays legible where edges converge */
.gchip {
  fill: var(--bg-page);
  opacity: 0.9;
}

.gedge-label {
  fill: var(--text-tertiary);
  font-family: var(--font-mono);
  font-size: 9px;
}

/* the flow cue: a chevron just past each rate chip, nose pointing producer ->
   consumer - the belt's own direction arrows, in the label's quiet grey */
.gchevron {
  fill: var(--text-tertiary);
}

.gpower-pos {
  fill: var(--text-success);
  font-family: var(--font-mono);
  font-size: 10px;
}

.gpower-neg {
  fill: var(--text-danger);
  font-family: var(--font-mono);
  font-size: 10px;
}

/* production lines: the tab bar sits above everything, because which line
   you're in changes the meaning of every panel below it */
.line-tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0.75rem 0 0.5rem;
  border-bottom: 0.5px solid var(--border-tertiary);
  padding-bottom: 0.5rem;
}

.line-tab {
  padding: 0.3rem 0.8rem;
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--text-secondary);
  font-size: 0.9rem;
  max-width: 16rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.line-tab:hover {
  border-color: var(--accent);
  color: var(--text-primary);
}

/* the current line reads as selected, not merely hovered */
.line-tab-on {
  border-color: var(--accent);
  background: var(--accent-surface);
  color: var(--accent);
  font-weight: 600;
}

.line-tab-add {
  font-weight: 600;
  padding: 0.3rem 0.7rem;
}

.line-bar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
}

/* the name field is the tab's label; sized to look like the heading it is */
.line-name {
  flex: 1 1 auto;
  max-width: 22rem;
  font-size: 1rem;
  padding: 0.3rem 0.5rem;
  border: 0.5px solid var(--border-secondary);
  border-radius: var(--radius-md);
  background: var(--bg-primary);
  color: var(--text-primary);
}

/* import/export: one row per scope, the scope named on the left so a button
   can never be ambiguous about how much it replaces */
.io-row {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.5rem;
}

.io-scope {
  min-width: 7rem;
  color: var(--text-secondary);
  font-size: 0.9rem;
}

/* the armed state of the only irreversible button in the app */
.io-danger {
  border-color: var(--text-danger);
  color: var(--text-danger);
  font-weight: 600;
}

/* storage had something to say about the player's data - never quiet */
.storage-note {
  border-color: var(--text-accent);
  color: var(--text-accent);
}

/* an on-hand part: the left edge like a source, but nothing to place - so it
   reads quieter than an extractor, and never carries the binding accent */
.gnode-onhand {
  fill: var(--bg-secondary);
  stroke: var(--border-secondary);
  stroke-dasharray: 4 3;
}

/* the rates an on-hand plan depends on are a claim to verify, not a result */
.on-hand-check {
  color: var(--text-accent);
}

/* the "recomputing" badge, shown while a solve is in flight. it's mounted before
   the solve blocks the thread; the reveal holds it invisible for ~250ms, so a
   fast solve unmounts it before it ever shows - only a slow one fades in. the
   fade and the spin are compositor animations (opacity, transform), so they keep
   advancing even while the main thread is frozen mid-solve. translateZ(0) forces
   the layer that lets that happen.

   It sits in the *middle* of the viewport, not in a corner. A corner badge is
   missable unless you already know it's there, and the thing it reports - the
   app is busy and the numbers you're reading are stale - is exactly what you
   must not miss. It stays pointer-transparent, so it blocks nothing but the
   eye. */
.busy {
  position: fixed;
  top: 50%;
  left: 50%;
  z-index: 1000;
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.75rem 1.25rem;
  background: var(--bg-primary);
  color: var(--text-primary);
  border: 1px solid var(--border-secondary);
  border-radius: 999px;
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
  font-size: 1rem;
  pointer-events: none;
  will-change: opacity;
  transform: translate(-50%, -50%) translateZ(0);
  animation: busy-reveal 450ms ease-out forwards;
}

.busy-spinner {
  width: 1.125rem;
  height: 1.125rem;
  border-radius: 50%;
  border: 2px solid var(--border-tertiary);
  border-top-color: var(--accent);
  animation: busy-spin 0.7s linear infinite;
}

/* held invisible, then eased in - the hold is the threshold that skips the
   flicker on a fast solve */
@keyframes busy-reveal {
  0%,
  55% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@keyframes busy-spin {
  to {
    transform: rotate(360deg);
  }
}

/* respect a reduced-motion preference: no spin, and a plain quick fade */
@media (prefers-reduced-motion: reduce) {
  .busy {
    animation-duration: 200ms;
  }
  .busy-spinner {
    animation: none;
  }
}

/* --- the world axis (§6.8): the map, and where to build ------------------- */

/* the map is a planning surface, not a picture: it fills the card, keeps its
   aspect, and never grows tall enough to push the ranking off the screen.

   No box of its own. The world's edge is *drawn* now, at the extent the world
   actually has, so a CSS border would be a second frame at a different size -
   and since the figure is square in a wide card, the space between the two
   frames is dead space that reads as part of the map. One frame, and it is the
   one that means something. */
.map {
  display: block;
  width: 100%;
  max-height: 72vh;
}

/* zoom scales the figure inside this container, and its scrollbars are the
   pan - the flow graph's idiom pointed at the map */
.map-scroll {
  overflow: auto;
  max-height: 76vh;
  margin: 0.75rem 0;
}

/* past 100% the fit-to-card cap comes off, so width brings height with it
   through the figure's own aspect ratio and the container scrolls */
.map-zoomed {
  max-height: none;
  height: auto;
}

/* the kilometre lattice. It has to be almost subliminal: the moment you can
   read the grid instead of through it, it has become the subject */
.map-rule {
  stroke: var(--border-tertiary);
  stroke-width: 250;
  opacity: 0.35;
}

/* the lines through zero are the two a player can actually name */
.map-rule-axis {
  stroke: var(--text-tertiary);
  stroke-width: 450;
  opacity: 0.5;
}

/* the graticule's numbers, in the game's own signed kilometres */
.map-ticks text {
  fill: var(--text-tertiary);
  font-family: var(--font-sans);
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
}

/* the world's edge - the one line on this map that says how much of it you
   still haven't looked at */
.map-border {
  fill: none;
  stroke: var(--border-secondary);
  stroke-width: 600;
}

/* the player's own buildings: the orientation layer, deliberately quiet - it is
   there to be recognized, never to be read */
.map-footprint rect {
  fill: var(--text-tertiary);
}

/* a site's colour carries its resource, so the outline only has to separate it
   from its neighbours and from the footprint underneath */
.map-site {
  stroke: var(--bg-page);
  stroke-width: 300;
}

/* claimed sites are drawn hollow - present, yours, and not on offer */
.map-site-claimed {
  fill-opacity: 0.15;
  stroke: var(--text-secondary);
  stroke-width: 600;
}

/* the sites of the place being shown */
.map-site-lit {
  stroke: var(--accent);
  stroke-width: 1200;
}

/* this line's own claimed sites: where you decided to build, kept in ink after
   the ranking that chose them is gone. inked in the text colour, not the
   accent - the accent is the ranking's transient voice, and a claim has to
   read as yours even inside a lit place (this sits after -lit so it wins) */
.map-site-mine {
  stroke: var(--text-primary);
  stroke-width: 900;
}

/* sites another line's plan already spends: spoken for by a plan rather than a
   building, so the same hollow reading as a save claim, dashed to say which */
.map-site-spoken {
  fill-opacity: 0.15;
  stroke: var(--text-secondary);
  stroke-width: 600;
  stroke-dasharray: 900 600;
}

/* the pointer's site, popped in size so the card below and the dot agree -
   a scale rather than a stroke, so the site keeps whatever its marks say */
.map-site-hover {
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(1.4);
}

/* claims the loaded world can't corroborate - counted, never silently absent */
.map-note {
  margin: 0.375rem 0 0;
  color: var(--text-secondary);
  font-size: 0.85rem;
}

/* the hovered site, named. steady height, so the map doesn't jump underfoot
   as the pointer crosses sites */
.map-site-card {
  margin: 0.25rem 0 0;
  min-height: 1.4rem;
  font-size: 0.9rem;
  font-variant-numeric: tabular-nums;
}

.map-site-card-empty {
  color: var(--text-tertiary);
}

/* the ring that says "here" */
.map-ring {
  fill: none;
  stroke: var(--accent);
  stroke-width: 900;
  stroke-dasharray: 3000 2000;
  opacity: 0.9;
}

/* the ranking: one row per place, best first */
.places {
  list-style: none;
  padding: 0;
  margin: 0.5rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.place {
  padding: 0.625rem 0.75rem;
  background: var(--bg-secondary);
  border: 1px solid var(--border-tertiary);
  border-radius: var(--radius-md);
}

.place-head {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: baseline;
}

.place-head button {
  margin-left: auto;
}

.place-where {
  color: var(--text-secondary);
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

/* one chip per node you would claim - the thing you place, in the words the
   supply editor uses, with the map's colour on its edge so the two views read
   as one */
.place-sites {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.5rem;
}

.place-site {
  padding: 0.125rem 0.5rem;
  background: var(--bg-primary);
  border: 1px solid var(--border-tertiary);
  border-left: 4px solid var(--text-tertiary);
  border-radius: var(--radius-md);
  font-size: 0.85rem;
}

/* what this pocket can't reach, priced. One line per raw, stacked rather than
   run together, because each is a separate offer to go and get something */
.place-missing {
  margin: 0.5rem 0 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  color: var(--text-accent);
  font-size: 0.85rem;
}

/* a site this plan can't use is context, not information: uniform, quiet, and
   never drawn over one it can */
.map-context {
  color: var(--text-tertiary);
  opacity: 0.45;
}

.map-site-context {
  stroke: none;
}

/* the ranked places, numbered to match the list under the map */
.map-place {
  color: var(--accent);
  opacity: 0.55;
}

.map-place-best {
  opacity: 1;
}

.map-ring {
  fill: none;
  stroke: currentColor;
  stroke-width: 900;
  stroke-dasharray: 3000 2000;
}

.map-rank {
  fill: currentColor;
  font-family: var(--font-sans);
  font-weight: 600;
}

/* the key to the map's colours, for the resources in play only */
.map-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
  margin-top: 0.5rem;
}

/* a place's number, tying its row to its ring */
.place-rank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.5rem;
  height: 1.5rem;
  padding: 0 0.25rem;
  background: var(--accent-surface);
  color: var(--accent);
  border-radius: 999px;
  font-size: 0.8rem;
  font-weight: 600;
}

/* a place's ladder: how far it can be given up, one node at a time */
.ladder {
  margin-top: 0.5rem;
  border-collapse: collapse;
  font-size: 0.85rem;
  font-variant-numeric: tabular-nums;
}

.ladder th {
  padding: 0.125rem 0.75rem 0.125rem 0;
  color: var(--text-secondary);
  font-weight: 500;
  text-align: left;
}

.ladder td {
  padding: 0.125rem 0.75rem 0.125rem 0;
}

/* the full place is the first rung and is claimed by the row above, so its
   empty cell shouldn't leave the column looking broken */
.ladder td:last-child {
  min-width: 4rem;
}

/* a rung that gives up a node for no output at all - the supply side's free
   simplification, in the same green the plan uses for a gain */
.rung-free {
  margin-left: 0.5rem;
  padding: 0 0.375rem;
  border-radius: 999px;
  background: var(--bg-secondary);
  color: var(--text-success);
  font-size: 0.75rem;
}
