/* Octant — see docs/BRAND.md. Brass is identity; the cool colours are data only. */
:root {
  --ink:       #0A0E17;
  --surface:   #131A28;
  --line:      #2A3547;
  --text:      #E6EAF2;
  --muted:     #8B97AD;
  --brass:     #C9A227;
  --brass-lit: #E3BE4A;
  --cyan:      #4CC9F0;

  /* THE TYPE SCALE. Six steps, and the reason it is written down is that there were twenty-four
   * sizes before it -- .58, .6, .62, .64, .66, .68, .7, .72, .74, .75, .76, .78, .8, .82, .84,
   * .86, .88, .9, .95, 1, 1.05, 1.1 -- which is not a scale, it is a history of individual
   * decisions. Nothing was WRONG with any of them; the problem is that a panel built from
   * eleven sizes between 10 and 14 px reads as approximate, because the eye registers the
   * inconsistency long before it could name it.
   *
   * Each step is the value nearest the cluster it replaces, so this consolidates the set
   * without redesigning the layout: no text moves by more than about half a pixel.
   *
   * Not every font-size is typography. The 16px on touch inputs is the iOS zoom guard, the
   * wordmark carries its own size with its letter-spacing, and the timeline step button is
   * a glyph filling a fixed-height button. Those three keep their own numbers. */
  --fs-2xs:    .64rem;   /* micro labels, badges, the watermark */
  --fs-xs:     .7rem;    /* hints and small print */
  --fs-sm:     .78rem;   /* secondary UI: node titles, definition lists */
  --fs-md:     .86rem;   /* controls and body copy */
  --fs-lg:     1.05rem;  /* dialog and section headings */

  --font-ui:   "IBM Plex Sans", system-ui, -apple-system, sans-serif;
  --font-data: "IBM Plex Mono", ui-monospace, "Cascadia Mono", monospace;
}

* { box-sizing: border-box; }

/* Every link, in every state.
   Browsers colour unvisited links dim blue and visited ones purple, and on this background
   both are close to unreadable — which is exactly what happened to the CelesTrak link once
   anyone had clicked it. Styling `a` alone does not fix it: `a:visited` has to be named
   explicitly or the user-agent rule keeps winning for visited links.
   Later, more specific rules (.credits a, .topnav a) still override this. */
a, a:visited { color: var(--brass-lit); }
a:hover { color: var(--brass); }

html, body {
  margin: 0;
  height: 100%;
  background: var(--ink);
  color: var(--text);
  font-family: var(--font-ui);
  overflow: hidden;
}

/* SMALL PRINT IS ONE SIZE, DECIDED HERE RATHER THAN BY HOW DEEP IT SITS.
 *
 * Every font-size in this file is already in rem, so nothing here compounds -- and the
 * measurement said otherwise: seventeen distinct sizes on screen, values like 13.12, 11.84 and
 * 9.87 px that nobody would choose, and small.hint rendering at 13.33 px in one panel and
 * 9.87 px in another. Same element, same class, different size.
 *
 * The culprit is the BROWSERS OWN default. small is 0.8333em in the user-agent stylesheet, and
 * em is relative, so wherever no rule of ours matched, the size became a function of whatever
 * panel it happened to sit in. A hint inside a .74rem block came out at 9.87 px, which is below
 * anything readable and was never a decision.
 *
 * One rem-based rule at the lowest possible specificity fixes it: every context that genuinely
 * wants another size already states one and still wins, and everything else lands on a size that
 * was chosen. */
small,
.hint {
  font-size: var(--fs-xs);
}

/* The jump-to-the-moment button. Text rather than a glyph, because "closest approach" is not
   a thing anybody has an icon for, and brass because it is the one control here that is about
   this scenario rather than about playback in general. */
.focus-btn {
  width: auto;
  padding: 0 .5rem;
  font-size: var(--fs-2xs);
  letter-spacing: .04em;
  color: var(--brass);
  border-color: rgba(201, 162, 39, .45);
  white-space: nowrap;
}

.focus-btn:hover {
  background: rgba(201, 162, 39, .16);
}

/* HIT AREAS, WITHOUT CHANGING THE PICTURE.
 *
 * Measured in the running page: of 223 interactive elements, 50 are under 24 px on some axis.
 * Twenty-one of those are checkboxes wrapped in a label, and a label 234 px wide is a perfectly
 * good target -- the box being 13 px is irrelevant when the whole row is clickable. That left
 * 29 that are genuinely as small as they look, and almost all of them are the round icon
 * buttons: the "?" at 15x15, the info button at 15x15, the visibility eye at 16x12.
 *
 * They are the right SIZE -- a 24 px "?" would shout -- so the drawing stays and only the
 * target grows, as a transparent box behind each one. 24 px is the WCAG 2.2 minimum and is
 * about where a small control stops feeling like a dare.
 *
 * The insets differ because the neighbours do. The eye sits in a dense object row next to the
 * name button, so it gets less; the others have space around them. Checked afterwards for
 * overlap between siblings -- an invisible box that steals its neighbour's clicks is a worse
 * bug than the one being fixed, and it is invisible in both senses. */
.hint-toggle,
.info-btn,
.node-add,
.obj-eye {
  position: relative;
}

.hint-toggle::after,
.info-btn::after,
.node-add::after,
.obj-eye::after {
  content: "";
  position: absolute;
  /* MEASURED, NOT CHOSEN. Expanding uniformly stole clicks: at 6 px the "?" starts covering the
     select above it. Sweeping the two axes against every visible control in an open section, 5
     px horizontally and 2 px vertically is the largest expansion with ZERO overlap; 6 px
     horizontally introduces four. Vertical is the tight axis because these buttons sit directly
     under the control they annotate.
     So the width reaches the 24 px minimum and the height does not. That is a layout fact, not
     a CSS one: the rows would have to grow, which is a visible change to the design rather than
     an invisible fix to a target. */
  inset: -2px -5px;
}

/* THE FOCUS RING, WHICH THIS FILE HAD ONE OF.
 *
 * Exactly one :focus-visible rule existed, on the panel grip. Everything else fell through to
 * the browser default -- visible, so not broken, but a different shape and colour from anything
 * else here, and inconsistent between a button, a select and a slider.
 *
 * :focus-visible rather than :focus, so a mouse click does not leave a ring behind; that is the
 * behaviour people read as "stuck". :where() keeps the specificity at zero, so every existing
 * rule still wins and this only fills the gap where nothing else spoke. */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 2px solid var(--brass);
  outline-offset: 2px;
}

/* The "?" and the prose it heads, on one row.
 *
 * The button used to sit on a line of its own above the paragraph, which spent a row of a
 * narrow panel to display one character. align-items: flex-start keeps it level with the first
 * line of text rather than centred against a paragraph that may be five lines tall, and
 * min-width: 0 lets the text column shrink -- without it a flex child refuses to go below its
 * longest word and the panel gains a horizontal scrollbar. */
.note-block {
  display: flex;
  align-items: flex-start;
  gap: .4rem;
}

.note-block-body {
  flex: 1 1 auto;
  min-width: 0;
}

header {
  display: flex;
  align-items: center;
  gap: .75rem;
  padding: .6rem 1rem;
  border-bottom: 1px solid var(--line);
  background: var(--surface);
  height: 52px;
}

.logo { width: 26px; height: 26px; color: var(--brass); flex: none; }

h1 {
  font-size: var(--fs-lg);
  margin: 0;
  letter-spacing: .14em;
  text-transform: uppercase;
  font-weight: 600;
}

.tag { color: var(--muted); font-size: var(--fs-md); margin-left: .25rem; }

main {
  display: flex;
  height: calc(100% - 52px);
}

#panel {
  /* Width is a user decision now, not a constant. 300px was chosen when the panel held a
     handful of sliders; it now carries instruments, a vehicle, drag properties and the object
     tree, and a fixed width means whichever of those is widest decides for the rest. */
  width: var(--panel-w, 300px);
  flex: none;
  padding: 1rem;
  border-right: 1px solid var(--line);
  background: var(--surface);
  overflow-y: auto;
  /* HORIZONTAL overflow is a bug, not a feature. A sideways scrollbar on a control panel
     means a control is off screen and nothing says so -- which is how the antenna picker
     pushed station names out of view. Clip it here and let the contents wrap instead. */
  overflow-x: hidden;
}
#panel * { max-width: 100%; }

#panel h2 {
  font-size: var(--fs-xs);
  text-transform: uppercase;
  letter-spacing: .14em;
  color: var(--muted);
  margin: 1.25rem 0 .6rem;
  font-weight: 600;
}
#panel h2:first-child { margin-top: 0; }

label {
  display: block;
  margin-bottom: .9rem;
  font-size: var(--fs-md);
  color: var(--muted);
}

.val {
  float: right;
  color: var(--text);
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}

input[type=range] {
  width: 100%;
  margin-top: .45rem;
  accent-color: var(--brass);
}

dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: .3rem .75rem;
  margin: 0;
  font-size: var(--fs-md);
}
dt { color: var(--muted); }
dd {
  margin: 0;
  text-align: right;
  font-family: var(--font-data);
  font-variant-numeric: tabular-nums;
}

.model, .note {
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding-top: .7rem;
  margin-top: 1.1rem;
}
.model strong { color: var(--text); }
.note a { color: var(--brass-lit); }

#globe { flex: 1; position: relative; }

#boot {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--ink);
  color: var(--muted);
  font-family: var(--font-data);
  z-index: 50;
  transition: opacity .3s;
}
#boot.done { opacity: 0; pointer-events: none; }

/* Cesium chrome we do not want */
.cesium-viewer-bottom, .cesium-widget-credits { display: none !important; }
.cesium-viewer, .cesium-widget, .cesium-widget canvas { background: var(--ink); }

label.check {
  display: block;
  margin: 1rem 0 .3rem;
  color: var(--text);
  font-size: var(--fs-md);
  cursor: pointer;
}
label.check input { accent-color: var(--brass); margin-right: .4rem; }
label.check small {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin: .25rem 0 0 1.4rem;
  line-height: 1.4;
}

.credits {
  font-size: var(--fs-xs);
  line-height: 1.5;
  color: var(--muted);
  margin-top: .6rem;
}
.credits a { color: var(--muted); }

select {
  width: 100%;
  margin-top: .3rem;
  padding: .4rem .5rem;
  background: var(--ink);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 3px;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
}
label small {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-top: .35rem;
  line-height: 1.4;
}


/* Watermark. Deliberately unobtrusive but always present: a screenshot of a preliminary
   result should carry both its provenance and the word "preview" wherever it ends up. */
#watermark {
  /* Anchored to the TOP of the timeline, not to the globe.
     Bottom-left of the globe is where the timeline's own readout lives, and bottom:10px put
     this straight on top of it -- trading the collision with the header for a collision with
     the clock. bottom:100% of the timeline means "directly above whatever height the timeline
     currently is", which survives the chart dock opening and closing without measuring
     anything. */
  position: absolute;
  left: 0;
  bottom: 100%;
  margin-bottom: 8px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .3rem .55rem;
  border-radius: 4px;
  background: rgba(10, 14, 23, .55);
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: var(--fs-xs);
  letter-spacing: .02em;
  pointer-events: none;
  user-select: none;
}
#watermark svg { width: 13px; height: 13px; color: var(--brass); flex: none; }
#watermark strong { color: var(--text); letter-spacing: .12em; font-weight: 600; }


button {
  width: 100%;
  padding: .5rem .6rem;
  background: var(--brass);
  color: var(--ink);
  border: 0;
  border-radius: 3px;
  font-family: var(--font-ui);
  font-size: var(--fs-md);
  font-weight: 600;
  cursor: pointer;
}
button:hover { background: var(--brass-lit); }
#shareNote {
  display: block;
  color: var(--muted);
  font-size: var(--fs-xs);
  margin-top: .4rem;
  line-height: 1.4;
  min-height: 2.1em;
}

.topnav { margin-left: auto; }
.topnav a { color: var(--muted); text-decoration: none; font-size: var(--fs-sm); }
.topnav a:hover { color: var(--brass-lit); }

.topnav a { margin-left: .8rem; }


/* TLE panel: swaps between an input form and a loaded-satellite summary. */
/* The loaded-satellite summary sits in the SELECTED OBJECT block now, not inside the lookup
   form, so it shows and hides by its own [hidden] rather than by a class on a container that
   is only on screen while the add dialog is open. The lookup form is always usable: it is
   reached by pressing add, and pressing add always means you want to add something. */
.tle-loaded[hidden] { display: none; }
.tle-loaded { display: block; margin-bottom: .6rem; }
/* .row was only ever defined inside #tleBlock, so every other row in the app — the target
   entry, the file buttons, the whole vehicle form — was a plain block and its fields stacked
   to full width one per line. The scoped rules below still refine it; this makes it mean
   something everywhere it is used. */
.row { display: flex; gap: .4rem; align-items: flex-end; }
.row > * { min-width: 0; }
.row input[type="text"], .row select { flex: 1; }
.row button { width: auto; flex: none; }

#tleBlock .row { display: flex; gap: .4rem; }
#tleBlock .row input { flex: 1; min-width: 0; }
#tleBlock input[type=text], #tleBlock textarea {
  width: 100%;
  padding: .4rem .5rem;
  background: var(--ink);
  color: var(--text);
  border: 1px solid var(--line);
  border-radius: 3px;
  font-family: var(--font-data);
  font-size: var(--fs-sm);
}
#tleBlock textarea { margin-top: .4rem; resize: vertical; white-space: pre; overflow-x: auto; }
#tleBlock .row button { width: auto; padding: .4rem .8rem; flex: none; }
button.ghost-btn {
  width: 100%; margin-top: .4rem;
  background: transparent; color: var(--muted);
  border: 1px solid var(--line);
}
button.ghost-btn:hover { background: var(--surface); color: var(--text); }
#tleStatus { display: block; margin-top: .4rem; color: var(--muted); font-size: var(--fs-xs); }
#tleStatus.bad { color: var(--coral, #F4795B); }
#tleName { display: block; color: var(--text); font-size: var(--fs-md); }
#tleDetail { display: block; color: var(--muted); font-size: var(--fs-xs); font-family: var(--font-data); margin-top: .25rem; line-height: 1.5; }

/* The CelesTrak credit sits inside muted small text, where an inherited grey link is
   invisible. Underline it too — colour alone is not an accessible affordance. */
#tleBlock small a, #tleBlock small a:visited {
  color: var(--brass-lit);
  text-decoration: underline;
}


/* Quick picks and name-search results. */
.chips { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
button.chip {
  width: auto; padding: .25rem .5rem;
  background: var(--ink); color: var(--muted);
  border: 1px solid var(--line); border-radius: 999px;
  font-size: var(--fs-xs); font-weight: 400;
}
button.chip:hover { background: var(--surface); color: var(--brass-lit); border-color: var(--brass); }

#searchResults { max-height: 190px; overflow-y: auto; margin: .4rem 0; }
#searchResults:empty { margin: 0; }
button.result {
  display: flex; justify-content: space-between; align-items: baseline; gap: .5rem;
  width: 100%; padding: .35rem .5rem; margin-bottom: .2rem;
  background: var(--ink); color: var(--text);
  border: 1px solid var(--line); border-radius: 3px;
  font-size: var(--fs-sm); font-weight: 400; text-align: left;
}
button.result:hover { background: var(--surface); border-color: var(--brass); }
button.result small { color: var(--muted); font-family: var(--font-data); font-size: var(--fs-xs); }
#searchResults > small { display: block; color: var(--muted); font-size: var(--fs-xs); padding: .3rem .1rem; }
#tleBlock .row + .row { margin-top: .4rem; }

/* ------------------------------------------------------------------ animated mark
   The globe from docs/BRAND.md, built by globe-mark.js into any [data-octant-globe].
   Square by construction: the canvas is a circle and the orbit is laid out from the
   host's width, so a non-square box would put the satellite through the planet. */
.octant-globe {
  position: relative;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  --oct-accent: var(--brass);
  --oct-panel:  var(--cyan);
  --oct-muted:  var(--muted);
}
.octant-globe .og-ring {
  position: absolute; inset: 3%;
  border: 1px solid var(--line);
  border-radius: 50%;
  /* The orbit is a circle seen at 74° — an ellipse. Rotating the ring rather than
     hand-fitting an ellipse keeps it in step with the satellite, which is placed from
     the same two angles in globe-mark.js. */
  transform: rotateZ(-18deg) rotateX(74deg);
}
.octant-globe .og-sphere {
  position: relative; z-index: 2;
  width: 74%; aspect-ratio: 1;
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 32% 27%, #45597D 0%, #2B3A54 38%, #162033 68%, #070A11 100%);
  box-shadow: 0 0 0 1px var(--line), 0 0 70px -12px rgba(227, 190, 74, .3);
}
.octant-globe canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
/* Specular highlight and the night side, over the coastlines. */
.octant-globe .og-shade {
  position: absolute; inset: 0; border-radius: 50%;
  background:
    radial-gradient(circle at 30% 26%, rgba(206, 224, 255, .26) 0%, rgba(255, 255, 255, 0) 44%),
    radial-gradient(circle at 76% 78%, rgba(3, 5, 10, .86) 0%, rgba(3, 5, 10, .3) 46%, rgba(0, 0, 0, 0) 72%);
}
.octant-globe .og-rim {
  position: absolute; inset: 0; border-radius: 50%;
  box-shadow: inset 0 0 28px 5px rgba(5, 8, 14, .85), inset 0 0 2px 1px rgba(227, 190, 74, .32);
}
.octant-globe .og-sat {
  position: absolute; left: 0; top: 0;
  width: 20%; max-width: 72px;
  will-change: transform;
  transition: opacity .2s linear;
}
.octant-globe .og-sat svg { display: block; width: 100%; height: auto; }

/* ------------------------------------------------------------------ boot splash */
/* The base rule above already makes this a centred grid; a single column falls out of
   that, so only the spacing between the globe, the wordmark and the status line is new. */
/* place-items in the base rule centres each item within its own row, but a grid's auto
   rows stretch to fill the container — with three children that spread the globe, the
   wordmark and the status line across the whole viewport instead of grouping them.
   align-content is what packs the rows together. */
#boot { gap: 1.4rem; align-content: center; }
#boot .octant-globe { width: min(46vmin, 300px); }
#boot .boot-word {
  display: flex; align-items: center; gap: .7rem;
  font-family: var(--font-ui);
  font-size: 1.6rem; font-weight: 600; letter-spacing: .14em;
  color: var(--text);
}
#boot .boot-word .logo { width: 30px; height: 30px; }
#boot .boot-status { font-size: var(--fs-sm); letter-spacing: .1em; text-transform: uppercase; }

/* ------------------------------------------------------------------ small screens
   The tool is installable to a phone home screen (manifest.webmanifest), and inviting
   people to install it means the layout has to survive a phone. Side by side, a 300px
   panel on a 375px screen leaves 75px of globe.

   Stacked instead: the globe takes the top of the viewport at a fixed share, the controls
   scroll underneath it. The globe keeps the larger half because it is the thing being
   looked at, and a scrolling panel loses nothing by being below the fold. */
@media (max-width: 760px) {
  header { height: 46px; padding: .5rem .7rem; gap: .5rem; }
  header .tag { display: none; }     /* first thing to go: it is the least load-bearing */
  header h1 { font-size: var(--fs-lg); }
  .topnav { font-size: var(--fs-sm); }

  main { flex-direction: column; height: calc(100% - 46px); }

  #globe { height: 52vh; flex: none; order: -1; }   /* above the controls, whatever the source order */

  #panel {
    width: 100%;
    flex: 1 1 auto;
    border-right: none;
    border-top: 1px solid var(--line);
    /* Home-screen apps sit under the iOS gesture bar; without this the last control is
       unreachable behind it. Harmless everywhere the inset is zero. */
    padding-bottom: calc(1rem + env(safe-area-inset-bottom));
  }

  /* Touch targets. A 16px slider thumb is a fingernail; the WCAG floor is 44px and these
     sliders are the primary way the tool is operated. */
  input[type="range"] { height: 32px; }
  #panel button { min-height: 40px; }
  #panel input[type="text"], #panel select, #panel textarea { min-height: 40px; font-size: 16px; }

  #watermark { font-size: var(--fs-2xs); }
}

/* iOS zooms the page when a font-size under 16px is focused, and never zooms back out.
   Applies at every width, not just the phone breakpoint, because a small tablet in
   portrait hits the same behaviour. */
@supports (-webkit-touch-callout: none) {
  input, select, textarea { font-size: 16px; }
}

/* Playback controls. */
.playback { display: flex; gap: .4rem; align-items: center; margin-bottom: .5rem; }
.play-btn { flex: 1; }
.playback select { width: auto; flex: none; }
#scrub { width: 100%; }
#elapsed {
  display: block; color: var(--muted);
  font-family: var(--font-data); font-size: var(--fs-xs); margin-top: .2rem;
}

/* ------------------------------------------------------------------ object list
   The scenario, as rows. The selected row is what every control below it edits, so the
   highlight has to be unmissable — a brass left edge rather than a subtle tint. */
#objectList { margin-bottom: .5rem; }

.obj-row {
  display: flex; align-items: center; gap: .4rem;
  padding: .3rem .4rem;
  border: 1px solid transparent;
  border-left: 3px solid transparent;
  border-radius: 3px;
}
/* A small grip, not the whole row. Most of a row's own width is already interactive --
   colour swatch, name, an antenna picker on a station, eye, delete -- and making all of that
   draggable risks a browser treating a click meant for one of them as the start of a drag
   instead. A dedicated handle keeps every existing click exactly what it was; only the
   handle carries draggable="true". */
.obj-drag {
  flex: none; width: 14px; text-align: center;
  color: var(--line); cursor: grab; user-select: none;
}
.obj-drag:hover { color: var(--muted); }

/* Dragging an object onto a group's header, or onto any of its members, moves it into that
   group; dropping it on an ungrouped object or on empty space below the list takes it back
   out -- the same gesture in reverse, so there is no separate ungroup control to find. */
.obj-row.dragging { opacity: .35; }
.obj-row.drag-over { outline: 2px dashed var(--brass); outline-offset: -2px; }

.obj-row.selected {
  background: var(--ink);
  border-color: var(--line);
  border-left-color: var(--brass);
}
.obj-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; }

button.obj-name {
  flex: 1; width: auto; min-width: 0;
  padding: 0; margin: 0;
  background: none; border: none;
  color: var(--text); font-size: var(--fs-sm); font-weight: 400; text-align: left;
  /* Names come from CelesTrak and can be long; the row must not grow a scrollbar. */
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.obj-row.selected button.obj-name { color: var(--brass-lit); }

button.obj-eye, button.obj-del {
  width: auto; flex: none;
  padding: 0 .25rem; margin: 0;
  background: none; border: none;
  color: var(--muted); font-size: var(--fs-sm); line-height: 1;
}
button.obj-eye:hover, button.obj-del:hover { color: var(--text); background: none; }
button.obj-eye.off { color: var(--line); }
button.obj-del:disabled { opacity: .3; cursor: default; }
/* Delete is red always, not only on hover.
   A destructive control that looks identical to a neutral one until the pointer is already
   on it tells you what it does at the moment it is too late to be useful. */
button.obj-del { color: var(--coral, #F4795B); opacity: .75; }
button.obj-del:hover:not(:disabled) { color: var(--coral, #F4795B); opacity: 1; }

/* Elements cannot be edited while a TLE is driving the object: the numbers come from the
   elements, and offering sliders that silently discard them would be a lie. */
#orbitControls.inactive, #sensorControls.inactive { opacity: .4; pointer-events: none; }

/* Target rows carry two lines: the name, and its coordinates with the access it gets. */
.target-row .obj-name { display: block; cursor: default; }
.target-row .obj-name strong { display: block; font-weight: 600; }
.target-row .obj-name small {
  display: block; color: var(--muted);
  font-family: var(--font-data); font-size: var(--fs-xs);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
#targetList { margin-bottom: .4rem; }
#targetList + .row input { min-width: 0; }
#targetList + .row #targetName { flex: 2; }

/* ------------------------------------------------------------------ drawers
   Two things in the panel are set once and then ignored: adding a real satellite, and the
   view options. Both used to sit at full height between the controls that ARE adjusted
   repeatedly, which is most of why the sidebar needed scrolling before it needed thinking.
   <details> rather than a scripted accordion: it opens without JavaScript, it is keyboard
   operable for free, and the browser finds text inside a closed one when the page is
   searched. */
details.drawer { margin: .2rem 0 1rem; }

details.drawer > summary {
  cursor: pointer;
  list-style: none;
  /* Matched to button.ghost-btn -- padding, background, font-size and weight, centred text
     -- because "+ Add a ground station" and "+ Add a real satellite" sit in the same list of
     actions as "+ Add orbit", an actual .ghost-btn. A <details> summary's own defaults
     (left-aligned, filled background) made the two pairs read as different, heavier kinds of
     control when they are all the same kind of thing: a way to add to the scenario. */
  padding: .5rem .6rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-md);
  font-weight: 600;
  text-align: center;
  user-select: none;
}
details.drawer > summary::-webkit-details-marker { display: none; }
details.drawer > summary:hover { background: var(--surface); color: var(--text); }
details.drawer[open] > summary { margin-bottom: .6rem; color: var(--text); }

/* The heading names the object the controls below it act on. Without it, a list of objects
   above a slider labelled "Inclination" is a question rather than a statement. */
#selectedName {
  color: var(--brass-lit);
  text-transform: none;
  letter-spacing: 0;
  font-weight: 600;
}

/* ------------------------------------------------------------------ map controls
   View state belongs over the view. These sat in the sidebar between the orbit and its
   answer, which pushed the numbers below the fold and made set-once options read as
   scenario inputs. */
#mapControls {
  position: absolute;
  top: 10px; right: 12px;
  z-index: 10;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
}

#layersBtn {
  width: auto; margin: 0;
  /* Square, and large enough to hit on a touch screen without aiming. */
  padding: .4rem;
  display: grid; place-items: center;
  background: rgba(10, 14, 23, .82);
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--muted);
}
#layersBtn svg {
  width: 18px; height: 18px;
  fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linejoin: round; stroke-linecap: round;
}
#layersBtn:hover, #layersBtn.active { color: var(--brass-lit); border-color: var(--brass); }

.layers-zoom-col { display: flex; flex-direction: column; align-items: flex-end; gap: .4rem; }

/* Same joined-pair look #viewSwitch already uses (shared border, only the outer corners
   round), turned vertical: one control, two ends, rather than two buttons that happen to be
   next to each other. */
#zoomControls { display: flex; flex-direction: column; }
#zoomControls button {
  width: auto; margin: 0;
  padding: .35rem;
  display: grid; place-items: center;
  background: rgba(10, 14, 23, .82);
  border: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--fs-lg); line-height: 1;
}
#zoomControls button:first-child { border-radius: 3px 3px 0 0; }
#zoomControls button:last-child { border-radius: 0 0 3px 3px; margin-top: -1px; }
#zoomControls button:hover { color: var(--brass-lit); border-color: var(--brass); }

/* The row that holds both controls, so they share one line rather than stacking. */
#controlRow { display: flex; gap: .4rem; align-items: flex-start; }

#layersPanel {
  margin-top: .3rem;
  /* NEVER TALLER THAN THE ROOM IT HAS.
     The panel grew past the bottom of the globe and its last two toggles ended up behind the
     timeline, unreachable -- there was no way to turn station cones on at all. It is capped
     to the space between its own top and the timeline, and scrolls if it needs more. Moving
     the button would not have fixed this: a panel taller than the window is unreachable
     wherever it hangs from. */
  max-height: calc(100vh - 210px);
  overflow-y: auto;
  padding: .6rem .7rem;
  width: 250px;
  border: 1px solid var(--line);
  border-radius: 4px;
  /* Opaque: it sits over city lights and over the terminator, the two busiest backgrounds
     it will ever have. */
  background: rgba(10, 14, 23, .94);
  text-align: left;
}
#layersPanel label { margin-bottom: .7rem; }
#layersPanel > *:last-child label, #layersPanel label:last-child { margin-bottom: 0; }
#layersPanel select { width: 100%; }
#layersPanel { scrollbar-width: thin; }

.map-toggle {
  display: block;
  margin: 0 0 .7rem;
  color: var(--text);
  font-size: var(--fs-sm);
  cursor: pointer;
  user-select: none;
}
.map-toggle input { accent-color: var(--brass); margin-right: .4rem; }
.map-toggle span { color: var(--text); }
/* Checked state carries the brass, so which layers are on reads at a glance. */
.map-toggle:has(input:checked) span { color: var(--brass-lit); }
.map-toggle small {
  display: block; margin-left: 1.3rem;
  color: var(--muted); font-size: var(--fs-xs); line-height: 1.35;
}

/* ------------------------------------------------------------------ help
   The panel used to end in two paragraphs of small print, which is where text goes to be
   read once and scrolled past forever. Behind a button it can afford to say more. */
#helpBtn {
  width: auto; margin: 0 .5rem 0 0;
  padding: .25rem;
  display: inline-grid; place-items: center; vertical-align: middle;
  background: none;
  border: 1px solid var(--line);
  border-radius: 3px;
  color: var(--muted);
}
#helpBtn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor;
  stroke-width: 1.7; stroke-linecap: round;
}
#helpBtn:hover { color: var(--brass-lit); border-color: var(--brass); }

#helpDialog {
  max-width: 46rem;
  width: calc(100% - 2rem);
  max-height: 82vh;
  padding: 0;
  display: flex; flex-direction: column; overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface);
  color: var(--text);
}
#helpDialog::backdrop { background: rgba(6, 9, 15, .72); }
#helpDialog article { padding: 1.6rem 1.8rem; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
#helpDialog h2 { margin: 0 0 1rem; font-size: var(--fs-lg); letter-spacing: .04em; }
#helpDialog h3 {
  margin: 1.4rem 0 .4rem;
  font-size: var(--fs-sm); text-transform: uppercase; letter-spacing: .12em;
  color: var(--muted);
}
#helpDialog p { margin: 0 0 .7rem; font-size: var(--fs-md); line-height: 1.55; color: var(--text); }
#helpDialog .credits { margin-top: 1.4rem; color: var(--muted); font-size: var(--fs-sm); }
#helpClose {
  width: auto; margin: 0 1.8rem 1.4rem;
  padding: .45rem 1.1rem;
}

/* ------------------------------------------------------------------ timeline
   A timeline gets the width of the globe rather than the width of a sidebar, which is the
   difference between dragging to a moment and dragging near one. */
#timeline {
  position: absolute;
  left: 12px; right: 12px; bottom: 10px;
  z-index: 10;
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "transport . when" "scrub scrub scrub" "elapsed elapsed toggle" "dock dock dock";
  align-items: center;
  gap: .35rem .7rem;
  padding: .5rem .7rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: rgba(10, 14, 23, .85);
}

/* THE THREE TRANSPORT BUTTONS ARE ONE CONTROL, so they are one height -- and the height is the
   SMALL one. Play reads "▶ Play", which wrapped onto two lines in a narrow flex item and made
   Play a third taller than the chevrons either side of it. The first attempt at this stretched
   the chevrons up to match, which is the wrong way round: it made three tall buttons out of
   one wrapped label instead of unwrapping the label. nowrap keeps Play on one line and all
   three come out the same height on their own. */
.tl-transport { grid-area: transport; display: flex; gap: .35rem; align-items: center; }
/* One height, stated once, because three buttons with different fonts and different padding
   will not agree on it by accident: measured at 27 px against 22 px when left to themselves. */
#timeline .play-btn, #timeline .step-btn {
  white-space: nowrap; height: 1.75rem; line-height: 1;
  display: inline-flex; align-items: center; justify-content: center;
}
#timeline #scrub { grid-area: scrub; width: 100%; margin: 0; }
.tl-when { grid-area: when; display: flex; gap: .35rem; align-items: center; }
#timeline #elapsed {
  grid-area: elapsed;
  color: var(--muted);
  font-family: var(--font-data); font-size: var(--fs-xs);
}

#timeline .play-btn { width: auto; padding: .3rem .6rem; margin: 0; font-size: var(--fs-sm); }
#timeline select, #timeline input[type="datetime-local"] {
  width: auto; margin: 0;
  padding: .28rem .4rem;
  background: var(--ink); border: 1px solid var(--line); color: var(--text);
  font-size: var(--fs-xs); font-family: var(--font-data);
}

/* Top left, which is the only corner nothing else wants: the layer button is top right,
   the timeline owns the bottom, and with the chart dock open the bottom right is a chart.
   It still travels with a screenshot, which is the whole reason it exists. */
/* Bottom-left, not top-left. Sitting directly under the header it read as a second
   subtitle -- brand, tagline, brand, caveat, four lines of chrome before the globe. The
   EXPORT watermark is the one that has to carry provenance; this one only has to be present. */
#watermark { bottom: 100%; left: 0; right: auto; top: auto; }

@media (max-width: 760px) {
  /* On a phone the globe is 52vh and the timeline would eat a fifth of it, so the date and
     duration fold onto their own row and the labels go. */
  #timeline {
    left: 6px; right: 6px; bottom: 6px;
    grid-template-columns: auto 1fr;
    grid-template-areas: "transport scrub" "when when" "elapsed elapsed";
  }
  .tl-when { width: 100%; }
  .tl-when select, .tl-when input { flex: 1; min-width: 0; }
  #mapControls { top: 6px; right: 6px; }
  #layersPanel { width: min(250px, calc(100vw - 24px)); }
  #watermark { display: none; }   /* no room, and the export watermark is what matters */
}

/* The viewer's own box, filling the globe pane. Its siblings - map controls, timeline,
   watermark - survive the rebuild that switching central body performs. */
#cesium { position: absolute; inset: 0; }

/* ------------------------------------------------------------------ chart dock
   Part of the timeline, not a panel beside it: same element, same left and right edges, so
   the charts inherit the strip's x-axis by construction rather than by arithmetic. */

#chartsToggle {
  grid-area: toggle;
  width: auto; margin: 0; justify-self: end;
  padding: .15rem .5rem;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted); font-size: var(--fs-xs); font-weight: 400;
}
#chartsToggle:hover, #chartsToggle.active { color: var(--brass-lit); border-color: var(--brass); }

#chartDock { grid-area: dock; margin-top: .4rem; }
#chartTabs { display: flex; gap: .25rem; margin-bottom: .3rem; flex-wrap: wrap; }
#chartTabs button {
  width: auto; margin: 0;
  padding: .2rem .55rem;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted); font-size: var(--fs-xs); font-weight: 400;
}
#chartTabs button:hover { color: var(--text); }
#chartTabs button.active { color: var(--brass-lit); border-color: var(--brass); background: var(--ink); }

/* An action, not a tab -- it does not select a chart, it exports the one already showing.
   Reported directly: sharing the plain #chartTabs button look made it read as an eighth
   chart to click through rather than something that acts on the other seven. Pushed to the
   far right of the row (margin-left: auto on a flex item, not a separate rule) and given a
   dashed outline instead of a solid one so it never gets mistaken for a tab at a glance. */
#chartTabs button#chartExport {
  margin-left: auto;
  border-style: dashed;
  color: var(--muted);
}
#chartTabs button#chartExport:hover { color: var(--text); border-color: var(--brass); }

/* Scroll wraps only the canvas (see index.html) so zooming the x-axis widens the canvas
   past this box without moving the tabs above it. The height is this box's own fixed
   height, never the canvas's -- see charts.js: zoom only ever writes canvas.style.width. */
#chartScroll { overflow-x: auto; overflow-y: hidden; scrollbar-width: thin; }

#chartCanvas {
  display: block;
  width: 100%;
  /* Tall enough for a Gantt of several targets and the three bands of the elements chart,
     short enough that the globe is still the thing on screen. */
  height: 170px;
  background: rgba(6, 9, 15, .5);
  border: 1px solid var(--line);
  border-radius: 3px;
}

@media (max-width: 760px) {
  #timeline {
    grid-template-columns: auto 1fr;
    grid-template-areas: "transport when" "scrub scrub" "elapsed toggle" "dock dock";
  }
  #chartCanvas { height: 130px; }
}

/* No inset: T0 and TF should sit exactly at the ends of the bar, thumb flush with each
   edge -- an 8 px pad here once matched the charts' own PAD (see charts.js), keeping the
   scrub bar and the dock's x-axis pixel-aligned, but it read as unexplained slack at both
   ends of the control everyone actually looks at. That alignment was a nicety visible only
   with the dock open; this dead zone was visible always. Traded the former for the latter. */
#timeline #scrub { padding: 0; box-sizing: border-box; }

/* Spacecraft parameters, three to a row: the ballistic coefficient is one idea, not three. */
.sc-params {
  display: grid;
  /* Short numeric fields, as many across as fit. Flex made a five-field row wrap one field
     per line the moment the labels were long. */
  grid-template-columns: repeat(auto-fit, minmax(88px, 1fr));
  gap: .35rem;
  align-items: end;
}
.sc-params label { margin: 0; font-size: var(--fs-xs); color: var(--muted); min-width: 0; }
.sc-params input { width: 100%; margin-top: .15rem; }
#readout.one { margin: .5rem 0 .3rem; }
#decayChart {
  display: block; width: 100%; height: 72px;
  background: rgba(6, 9, 15, .5);
  border: 1px solid var(--line); border-radius: 3px;
}
#decayNote.bad { color: var(--coral, #F4795B); }

/* Groups: one row that folds, hides and shows as a unit. */
.obj-row.group-row { border-left-color: var(--line); background: rgba(19, 26, 40, .6); }
.obj-row.group-row .obj-name strong { display: block; font-size: var(--fs-sm); color: var(--text); }
.obj-row.group-row .obj-name small { display: block; color: var(--muted); font-size: var(--fs-xs); }
.obj-row.nested { padding-left: 1.1rem; }

/* The swatch IS the colour control: the dot already meant "this object", so making it the
   input keeps that meaning rather than adding a second control beside it. */
input.obj-dot {
  width: 12px; height: 12px; flex: none;
  padding: 0; margin: 0;
  border: none; border-radius: 50%;
  background: none; cursor: pointer;
  -webkit-appearance: none; appearance: none;
}
input.obj-dot::-webkit-color-swatch-wrapper { padding: 0; }
input.obj-dot::-webkit-color-swatch { border: none; border-radius: 50%; }
input.obj-dot::-moz-color-swatch { border: none; border-radius: 50%; }

/* The 2D map fills the same box the globe does, so switching swaps one for the other rather
   than resizing anything around it. */
#mapCanvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#mapCanvas[hidden] { display: none; }

#viewSwitch { display: flex; margin-bottom: .3rem; }
#viewSwitch button {
  width: auto; margin: 0;
  padding: .3rem .5rem;
  background: rgba(10, 14, 23, .82);
  border: 1px solid var(--line);
  color: var(--muted); font-size: var(--fs-xs); font-weight: 400;
}
#viewSwitch button:first-child { border-radius: 3px 0 0 3px; }
/* Overlap the shared edge instead of deleting it.
   `border-left: none` on the last button meant that when 2D was active its outline was open
   on the left -- a highlight drawn round three sides of a box. Every button keeps all four
   borders now and the pair overlap by a pixel, which is what a segmented control actually is;
   the active one is raised so its own border wins along the join. */
#viewSwitch button:last-child { border-radius: 0 3px 3px 0; margin-left: -1px; }
#viewSwitch button.active {
  color: var(--brass-lit); border-color: var(--brass);
  position: relative; z-index: 1;
}

/* Hover readout on the 2D map. Positioned in page coordinates by map2d.js. */
#mapTip {
  position: fixed; z-index: 40; pointer-events: none;
  padding: .25rem .45rem;
  background: rgba(10, 14, 23, .92);
  border: 1px solid var(--line); border-radius: 3px;
  color: var(--text); font-size: var(--fs-xs); line-height: 1.4;
  white-space: nowrap;
}
#mapTip[hidden] { display: none; }
#mapTip .tip-sub { color: var(--muted); }

/* ------------------------------------------------------------------ vehicle editor */
#vehicleDialog {
  max-width: 62rem; width: calc(100% - 2rem); max-height: 88vh;
  padding: 0; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--text);
  /* One scrollbar, not two. The dialog was scrollable AND so was the article inside it, so
     a tall form produced a pair of vertical bars side by side. The dialog is now a column
     that never scrolls; only the article does. */
  display: flex; flex-direction: column; overflow: hidden;
}
#vehicleDialog::backdrop { background: rgba(6, 9, 15, .72); }
#vehicleDialog article { padding: 1.4rem 1.6rem; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
#vehicleDialog h2 { margin: 0 0 1rem; font-size: var(--fs-lg); letter-spacing: .04em; }
#vehicleDialog h3 {
  margin: 1.2rem 0 .4rem; font-size: var(--fs-xs);
  text-transform: uppercase; letter-spacing: .12em; color: var(--muted);
}
.veh-grid { display: grid; grid-template-columns: 1fr 300px; gap: 1.4rem; align-items: start; }
.veh-form label { display: block; margin-bottom: .5rem; }
/* The label text sits ABOVE its field.
   Without display:block the text and a width:100% input share a line, and the input wins:
   "Name" was rendered as "Nam" with the box on top of the rest of it. */
.veh-form label > input[type="text"] { display: block; width: 100%; margin-top: .15rem; }
.veh-form .sc-params label > input[type="text"] { margin-top: .1rem; }

#vehCanvas {
  display: block; width: 100%; height: 240px;
  background: rgba(6, 9, 15, .6);
  border: 1px solid var(--line); border-radius: 3px;
  touch-action: none;      /* the drag rotates the model, it does not scroll the dialog */
}
.veh-preview small { display: block; margin: .4rem 0; color: var(--muted); font-size: var(--fs-xs); }
#vehNumbers {
  display: grid; grid-template-columns: auto 1fr; gap: .15rem .6rem;
  margin: .6rem 0; font-family: var(--font-data); font-size: var(--fs-sm);
}
#vehNumbers dt { color: var(--muted); }
#vehNumbers dd { margin: 0; color: var(--brass-lit); text-align: right; }
.veh-actions { padding: 0 1.6rem 1.2rem; }
#scParams.derived { opacity: .45; pointer-events: none; }

@media (max-width: 760px) {
  .veh-grid { grid-template-columns: 1fr; }
}

/* Tools. Same construction as the other dialogs: a column that does not itself scroll. */
#toolsBtn {
  width: auto; margin: 0 .5rem 0 0;
  padding: .25rem;
  display: inline-grid; place-items: center; vertical-align: middle;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted);
}
#toolsBtn:hover { color: var(--brass-lit); border-color: var(--brass); }
#toolsBtn svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.7;
  stroke-linecap: round; stroke-linejoin: round;
}


/* The tools menu. A list of destinations anchored under its button, not a container: one
   click reaches any tool, and adding a tool lengthens the list rather than burying the
   others one level deeper. */
#toolsMenu {
  position: fixed; z-index: 60;
  min-width: 15rem; padding: .3rem;
  background: var(--surface); border: 1px solid var(--line); border-radius: 5px;
  box-shadow: 0 8px 28px rgba(0, 0, 0, .5);
}
#toolsMenu[hidden] { display: none; }
#toolsMenu button {
  display: block; width: 100%; margin: 0; text-align: left;
  padding: .4rem .55rem; background: none; border: 0; border-radius: 3px;
  color: var(--text); cursor: pointer;
}
#toolsMenu button:hover:not(:disabled) { background: rgba(201, 162, 39, .12); }
#toolsMenu button:disabled { cursor: default; opacity: .45; }
.tm-name { display: block; font-size: var(--fs-md); }
.tm-sub { display: block; font-size: var(--fs-xs); color: var(--muted); margin-top: .1rem; }
.tm-sep { height: 1px; margin: .3rem .2rem; background: var(--line); }

#timeDialog {
  max-width: 32rem; width: calc(100% - 2rem); max-height: 86vh;
  padding: 0; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--text);
  display: flex; flex-direction: column; overflow: hidden;
}
#timeDialog::backdrop { background: rgba(6, 9, 15, .72); }
#timeDialog article { padding: 1.4rem 1.6rem; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
#timeDialog h2 { margin: 0 0 .8rem; font-size: var(--fs-lg); letter-spacing: .04em; }
p.tool-note { margin: 0 0 .6rem; color: var(--muted); font-size: var(--fs-sm); line-height: 1.5; }

#tcOut {
  display: grid; grid-template-columns: auto 1fr; gap: .2rem .8rem;
  margin: .8rem 0 .4rem; font-family: var(--font-data); font-size: var(--fs-sm);
}
#tcOut dt { color: var(--muted); }
#tcOut dd { margin: 0; color: var(--brass-lit); text-align: right; }
#tcNote { display: block; color: var(--muted); font-size: var(--fs-xs); }
#tcNote.bad { color: var(--coral, #F4795B); }

/* Parts list. Each part is a row that can be edited in place and removed; the kind is a
   coloured tag matching the colour the preview draws it in, so the list and the picture
   agree without anyone having to work out which box is which. */
.part {
  margin: .4rem 0; padding: .45rem .5rem;
  border: 1px solid var(--line); border-left-width: 3px; border-radius: 3px;
  background: rgba(255, 255, 255, .015);
}
.part-panel    { border-left-color: var(--cyan, #4CC9F0); }
.part-camera   { border-left-color: var(--green, #5FD9A0); }
.part-thruster { border-left-color: var(--coral, #F4795B); }

.part-head { display: flex; align-items: baseline; gap: .45rem; margin-bottom: .35rem; }
.part-tag {
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .1em;
  color: var(--muted); flex: none;
}
.part-name { font-size: var(--fs-sm); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.part-note { font-size: var(--fs-2xs); color: var(--brass); flex: none; }
.part-del {
  width: auto; flex: none; margin: 0; padding: 0 .35rem; line-height: 1.2;
  background: none; border: 1px solid var(--line); border-radius: 3px; color: var(--muted);
}
.part-del:hover { color: var(--coral, #F4795B); border-color: var(--coral, #F4795B); }
.part .sc-params label { font-size: var(--fs-2xs); }

/* The dialog action bars.
   Making .row mean something everywhere had a casualty here: .ghost-btn is width:100% and
   .row button is flex:none, so "Save to my library" took the full width and refused to
   shrink, pushing "Use this vehicle" off the right edge of the dialog. An action bar wants
   the opposite of a form row — the secondary action gives way, the primary keeps its size,
   and neither is allowed to overflow its container. */
.veh-actions { display: flex; gap: .5rem; align-items: center; }
.veh-actions button { margin-top: 0; }
.veh-actions button.ghost-btn { width: auto; flex: 1 1 auto; min-width: 0; }
.veh-actions button:not(.ghost-btn) { flex: 0 1 auto; }

/* The colour well on a part row. Small and unlabelled: the swatch IS the label, and the
   kind tag beside it wears the same colour so the list and the preview agree at a glance. */
.part-colour {
  width: 20px; height: 20px; flex: none; margin: 0; padding: 0;
  background: none; border: 1px solid var(--line); border-radius: 3px; cursor: pointer;
}
.part-colour::-webkit-color-swatch-wrapper { padding: 2px; }
.part-colour::-webkit-color-swatch { border: 0; border-radius: 2px; }

/* The two remaining converters. Same construction as the time one: a column that does not
   itself scroll, so only the article gets a scrollbar. */
#stateDialog, #frameDialog {
  max-width: 40rem; width: calc(100% - 2rem); max-height: 86vh;
  padding: 0; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--text);
  display: flex; flex-direction: column; overflow: hidden;
}
#stateDialog::backdrop, #frameDialog::backdrop { background: rgba(6, 9, 15, .72); }
#stateDialog article, #frameDialog article {
  padding: 1.4rem 1.6rem; overflow-y: auto; flex: 1 1 auto; min-height: 0;
}
#stateDialog h2, #frameDialog h2 { margin: 0 0 .8rem; font-size: var(--fs-lg); letter-spacing: .04em; }
#stNote, #frNote { display: block; margin-top: .8rem; color: var(--muted); font-size: var(--fs-xs); line-height: 1.5; }
#stNote.bad, #frNote.bad { color: var(--coral, #F4795B); }
#frOut {
  display: grid; grid-template-columns: auto 1fr; gap: .2rem .8rem;
  margin: .8rem 0 .2rem; font-family: var(--font-data); font-size: var(--fs-sm);
}
#frOut dt { color: var(--muted); }
#frOut dd { margin: 0; color: var(--brass-lit); text-align: right; }
.st-group { margin: .9rem 0 0; }
.st-group h3 {
  margin: 0 0 .3rem; font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: .12em; color: var(--muted);
}
.st-group dl {
  display: grid; grid-template-columns: auto 1fr; gap: .15rem .8rem;
  margin: 0; font-family: var(--font-data); font-size: var(--fs-sm);
}
.st-group dt { color: var(--muted); }
.st-group dd { margin: 0; color: var(--brass-lit); text-align: right; }

/* Instruments in the panel. Each carries its own cone, its own aim and its own toggle,
   because "show cones" stops meaning anything once a spacecraft has three of them. */
.instr-block { margin: .6rem 0; }
.instr-head {
  display: flex; align-items: center; gap: .4rem; margin-bottom: .3rem;
  font-size: var(--fs-xs); text-transform: uppercase; letter-spacing: .12em; color: var(--muted);
}
.instr-head span { flex: 1; }
.instr-head button {
  width: auto; flex: none; margin: 0; padding: 0 .4rem; line-height: 1.3;
  background: none; border: 1px solid var(--line); border-radius: 3px; color: var(--muted);
}
.instr-head button:hover { color: var(--brass-lit); border-color: var(--brass); }
.instr {
  margin: .3rem 0; padding: .4rem .45rem;
  border: 1px solid var(--line); border-left: 3px solid var(--cyan, #4CC9F0);
  border-radius: 3px;
}
.instr.off { opacity: .45; }
.instr-top { display: flex; align-items: center; gap: .35rem; margin-bottom: .3rem; }
.instr-top input[type="text"] { flex: 1; min-width: 0; margin: 0; padding: .15rem .3rem; font-size: var(--fs-sm); }
.instr-top input[type="checkbox"] { flex: none; margin: 0; }
.instr-top button {
  width: auto; flex: none; margin: 0; padding: 0 .35rem; line-height: 1.2;
  background: none; border: 1px solid var(--line); border-radius: 3px; color: var(--muted);
}
.instr-top button:hover { color: var(--coral, #F4795B); border-color: var(--coral, #F4795B); }
.instr label { display: block; margin: .25rem 0 0; font-size: var(--fs-2xs); color: var(--muted); }
.instr label .val { float: right; color: var(--brass-lit); font-family: var(--font-data); }
.instr input[type="range"] { margin-top: .1rem; }
.instr .warn-gimbal { display: block; margin-top: .25rem; font-size: var(--fs-2xs); color: var(--coral, #F4795B); }

/* The lifetime analysis, which lives in the chart dock rather than the modelling panel: the
   panel says what a thing IS, the dock holds what has been worked out about it (§8n.3). */
#lifetimePanel {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .4rem .6rem; border-bottom: 1px solid var(--line);
}
#lifetimePanel[hidden] { display: none; }
#lifetimePanel button { width: auto; flex: none; margin: 0; }
#lifetimePanel #lifetime {
  font-family: var(--font-data); color: var(--brass-lit); font-size: var(--fs-md); flex: none;
}
#lifetimePanel small { color: var(--muted); font-size: var(--fs-xs); flex: 1 1 14rem; min-width: 0; }
#lifetimePanel small.bad { color: var(--coral, #F4795B); }
/* The power budget's card. Same shape as the lifetime one — an analysis in the dock reads as
   a strip of its inputs above its curve, and two analyses that looked different would imply a
   difference that is not there. */
#powerPanel {
  display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
  padding: .4rem .6rem; border-bottom: 1px solid var(--line);
}
#powerPanel[hidden] { display: none; }
#powerPanel label {
  display: flex; align-items: center; gap: .3rem; margin: 0;
  font-size: var(--fs-xs); color: var(--muted); flex: none;
}
#powerPanel input { width: 4.2rem; margin: 0; padding: .15rem .3rem; font-size: var(--fs-xs); }
#powerPanel #powerArray, #powerPanel #powerBudget {
  font-family: var(--font-data); font-size: var(--fs-xs); color: var(--brass-lit); flex: none;
}
#powerPanel #powerBudget.bad { color: var(--coral, #F4795B); }
#powerPanel #powerLimits { color: var(--muted); font-size: var(--fs-xs); flex: 1 1 16rem; min-width: 0; }

#chartTabs button[hidden] { display: none; }

/* The antenna picker on a station row. Narrow: it is a property of the station, not the
   subject of the row, and the name plus its access statistics are what someone reads. */
.ant-pick {
  width: auto; flex: 0 1 8.5rem; min-width: 0; margin: 0;
  padding: .1rem .2rem; font-size: var(--fs-xs);
}

/* A station row is two lines, not one.
   The antenna picker was put inline beside the name and won: a select needs ~9rem, the panel
   gives the whole row ~15rem, and "Curitiba" came out as "Cu...". They were never competing
   for the same space in the design -- they were competing because I put them on one line.
   The name gets the full width and its coordinates and access below it; the picker gets its
   own line, indented under the colour dot so it reads as belonging to the station above. */
.obj-row.station { flex-wrap: wrap; align-items: flex-start; }
.obj-row.station button.obj-name {
  white-space: normal; overflow: visible; line-height: 1.35;
}
.obj-row.station button.obj-name small {
  display: block; color: var(--muted); font-size: var(--fs-xs); margin-top: .05rem;
}
.obj-row.station .ant-pick {
  flex: 1 0 100%; margin: .3rem 0 0 18px; width: auto;
  padding: .15rem .25rem; font-size: var(--fs-xs);
}

/* The resize grip. A real handle, not a scrollbar: dragging the edge of a panel is the
   gesture people already have for this, and a scrollbar appearing because content did not
   fit is the tool admitting it ran out of room rather than offering a way to make room. */
/* Station rows: the name line keeps its controls.
   Letting the row wrap put the antenna picker on its own line, which was the point -- but it
   also pushed the eye and the delete onto a THIRD line, orphaned under the picker. The name
   shrinks instead, and the picker is ordered last so it is always the thing that wraps. */
.obj-row.station button.obj-name { flex: 1 1 6rem; }
.obj-row.station .ant-pick { order: 99; }
.obj-row.station .obj-eye, .obj-row.station .obj-del { align-self: flex-start; }

#panelGrip {
  flex: none;
  width: 6px;
  margin-left: -3px;
  cursor: col-resize;
  background: transparent;
  position: relative;
  z-index: 5;
}
#panelGrip::after {
  content: '';
  position: absolute; inset: 0 2px;
  border-radius: 2px;
  background: transparent;
  transition: background .12s;
}
#panelGrip:hover::after, #panelGrip.dragging::after { background: var(--brass); }
#panelGrip:focus-visible { outline: 2px solid var(--brass); outline-offset: -1px; }
body.resizing { cursor: col-resize; user-select: none; }

/* A ghost button inside a row shares the row.
   .ghost-btn is width:100% because it is usually the only thing on its line. Put two in a
   .row -- "Open…" beside "Save…" -- and the first takes the whole width and pushes the second
   clean out of the panel, which is what was generating the horizontal scrollbar. I fixed this
   for .veh-actions this morning and only for .veh-actions; this is the general form, and the
   panel's sideways overflow goes with it. */
.row button.ghost-btn { width: auto; flex: 1 1 auto; min-width: 0; }

/* How many to add. Narrow: it is a modifier on the Add button, not a field in its own right. */
#partCount { width: 2.6rem; flex: none; text-align: center; padding: .2rem .1rem; }

#libraryDialog {
  max-width: 44rem; width: calc(100% - 2rem); max-height: 86vh;
  padding: 0; border: 1px solid var(--line); border-radius: 5px;
  background: var(--surface); color: var(--text);
  display: flex; flex-direction: column; overflow: hidden;
}
#libraryDialog::backdrop { background: rgba(6, 9, 15, .72); }
#libraryDialog article { padding: 1.4rem 1.6rem; overflow-y: auto; flex: 1 1 auto; min-height: 0; }
#libraryDialog h2 { margin: 0 0 .8rem; font-size: var(--fs-lg); letter-spacing: .04em; }
.lib-group { margin: 1rem 0 0; }
.lib-group h3 {
  margin: 0 0 .35rem; font-size: var(--fs-xs); text-transform: uppercase;
  letter-spacing: .12em; color: var(--muted);
}
.lib-item {
  display: flex; gap: .5rem; align-items: baseline;
  padding: .3rem .4rem; border-bottom: 1px solid var(--line);
}
.lib-item:last-child { border-bottom: none; }
.lib-name { flex: 0 0 12rem; font-size: var(--fs-sm); }
.lib-spec { flex: 1; color: var(--brass-lit); font-family: var(--font-data); font-size: var(--fs-xs); }
.lib-src { flex: 2; color: var(--muted); font-size: var(--fs-xs); line-height: 1.4; }

/* Edit, on a library row. Small and last: the row is about the asset, not the button. */
.lib-edit {
  width: auto; flex: none; margin: 0; padding: .1rem .45rem;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted); font-size: var(--fs-xs);
}
.lib-edit:hover { color: var(--brass-lit); border-color: var(--brass); }

/* A dialog that could not go modal must still sit above the chrome.
   The timeline is z-index 10 and a non-modal <dialog> is in the normal stacking order, so a
   fallback-opened dialog was drawn UNDER the clock. Modal dialogs are in the top layer and
   ignore this entirely; it only matters on the path where showModal was unavailable. */
dialog[open]:not(:modal) { position: fixed; z-index: 100; inset: 0; margin: auto; }

/* The colour swatch needs an edge.
   It is an <input type="color"> on a near-black panel, so a dark colour -- and black is a
   perfectly reasonable choice for a ground station -- rendered as a hole in the row rather
   than a swatch. A thin light ring costs nothing and makes every colour visible, including
   the ones that are almost the background. */
input.obj-dot {
  border: 1px solid rgba(230, 234, 242, .45);
  border-radius: 50%;
  padding: 0;
  box-sizing: content-box;
}
input.obj-dot:hover { border-color: rgba(230, 234, 242, .8); }
input.obj-dot::-webkit-color-swatch-wrapper { padding: 0; }
input.obj-dot::-webkit-color-swatch { border: none; border-radius: 50%; }

/* A CLOSED DIALOG MUST DISAPPEAR.
   The browser hides one with `dialog:not([open]) { display: none }`, but every dialog here
   sets `display: flex` on an ID selector to get its column layout -- and an ID beats a type
   selector, so the rule was overridden and closed dialogs stayed on screen. The backdrop went
   away, because the dialog really had closed; only the box remained, which is exactly what
   "the transparency is removed but the panel stays visible" describes.

   This says it once, with enough weight to win against every id rule above, rather than
   adding [open] to seven of them and missing the eighth next time. */
dialog:not([open]) { display: none !important; }

/* An X in the corner of a dialog.
   Escape works and the action row has a button, but neither is where a hand goes first, and
   a modal with no visible way out reads as stuck -- which this one has already been accused
   of being, for a different reason. */
.dlg-x {
  position: absolute; top: .5rem; right: .6rem; z-index: 2;
  width: auto; margin: 0; padding: .1rem .4rem;
  background: none; border: 1px solid transparent; border-radius: 3px;
  color: var(--muted); font-size: var(--fs-lg); line-height: 1.2;
}
.dlg-x:hover { color: var(--text); border-color: var(--line); }
dialog { position: relative; }

/* Two dialogs scroll their own article, and a native scrollbar renders at the outer edge of
   that box -- flush with the dialog's own edge, exactly where .dlg-x's shared right:.6rem
   sits. First attempt shrank the article with margin instead, which cleared the overlap but
   left a bare gap between the content and a button now stranded past the scrollbar --
   reported directly as "the close button is now after the scrollbar". The content stays full
   width; only the button itself moves left of where the scrollbar renders, snug rather than
   gapped, which is the button's own footprint doing the work instead of empty margin. */
#libraryDialog .dlg-x, #vehicleDialog .dlg-x { right: 1.4rem; }

/* Save-as-component, on a part row. Beside the delete, and quieter than it: keeping a part
   is a smaller act than removing one. */
.part-save {
  width: auto; flex: none; margin: 0; padding: 0 .3rem; line-height: 1.2;
  background: none; border: 1px solid var(--line); border-radius: 3px; color: var(--muted);
  font-size: var(--fs-xs);
}
.part-save:hover { color: var(--brass-lit); border-color: var(--brass); }

.lib-group h3 { display: flex; align-items: center; gap: .5rem; }
.lib-new {
  width: auto; flex: none; margin: 0; padding: 0 .35rem; line-height: 1.3;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted); font-size: var(--fs-2xs); letter-spacing: 0; text-transform: none;
}
.lib-new:hover { color: var(--brass-lit); border-color: var(--brass); }
.lib-mine { color: var(--brass); font-style: normal; font-size: var(--fs-2xs); }

/* Mission phases. Each is a block, because a phase carries several decisions and a flat list
   of controls would not say which belong together. */
.phase {
  margin: .4rem 0; padding: .45rem .5rem;
  border: 1px solid var(--line); border-left: 3px solid var(--brass);
  border-radius: 3px;
}
.phase-head { display: flex; align-items: baseline; gap: .45rem; margin-bottom: .3rem; }
.phase-tag {
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .1em; color: var(--muted);
}
.phase-span { flex: 1; font-family: var(--font-data); font-size: var(--fs-xs); color: var(--brass-lit); }
.phase label { display: block; margin: .3rem 0 0; font-size: var(--fs-2xs); color: var(--muted); }
.phase-burn { margin-top: .45rem; padding-top: .4rem; border-top: 1px solid var(--line); }
.phase-burn > span {
  font-size: var(--fs-2xs); text-transform: uppercase; letter-spacing: .1em; color: var(--muted);
}
.phase-burn .hint { display: block; margin-top: .3rem; font-size: var(--fs-2xs); }

/* The contact entry is an <a>, not a button: it is a mailto, and making it look like a button
   would hide that it leaves the page. It still has to sit in the menu like everything else. */
#toolsMenu a.tm-item {
  display: block; padding: .4rem .55rem; border-radius: 3px;
  color: var(--text); text-decoration: none;
}
#toolsMenu a.tm-item:hover { background: rgba(201, 162, 39, .12); }

/* The [hidden] attribute has to win.
   Found by the UI suite, and it is the dialog fault a second time in a different place: the
   model-scale row is a .map-toggle, .map-toggle sets `display: block`, and an author display
   rule outranks the browser's own `[hidden] { display: none }`. So the script set hidden, was
   right to believe it had, and the control stayed on screen with vehicle models switched off.
   Same treatment as `dialog:not([open])` above -- state the browser's rule at author level so
   any future `.thing { display: … }` cannot quietly undo it. */
[hidden] { display: none !important; }

/* Camera controls, in the map bar beside 3D/2D.
   Compact because the bar is narrow and sits over the globe; legible because it is the only
   way to discover that the camera can be moved at all. */
#cameraControls {
  display: flex;
  align-items: stretch;      /* the select and the checkbox share a height, not a baseline */
  gap: 0;
  height: 28px;
  background: rgba(10, 14, 22, .82);
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: 4px;
  padding: .15rem .3rem;
}
#cameraControls select {
  background: transparent;
  color: var(--text);
  border: 0;
  border-right: 1px solid rgba(255, 255, 255, .14);
  font-size: var(--fs-sm);
  font-family: inherit;
  max-width: 12rem;
  padding: 0 .45rem;
  margin: 0;
  height: 100%;
}
/* The native dropdown paints its own popup, and an optgroup LABEL takes the system
   default -- which on this dark UI came out light-grey on white and was unreadable. Both
   the options and the group headings have to be told. */
#cameraControls select option { background: #0a0e16; color: #e8ecf3; }
#cameraControls select optgroup {
  background: #0a0e16;
  color: var(--brass);
  font-style: normal;
  font-weight: 600;
}

/* The waiting line over the globe. Unobtrusive, and gone the moment it is untrue.
   Parented into #timeline by app.js, so bottom:100% is relative to the TIMELINE's own box
   (its nearest positioned ancestor) -- sitting just above it rather than mid-globe, where it
   used to sit directly on top of whatever the 3D view was showing. Same anchoring #watermark
   uses, which is what makes it track the timeline growing taller when the chart dock opens,
   with nothing here measuring that height. */
/* The shared busy indicator. Sits in the map bar beside the camera control, because that
   is where the eye already is when a wait starts -- picking a body to follow is one of the
   things that triggers one.

   NOT A PROGRESS BAR. Only the ephemeris fetch can report real progress; propagation returns
   in one lump from a worker and a bar creeping across for it would be inventing information
   about work whose duration nothing here knows. A spinner says "busy", which is all that is
   actually known, and the text says what for.

   NOT MODAL. These waits stay interactive -- you can keep editing while the Grand Tour
   propagates -- and an overlay would make a long propagation read as broken rather than busy. */
#busy {
  display: flex;
  align-items: center;
  gap: .45rem;
  padding: .3rem .6rem;
  border-radius: 4px;
  background: rgba(10, 14, 22, .86);
  border: 1px solid rgba(201, 162, 39, .35);
  color: var(--text);
  font-size: var(--fs-sm);
  white-space: nowrap;
}
#busy[hidden] { display: none; }

.busy-spin {
  width: .8rem;
  height: .8rem;
  border: 2px solid rgba(201, 162, 39, .28);
  border-top-color: var(--accent, #C9A227);
  border-radius: 50%;
  animation: busy-spin 800ms linear infinite;
  flex: none;
}

@keyframes busy-spin { to { transform: rotate(360deg); } }

/* A spinner is decoration for anyone who has asked not to be moved; the text still says
   everything the spinner does. */
@media (prefers-reduced-motion: reduce) {
  .busy-spin { animation: none; border-top-color: rgba(201, 162, 39, .28); }
}

#globeNote {
  position: absolute;
  left: 50%;
  bottom: 100%;
  margin-bottom: 8px;
  transform: translateX(-50%);
  max-width: 26rem;
  text-align: center;
  padding: .5rem .8rem;
  background: rgba(10, 14, 22, .86);
  border: 1px solid rgba(201, 162, 39, .35);
  border-radius: 4px;
  color: var(--text);
  font-size: var(--fs-md);
  line-height: 1.4;
  pointer-events: none;
}

/* ------------------------------------------------------------------ the panel as a tree
   The panel had grown to eight stacked headings, all expanded, all the time: scenario,
   objects, four sections of settings for whichever object was selected, targets, results.
   Everything about everything at once, and the phase editor -- now the main authoring
   surface -- was somewhere down the third screen.

   A tree matches the shape of the data. Objects and stations ARE a list; orbit, vehicle,
   phases and drag ARE settings of one selected object, which is why they are its children
   rather than its siblings. */
#panel details.node {
  border: 1px solid rgba(255, 255, 255, .09);
  border-radius: 4px;
  margin: .4rem 0;
  background: rgba(255, 255, 255, .015);
}
#panel details.node > summary {
  display: flex;
  align-items: baseline;
  gap: .4rem;
  cursor: pointer;
  padding: .35rem .5rem;
  list-style: none;
  user-select: none;
}
#panel details.node > summary::-webkit-details-marker { display: none; }
/* The disclosure triangle, drawn rather than inherited, so it turns with the node and does
   not depend on a browser default that differs across three of them. */
#panel details.node > summary::before {
  content: '';
  width: 0; height: 0;
  border-left: 5px solid var(--muted);
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  transition: transform .12s ease;
  flex: none;
}
#panel details.node[open] > summary::before { transform: rotate(90deg); }
#panel details.node > summary:hover { background: rgba(201, 162, 39, .07); }
#panel .node-title {
  font-size: var(--fs-sm);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text);
}
#panel .node-sub {
  font-size: var(--fs-sm);
  color: var(--brass);
  font-family: var(--font-data);
  margin-left: auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 55%;
}
#panel .node-body { padding: 0 .5rem .5rem; }

/* A small action beside a node's own heading, not a full-width row inside its body -- the
   same relationship instr-head's "+" has to "Instruments". margin-left:auto pushes it to the
   far right of the summary's flex row even where there is no .node-sub already claiming that
   space via the same trick. */
/* THE ACTIONS ARE SIBLINGS OF THE SUMMARY, NOT CHILDREN OF IT.
   A <summary> is the disclosure control; a button inside one is a control inside a control,
   and the browser gives the focus to the summary alone -- so these were unreachable by
   keyboard and unannounced by assistive technology, which is what the audit found. Moving
   them out of the summary fixes that; positioning them over its right-hand end keeps the
   panel looking exactly as it did. The details element is the positioning context, and the
   row height is the summary's own padding, so the two cannot drift apart. */
/* The wrapper is the positioning context, NOT the details. Inside a closed <details> the
   actions are part of the hidden disclosure content -- they vanished with the section, which
   is wrong twice over: the "+" should work whether or not the section is expanded, and a
   closed details does not lay its children out where an absolute top:0 would put them. */
#panel .node-wrap { position: relative; }
#panel .node-actions {
  position: absolute; top: 0; right: .5rem;
  display: flex; gap: .3rem; align-items: center;
  /* The summary's own vertical padding rather than a height in rem: a fixed height has to be
     re-derived every time the summary's padding or font changes, and it was already one pixel
     out on the child nodes, which use a smaller padding. */
  padding: .35rem 0;
}
/* Marked on the row itself, not inferred from an ancestor: the actions are a SIBLING of the
   details now, so a "details.node.child .node-actions" selector stopped matching and the
   child rows silently went back to full size. A child heading is smaller, so its action is
   too -- measured, the full-size button stood 3 px proud of a 23 px child summary. */
#panel .node-actions.child { right: .4rem; padding: .2rem 0; }
#panel .node-actions.child .node-add { font-size: var(--fs-xs); line-height: 1.15; padding: 0 .3rem; }
/* Inside the actions row they no longer need the margin trick that pushed them right. */
#panel .node-actions .node-add { margin: 0; }

#panel .node-add {
  width: auto; flex: none; margin: 0 0 0 auto;
  padding: 0 .4rem; line-height: 1.5;
  background: none; border: 1px solid var(--line); border-radius: 3px;
  color: var(--muted); font-size: var(--fs-sm); font-weight: 600;
}
#panel .node-add:hover { color: var(--brass-lit); border-color: var(--brass); }

/* Children sit inside their parent and say so with a rule rather than an indent, which at
   this width would cost more than it explains. */
#panel details.node.child {
  border: 0;
  border-left: 1px solid rgba(255, 255, 255, .12);
  border-radius: 0;
  margin: .25rem 0 .25rem .25rem;
  background: none;
}
#panel details.node.child > summary { padding: .25rem .4rem; }
#panel details.node.child .node-title { font-size: var(--fs-xs); color: var(--muted); }
#panel details.node.child[open] .node-title { color: var(--text); }
#panel details.node.node-selected { border-color: rgba(201, 162, 39, .3); }

/* The headings inside a node are now the node's own name; the h2 that used to carry it is
   redundant wherever one survives. */
#panel details.node .node-body > h2:first-child { display: none; }

/* The "?" that holds a long explanation. Small, inline, and obviously a control -- the text
   behind it is worth reading once and not worth re-reading on every visit. */
.hint-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px; height: 15px;
  /* NEVER SQUASHED. The button sits in a flex row beside the prose it opens, and the default
     flex-shrink of 1 let a long paragraph compress it: measured at 0, 7 and 7.5 px across three
     of the fourteen on screen, which is why the selected state read as a sliver rather than a
     circle. width alone does not defend a flex item -- flex-basis and shrink decide, and both
     were left at their defaults. */
  flex: 0 0 auto;
  margin: .1rem 0 .2rem;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1;
  cursor: pointer;
}
.hint-toggle:hover { border-color: var(--brass); color: var(--brass); }
.hint-toggle.on { background: rgba(201, 162, 39, .16); border-color: var(--brass); color: var(--brass); }
.hint-folded { display: block; margin-top: .2rem; }

/* The propagator line, kept out of the tree so it survives every node being collapsed:
   the tool must always be able to say which physics produced the numbers. */
#modelFooter {
  border-top: 1px solid rgba(255, 255, 255, .09);
  margin-top: .6rem;
  padding-top: .5rem;
  font-size: var(--fs-sm);
  color: var(--muted);
}

/* A station has no orbit, vehicle or drag. Those children are hidden rather than left showing
   the last spacecraft's numbers -- which is exactly what they did. Mission phases stays: it
   already explains that phases need a spacecraft, which is a better answer than a gap. */
#nodeSelected.is-station > .node-body > details.node.child[data-node="orbit"],
#nodeSelected.is-station > .node-body > details.node.child[data-node="vehicle"],
#nodeSelected.is-station > .node-body > details.node.child[data-node="drag"] { display: none; }

/* The selected object's settings, sitting under its own row in the tree.
   Indented to the row's content, and without a heading: the row directly above already
   names the object, and repeating it was the two-places-one-truth split in miniature. */
#panel details.node.in-tree {
  margin: .1rem 0 .5rem 1.1rem;
  border-left: 2px solid rgba(201, 162, 39, .35);
  border-radius: 0;
}
#panel details.node.in-tree > summary { display: none; }
.add-row { gap: .3rem; }
.add-row .ghost-btn { flex: 1; }

/* Renaming happens where the name is, so the input takes the name's place exactly. */
.obj-rename {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-weight: 600;
  background: rgba(255, 255, 255, .06);
  border: 1px solid var(--brass);
  border-radius: 3px;
  color: var(--text);
  padding: .1rem .3rem;
}
/* Three add actions beside the heading. Spaced so they read as a set, not as one control. */
#objectsNode .node-add + .node-add { margin-left: .15rem; }

/* The body dialog wears the same clothes as the help sheet. It was inheriting the browser's
   default -- white on white, in a dark application -- because it was written as a bare
   <dialog> and every other one here is styled by id. */
/* A row in a catalogue: the name, then what it costs to choose it. Full width and stacked,
   because these are decisions with consequences, not chips -- a curated scenario replaces
   everything on screen, and the row has to have room to say so before it is clicked. */
.pick-row {
  display: block;
  width: 100%;
  text-align: left;
  padding: .5rem .6rem;
  margin-bottom: .35rem;
  border: 1px solid var(--line);
  border-radius: 4px;
  background: var(--ink);
  color: var(--text);
  cursor: pointer;
}
.pick-row:hover { border-color: var(--brass); background: var(--surface); }
.pick-row strong { display: block; font-size: var(--fs-md); font-weight: 500; }
.pick-row small {
  display: block;
  margin-top: .18rem;
  font-size: var(--fs-xs);
  font-family: var(--font-data);
  color: var(--muted);
}
.pick-row .pick-note { font-family: inherit; line-height: 1.45; }

/* THE RISK ROWS ARE NOT BUTTONS, so they must not look like one. They reuse .pick-row for
   its layout and then give back the affordances: no pointer, no hover lift. The risk list
   carries no orbital elements -- it is 2,185 objects of published risk, and only the two
   dozen in the orbit catalogue can actually be flown -- so a row that invited a click would
   be promising something the data cannot do. */
.risk-row { cursor: default; }
.risk-row:hover { border-color: var(--line); background: var(--ink); }

#scenarioDialog, #orbitDialog { max-width: 34rem; }
/* ONE SCROLLER, NOT TWO.
   The sheet scrolls past 82vh and the pick list scrolled past 48vh, so a long catalogue put
   two vertical scrollbars side by side -- the inner one for the list and the outer one for a
   dialog that was only tall because of the list. Reported as exactly that.
   The list is the one that should scroll: it keeps the heading, the Curated/From-a-file
   choice and the Close button where they are. So the dialog becomes a column that does not
   scroll, and the list takes whatever height is left. Scoped to these two rather than to
   .sheet, which several other dialogs rely on. */
#scenarioDialog, #orbitDialog { display: flex; flex-direction: column; overflow: hidden; }
/* The pick list is a grandchild -- it sits inside .add-pane -- so the pane is the flex item
   and has to pass the height down, or the list keeps its natural size and the dialog
   overflows exactly as before. */
#scenarioDialog > .add-pane, #orbitDialog > .add-pane {
  flex: 1 1 auto; min-height: 4rem; display: flex; flex-direction: column;
}
#scenarioPicks, #orbitPicks { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
/* A flex column stretches its items, and a full-width Close button is not what these say.
   The row keeps its own alignment. */
#scenarioDialog > .row, #orbitDialog > .row { flex: none; }
#scenarioDialog > h2, #orbitDialog > h2,
#scenarioDialog > .model, #orbitDialog > .model,
#scenarioDialog > .add-choice { flex: none; }
#openWarnNote { color: var(--brass-lit); }
#openWarnNote[hidden] { display: none; }

/* Whether what is on screen is still what was opened. A word, not an icon: "edited, not
   saved" is the whole of the state, and a dot would need a legend to say the same thing. */
#scenarioNote.dirty { color: var(--brass-lit); }
#saveScenario.urgent { border-color: var(--brass); color: var(--brass-lit); }
#scenarioBar { display: flex; gap: .4rem; margin-bottom: .3rem; }
#scenarioBar button { flex: 1; }

/* A disclosure toggle, not a heading -- it had no styling at all and fell back to the
   browser's plain body-text size, reading as prominent as the note above it rather than as
   the small "there is more, click to see it" it actually is. Reported directly. */
#aboutScenario { margin-bottom: .4rem; }
#aboutScenario summary {
  cursor: pointer;
  font-size: var(--fs-sm);
  color: var(--muted);
  margin: .15rem 0;
}
#aboutScenario summary:hover { color: var(--text); }

/* ---- the small dialogs, dressed once
 *
 * ONE RULE, NOT ONE PER DIALOG. This was written as a copy of the body dialog's block with a
 * different id on the front, and the copy is what broke: the scenario and orbit dialogs were
 * added later with only a max-width, inherited the user agent's white-on-white default, and
 * appeared as blank sheets in a dark application. A shared selector cannot go stale that way
 * -- a new dialog is dressed by being named here, and being named here is one line.
 *
 * The big two are deliberately not in this list: #helpDialog and #vehicleDialog are
 * full-height flex layouts with their own scrolling article, which is a different shape, not
 * a different skin. */
/* Bigger glyph, same box: the chevrons were legible but small beside Play's word, and the
   height is pinned above, so the font can grow without the button following it. */
/* #timeline .play-btn sets font-size and the step buttons carry BOTH classes, so a bare
   .step-btn rule loses on specificity -- measured at 12.8 px when it was meant to be 23. */
#timeline .step-btn { font-size: 1.45rem; padding: 0 .55rem; opacity: .85; }
#timeline .step-btn:hover { opacity: 1; }
/* The scene's own tooltip. Positioned at the cursor by the pick handler; pointer-events off
   so it can never sit between the mouse and the thing it is describing. */
#sceneTip {
  position: fixed; z-index: 60; pointer-events: none;
  max-width: 22rem; padding: .35rem .55rem;
  background: rgba(10, 14, 23, .92); border: 1px solid var(--line); border-radius: 4px;
  color: var(--text); font: .74rem/1.45 "IBM Plex Mono", monospace;
  white-space: pre-line;
}

.sheet {
  width: calc(100% - 2rem);
  max-width: 30rem;
  max-height: 82vh;
  padding: 1.2rem 1.4rem;
  border: 1px solid var(--line);
  border-radius: 5px;
  background: var(--surface);
  color: var(--text);
  overflow-y: auto;
}
.sheet::backdrop { background: rgba(6, 9, 15, .72); }
.sheet h2 { margin: 0 0 .9rem; font-size: var(--fs-lg); letter-spacing: .04em; }
.sheet .model { font-size: var(--fs-sm); line-height: 1.5; color: var(--muted); margin: 0 0 .8rem; }
.sheet .add-pane[hidden] { display: none; }
.sheet .row { display: flex; gap: .4rem; margin-top: .6rem; }
.sheet .row input { flex: 1; min-width: 0; }
.sheet .row button { width: auto; padding: .4rem .8rem; flex: none; }
.sheet label { display: block; margin: .6rem 0; font-size: var(--fs-sm); color: var(--muted); }
.sheet select, .sheet input[type=text], .sheet textarea {
  width: 100%;
  margin-top: .25rem;
  padding: .38rem .5rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--ink);
  color: var(--text);
  font: inherit;
  font-size: var(--fs-md);
}

/* The choice itself, above both panes: two radios on one line, because it is one question
   with two answers and stacking them would imply a sequence. */
.add-choice {
  display: flex;
  gap: 1.1rem;
  margin: 0 0 .9rem;
  padding-bottom: .8rem;
  border-bottom: 1px solid var(--line);
  font-size: var(--fs-md);
}
.add-choice label { display: inline-flex; align-items: center; gap: .35rem; cursor: pointer; }
.add-choice input { margin: 0; }

/* Only the widths differ, and only because the content does: a table of twelve physical
   constants and a list of seven scenarios both need more room than a name field. */
#bodyDialog { max-width: 32rem; }
#bodyDialog dl { margin: 0 0 .9rem; }
#bodyDialog .model {
  margin: .8rem 0 1rem;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding-top: .7rem;
}

/* The "i" beside a label: the same weight as the "?" in the header, so the two read as one
   kind of affordance rather than two separate inventions. */
.label-row { display: inline-flex; align-items: center; gap: .35rem; }
.info-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px; height: 15px;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .22);
  border-radius: 50%;
  background: transparent;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-style: italic;
  line-height: 1;
  cursor: pointer;
}
.info-btn:hover { border-color: var(--brass); color: var(--brass); }

/* The add actions sit at the far end of the heading, together.
   The base .node-add rule gives EVERY one of them `margin: 0 0 0 auto`, which was right when
   there was exactly one -- it pushed the lone "+" to the right. With three, each takes its
   own share of the free space and they spread across the whole summary. Only the FIRST should
   claim the gap; the rest sit against it, separated by the flex gap alone.
   My previous rule set auto on the first, which it already had, and changed nothing. */
#panel details.node > summary .node-add { margin-left: .15rem; }
#panel details.node > summary .node-add:first-of-type { margin-left: auto; }

/* The chevron on an object row. Sits before the grip, at the muted weight of the group rows'
   own markers so the two levels read as one tree rather than two conventions. */
.obj-row .obj-chev {
  order: -1;
  flex: none;
  width: .8rem;
  color: var(--muted);
  font-size: var(--fs-xs);
  line-height: 1;
  text-align: center;
  user-select: none;
}
.obj-row.selected .obj-chev { color: var(--brass); }

/* The vehicle name beside an object: context, not identity. Quieter and smaller than the
   name, and separated by real space -- run together at the same weight, "Orbit 1Point mass"
   read as one long name. */
.obj-name .obj-vehicle {
  margin-left: .35rem;
  color: var(--muted);
  font-size: var(--fs-xs);
  font-weight: 400;
}

/* What just happened, said in the dialog the user is looking at. These dialogs are modal and
   cover the globe, so the usual globeNote would post the confirmation behind the thing being
   read. Success and refusal must not look the same: a save that silently failed is worse than
   one that never claimed to work. */
.save-note {
  display: block;
  margin: .6rem 0 0;
  padding: .45rem .6rem;
  border-left: 2px solid var(--brass);
  background: rgba(201, 162, 39, .09);
  color: var(--text);
  font-size: var(--fs-sm);
  line-height: 1.45;
}
.save-note[hidden] { display: none; }
.save-note.bad { border-left-color: #E4572E; background: rgba(228, 87, 46, .10); }

/* Library groups fold. Five component kinds and two vehicle lists is a page you scroll past
   to reach the one you wanted. */
details.lib-group > summary {
  display: flex;
  align-items: center;
  gap: .5rem;
  margin: 1rem 0 .4rem;
  padding: .25rem 0;
  border-bottom: 1px solid var(--line);
  cursor: pointer;
  list-style: none;
}
details.lib-group > summary::-webkit-details-marker { display: none; }
/* The triangle is drawn rather than inherited, so it turns with the group in every browser
   instead of only the ones that style the native marker. */
details.lib-group > summary::before {
  content: '\25B8';
  color: var(--muted);
  font-size: var(--fs-xs);
  transition: transform .12s ease;
}
details.lib-group[open] > summary::before { transform: rotate(90deg); }
details.lib-group > summary .lib-h {
  font-size: var(--fs-md);
  font-weight: 500;
  letter-spacing: .03em;
  /* Amber, not plain text colour -- asked directly, so a group name reads apart from the
     asset rows under it at a glance rather than blending in. */
  color: var(--brass-lit);
}
details.lib-group > summary .lib-count {
  color: var(--muted);
  font-family: var(--font-data);
  font-size: var(--fs-xs);
}
details.lib-group > summary .lib-new { margin-left: auto; }

/* A heading inside a catalogue, separating the shipped library from the user's own. Not an
   <h3>: these sit between rows in a scrolling list, and a heading's default margins would
   make the two groups look like two dialogs. */
.pick-head {
  margin: .7rem 0 .3rem;
  font-size: var(--fs-xs);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
}
.pick-head:first-child { margin-top: 0; }

/* The row for the thing already attached. The select this replaces showed the current vehicle
   by being set to it; a list of rows has to say so out loud or choosing again looks like the
   only way to find out. */
.pick-row.current { border-color: var(--brass); background: var(--surface); }
.pick-row.current strong { color: var(--brass-lit); }

/* What is attached, in the panel, where the select used to be. */
.val-line {
  display: block;
  padding: .35rem 0;
  font-size: var(--fs-md);
  color: var(--text);
}

/* The coverage block belongs to a ground object. A spacecraft's own coverage is its swath —
   a different question with a different picture — so the node is simply absent rather than
   empty, the same rule the orbit and vehicle nodes follow for a station. */
#nodeSelected:not(.is-station) > .node-body > details.node.child[data-node="coverage"] {
  display: none;
}

/* Term and figure, aligned. A definition list rather than a table because these are seven
   named quantities, not rows of one kind of thing. */
.fom {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: .3rem .8rem;
  margin: 0 0 .6rem;
  font-size: var(--fs-sm);
}
.fom dt { color: var(--muted); }
.fom dd {
  margin: 0;
  text-align: right;
  font-family: var(--font-data);
  color: var(--text);
  cursor: help;
}

/* The legend for whatever the orbits are coloured by. Inline chips rather than a table: it
   sits under a select in a popover, and four rows of anything would push the rest of the
   layers panel off the bottom. */
.legend { display: flex; flex-wrap: wrap; gap: .3rem .7rem; margin: .4rem 0 .2rem; }
.legend-key {
  display: inline-flex;
  align-items: center;
  gap: .3rem;
  font-size: var(--fs-xs);
  color: var(--muted);
}
.legend-key i {
  width: 10px;
  height: 3px;
  border-radius: 1px;
  /* A bar, not a dot: it stands for a stretch of orbit, and that is what it looks like. */
  display: inline-block;
}

/* The badge switches. A fieldset because they are one question with four independent answers,
   and a legend says so without a paragraph. */
#badgeSet {
  margin: .5rem 0;
  padding: .5rem .6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
}
#badgeSet legend { padding: 0 .3rem; font-size: var(--fs-xs); color: var(--muted); }
label.chk {
  display: flex;
  align-items: center;
  gap: .4rem;
  margin: .2rem 0;
  font-size: var(--fs-sm);
  color: var(--text);
  cursor: pointer;
}
label.chk input { margin: 0; }

/* The glyph on a badge switch, and in the key. Monospace so ▲▼ and ☀☾ occupy the same width
   they do in the label on the globe — a key whose symbols are a different shape from the ones
   being decoded is doing half its job. */
.gly {
  font-family: var(--font-data);
  font-weight: 400;
  color: var(--brass-lit);
  letter-spacing: .05em;
}
label.chk .gly { min-width: 2.4em; display: inline-block; text-align: center; }

/* The link node belongs to a ground object, like Coverage. A spacecraft's own link is this
   one seen from the other end, not a second thing to show. */
#nodeSelected:not(.is-station) > .node-body > details.node.child[data-node="link"] {
  display: none;
}
#linkNote.dirty { color: var(--brass-lit); }

/* The data budget belongs to a spacecraft, the mirror of the link node's belonging to a
   ground object: a station does not produce, store or downlink anything of its own. */
#nodeSelected.is-station > .node-body > details.node.child[data-node="data"] {
  display: none;
}
#dataNote.dirty { color: var(--brass-lit); }

/* The lines get their own group. A line between an object and a ground station is not a badge
   beside an object, and filing it under "Badges" is exactly how it went unnoticed. */
#lineSet {
  margin: .5rem 0;
  padding: .5rem .6rem;
  border: 1px solid var(--line);
  border-radius: 4px;
}
#lineSet legend { padding: 0 .3rem; font-size: var(--fs-xs); color: var(--muted); }

/* The delta-V budget belongs to a spacecraft, like the data budget: a ground object has no
   manoeuvres and no tank. */
#nodeSelected.is-station > .node-body > details.node.child[data-node="deltav"] {
  display: none;
}
#dvNote.dirty { color: var(--brass-lit); }

/* The 2029 encounter, drawn to scale (§8r.41). Colours come from the existing custom properties
   so this follows the theme rather than pinning its own palette. */
.enc-plot { margin: .5rem 0; }
.enc-svg { width: 100%; height: auto; display: block; }
.enc-earth { fill: var(--accent, #4b9fd5); opacity: .85; }
.enc-geo { stroke: var(--muted, #888); stroke-width: .8; stroke-dasharray: 2 2; opacity: .7; }
.enc-va { stroke: var(--fg, #ddd); stroke-width: 1.2; }
.enc-axis { stroke: var(--muted, #888); stroke-width: .6; opacity: .5; }
.enc-span { stroke: var(--fg, #ddd); stroke-width: .8; opacity: .45; }
/* The nominal is the answer; the others are the uncertainty around it, so it reads differently. */
.enc-va-nominal { stroke: var(--warn, #e0a030); stroke-width: 2.5; }
.enc-scale { stroke: var(--fg, #ddd); stroke-width: 1.5; opacity: .7; }
/* 7px in a 240-wide viewBox that renders at about its own size, so this is 7px on screen. */
.enc-label { fill: var(--muted, #999); font-size: 7px; text-anchor: middle; }
.enc-geo-label { font-size: 6.5px; }
.enc-nominal-label { fill: var(--warn, #e0a030); font-size: 8px; }
.enc-earth-label { fill: var(--fg, #ddd); }
