/* ============================================================
   VOID ORBITAL — HELIOS
   Design tokens lifted from the Void Orbital house style (Odyssey) and kept
   byte-identical to Atlas's, because the two products sit side by side under
   Noesis and a user reading one should not have to relearn the other:
   warm "aether" sand on near-black void, DM Serif Display for titles and
   headline figures, monospace for all data, wide-tracked uppercase
   micro-labels for structure. No emoji anywhere.
   ============================================================ */

:root {
  /* surfaces */
  --void: #0A0A0A;
  --panel: rgba(12, 12, 12, 0.86);
  --panel-solid: #0C0C0C;
  --inset: rgba(22, 22, 22, 0.9);
  --recess: #060606;

  /* aether — the house accent */
  --aether: #C8B8A2;
  --aether-bright: #E3D5BE;
  --aether-dim: rgba(200, 184, 162, 0.45);
  --aether-faint: rgba(200, 184, 162, 0.16);
  --aether-ghost: rgba(200, 184, 162, 0.08);

  /* ink */
  --white: #F5F5F3;
  --white-dim: rgba(245, 245, 243, 0.55);
  --white-faint: rgba(245, 245, 243, 0.28);

  /* semantics — kept inside the warm family so the instrument still
     reads as one system. aether doubles as "good". */
  --ok: #C8B8A2;
  --caution: #C99A6B;
  --alarm: #C4614A;

  /* type */
  --serif: 'DM Serif Display', Georgia, serif;
  --mono: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
  --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --radius: 4px;
  --radius-lg: 8px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--void);
  color: var(--white);
  font-family: var(--sans);
  font-size: 14px;
  -webkit-font-smoothing: antialiased;
}

body { overflow: hidden; }

a { color: var(--aether); }

::selection { background: var(--aether-dim); color: var(--void); }

::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--aether-faint); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: var(--aether-dim); }

:focus-visible { outline: 1px solid var(--aether-dim); outline-offset: 2px; }

button, select, input, textarea { font-family: inherit; color: inherit; }

/* Utility escape hatch for JS that toggles visibility by class rather than
   by inline style. */
.hidden { display: none !important; }

/* ---- shared micro-typography ---- */

/* The house micro-label: 10px, wide-tracked, uppercase, recessive. Used both
   as a standalone caption and as the wrapper for a labelled input, so it must
   not leak its tracking or casing into the control it contains. */
.micro {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white-faint);
  line-height: 1.5;
}

label.micro { display: block; }

.mono { font-family: var(--mono); }

.val {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: none;
  color: var(--aether);
}

.hint {
  font-size: 10px;
  color: var(--white-faint);
  line-height: 1.55;
  margin-top: 6px;
}

.note {
  font-size: 10.5px;
  color: var(--white-faint);
  line-height: 1.6;
  margin-top: 8px;
}

/* generic "nothing here yet" placeholder */
.empty {
  font-size: 11px;
  color: var(--white-faint);
  line-height: 1.6;
}

/* ============================================================
   Startup failure banner
   ============================================================ */

.boot-error {
  position: fixed;
  inset: 0;
  z-index: 9999;
  background: rgba(10, 10, 10, 0.97);
  color: var(--white);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 16px;
  padding: 40px;
  text-align: center;
}

.boot-error .boot-title {
  font-family: var(--serif);
  font-size: 30px;
  color: var(--alarm);
}

.boot-error .boot-detail {
  font-size: 14px;
  line-height: 1.6;
  max-width: 620px;
  color: var(--white-dim);
}

/* pre-wrap is load-bearing: the fix text is a shell transcript with newlines
   and a leading indent, and it is meant to be selected and pasted whole. */
.boot-error .boot-fix {
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.7;
  color: var(--aether);
  background: var(--recess);
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  padding: 12px 16px;
  max-width: 760px;
  white-space: pre-wrap;
  word-break: break-word;
  text-align: left;
  user-select: all;
}

.boot-error .boot-fix:empty { display: none; }

/* ============================================================
   App shell
   ============================================================ */

/* Only one .main is ever displayed, so the shell always has exactly three
   grid items: topbar, the live tab, statusbar. minmax(0,1fr) rather than 1fr
   so the middle row can actually shrink and hand scrolling to its own rails. */
.app-shell {
  display: grid;
  grid-template-rows: 56px minmax(0, 1fr) 24px;
  height: 100vh;
  width: 100vw;
}

/* ---- Top bar: the Void Orbital lockup ---- */

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 20px;
  background: linear-gradient(to bottom, rgba(10, 10, 10, 0.96), rgba(10, 10, 10, 0.82));
  border-bottom: 1px solid var(--aether-faint);
  min-width: 0;
}

/* wordmark links home to voidorbital.com; keeps the topbar's own 14px rhythm
   so wrapping the mark in an anchor does not shift anything */
.brand {
  display: inline-flex;
  align-items: center;
  gap: 14px;
  text-decoration: none;
  color: inherit;
  flex: 0 0 auto;
  border-radius: var(--radius);
  transition: opacity 0.18s;
}

.brand:hover { opacity: 0.72; }

.brand:focus-visible {
  outline: 1px solid var(--aether-dim);
  outline-offset: 4px;
}

/* animated beacon mark — same motion as the Odyssey vehicle marker */
.mark {
  position: relative;
  width: 26px;
  height: 26px;
  flex: 0 0 auto;
}

.mark .core {
  position: absolute;
  inset: 4px;
  border-radius: 50%;
  background: #000;
  box-shadow: 0 0 0 1.5px var(--aether),
              0 0 14px 2px rgba(200, 184, 162, 0.35),
              0 0 34px 6px rgba(200, 184, 162, 0.14);
  animation: breathe 4.2s ease-in-out infinite;
}

.mark .ring {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  border: 1px solid var(--aether);
  opacity: 0;
  animation: beacon 3.6s cubic-bezier(.22, .61, .36, 1) infinite;
}

/* Inert as written, and kept that way to match index.html: with the .mark
   children ordered ring, ring, core, the 3rd child is .core, so this selects
   nothing and both rings pulse in sync. The 1.8s stagger it was clearly meant
   to produce would need :nth-child(2) — changing it here alone would desync
   Helios from every other page. */
.mark .ring:nth-child(3) { animation-delay: 1.8s; }

@keyframes beacon {
  0%   { transform: scale(0.45); opacity: 0.75; }
  100% { transform: scale(2.1);  opacity: 0; }
}

@keyframes breathe {
  0%, 100% { box-shadow: 0 0 0 1.5px var(--aether), 0 0 12px 2px rgba(200, 184, 162, 0.28), 0 0 30px 5px rgba(200, 184, 162, 0.10); }
  50%      { box-shadow: 0 0 0 1.5px var(--aether-bright), 0 0 18px 4px rgba(200, 184, 162, 0.5), 0 0 44px 10px rgba(200, 184, 162, 0.2); }
}

.wordmark {
  font-family: var(--sans);
  font-size: 12px;
  letter-spacing: 0.34em;
  color: var(--white);
  font-weight: 600;
  white-space: nowrap;
}

.divider {
  width: 1px;
  height: 18px;
  background: var(--aether-faint);
  flex: 0 0 auto;
}

.prodname {
  font-family: var(--serif);
  font-size: 20px;
  color: var(--aether);
  letter-spacing: 0.02em;
  white-space: nowrap;
}

.topbar-status {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--white-faint);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  letter-spacing: 0.04em;
  min-width: 0;
}

.topbar-status .sep { color: var(--aether-faint); }
.topbar-status .val { color: var(--aether); }

.topbar-spacer { flex: 1; }

/* ---- product tabs (house segmented control, topbar variant) ---- */

.tabs {
  display: flex;
  gap: 4px;
  flex: 0 0 auto;
}

.tab {
  background: none;
  text-align: center;
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--white-faint);
  padding: 7px 12px;
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.tab:hover { color: var(--white-dim); }

.tab.active {
  color: var(--aether);
  background: var(--aether-faint);
  border-color: var(--aether-dim);
}

/* ============================================================
   Buttons
   ============================================================ */

.btn {
  background: none;
  border: 1px solid var(--aether-dim);
  color: var(--aether);
  font-family: var(--sans);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  padding: 8px 14px;
  border-radius: var(--radius);
  cursor: pointer;
  white-space: nowrap;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}

.btn:hover:not(:disabled) { background: var(--aether-faint); }

.btn:disabled {
  border-color: var(--aether-faint);
  color: var(--white-faint);
  cursor: not-allowed;
}

.btn-ghost {
  border-color: var(--aether-faint);
  color: var(--white-dim);
}

.btn-ghost:hover:not(:disabled) {
  border-color: var(--aether-dim);
  color: var(--aether);
}

/* the one filled button in the system — reserved for the committing action
   of a step (apply, solve). Still a wash of aether, never a slab of colour. */
.btn-solid {
  background: var(--aether-faint);
  border-color: var(--aether);
  color: var(--aether-bright);
}

.btn-solid:hover:not(:disabled) {
  background: rgba(200, 184, 162, 0.24);
  border-color: var(--aether-bright);
}

.btn-solid:disabled {
  background: none;
  border-color: var(--aether-faint);
  color: var(--white-faint);
}

.btn.small {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  padding: 5px 9px;
}

/* shared width modifier — used by .btn and .select alike */
.wide {
  display: block;
  width: 100%;
}

/* ============================================================
   Main three-pane layout
   ============================================================ */

.main {
  display: grid;
  grid-template-columns: 330px minmax(0, 1fr) 330px;
  grid-template-rows: minmax(0, 1fr);
  overflow: hidden;
  min-height: 0;
}

.rail {
  background: var(--panel-solid);
  overflow-y: auto;
  overflow-x: hidden;
  min-height: 0;
  min-width: 0;
}

/* ---- Left rail: the numbered pipeline ---- */

.rail-steps {
  border-right: 1px solid var(--aether-faint);
  padding-bottom: 40px;
}

.step {
  border-bottom: 1px solid var(--aether-faint);
  position: relative;
}

.step::before {
  content: '';
  position: absolute;
  left: 0; top: 0; bottom: 0;
  width: 2px;
  background: transparent;
  transition: background 0.15s;
}

.step[data-state="active"]::before { background: var(--aether); }
.step[data-state="done"]::before { background: var(--aether-dim); }

.step-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  cursor: pointer;
  user-select: none;
}

.step-head:hover .step-title { color: var(--white-dim); }

.step-num {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--white-faint);
  width: 18px;
  flex: 0 0 auto;
  letter-spacing: 0.04em;
}

.step[data-state="active"] .step-num { color: var(--aether); }
.step[data-state="done"] .step-num { color: var(--aether-dim); }

.step-title {
  flex: 1;
  min-width: 0;
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white-faint);
}

.step[data-state="active"] .step-title { color: var(--aether); }
.step[data-state="done"] .step-title { color: var(--white-dim); }

/* Every step body is OPEN by default and collapsing is opt-in, driven by a
   .collapsed class the user toggles from the header.
   
   This was an accordion keyed to [data-state="active"], which meant progress
   states controlled VISIBILITY: marking a step "done" hid its contents. Because
   only one step can be active, the Material panel (set to "done" as soon as a
   material was read) and the Solve panel (never set active at all) could not be
   opened by any means — while .step-head advertised `cursor: pointer` for a
   handler that did not exist. Progress state now drives colour only.
   
   The 44px left inset keeps every control aligned under the step title rather
   than under its number. */
.step-body {
  padding: 0 16px 18px 44px;
  display: block;
}

.step.collapsed .step-body { display: none; }

/* the header's own open/closed affordance, so the pointer cursor is honest */
.step-head::after {
  content: '–';
  font-family: var(--mono);
  font-size: 13px;
  color: var(--white-faint);
  flex: 0 0 auto;
}

.step.collapsed .step-head::after { content: '+'; color: var(--aether-dim); }
.step-head:hover::after { color: var(--aether); }

/* ============================================================
   Form controls
   ============================================================ */

.row {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 12px;
}

.btn-row {
  flex-direction: row;
  flex-wrap: wrap;
  gap: 6px;
}

.btn-row .btn {
  flex: 1 1 auto;
  padding: 7px 10px;
  font-size: 10px;
  letter-spacing: 0.12em;
}

.select {
  width: 100%;
  background-color: var(--inset);
  border: 1px solid var(--aether-faint);
  color: var(--white);
  font-family: var(--mono);
  font-size: 12px;
  padding: 7px 26px 7px 8px;
  border-radius: var(--radius);
  outline: none;
  cursor: pointer;
  /* native select chrome is light-mode grey in most browsers; draw our own
     caret out of two gradients instead */
  -webkit-appearance: none;
  appearance: none;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--aether-dim) 50%),
    linear-gradient(135deg, var(--aether-dim) 50%, transparent 50%);
  background-position: calc(100% - 14px) calc(50% - 1px), calc(100% - 10px) calc(50% - 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
}

.select:hover { border-color: var(--aether-dim); }
.select:focus { border-color: var(--aether-dim); }

.select option {
  background: var(--panel-solid);
  color: var(--white);
}

.num,
input[type="number"],
input[type="text"] {
  display: block;
  width: 100%;
  margin-top: 5px;
  background: var(--inset);
  border: 1px solid var(--aether-faint);
  color: var(--white);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.02em;
  text-transform: none;
  padding: 7px 8px;
  border-radius: var(--radius);
  outline: none;
}

input[type="number"]:focus,
input[type="text"]:focus { border-color: var(--aether-dim); }

/* labelled-input grid for the custom material fields */
.custom-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--aether-faint);
}

/* ---- sliders: 2px hairline track, 12px round aether thumb ---- */

input[type=range] {
  width: 100%;
  -webkit-appearance: none;
  appearance: none;
  height: 2px;
  background: var(--aether-faint);
  border-radius: 2px;
  outline: none;
  margin: 8px 0 2px;
}

input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--aether);
  cursor: pointer;
}

input[type=range]::-moz-range-thumb {
  width: 12px; height: 12px;
  border: none;
  border-radius: 50%;
  background: var(--aether);
  cursor: pointer;
}

.slider-row { margin-top: 14px; }

.slider-row > label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}

.slider-row.compact { margin-top: 8px; }
.slider-row.compact input[type=range] { margin: 6px 0 0; }

/* ============================================================
   Ledger — the house key/value idiom
   ============================================================ */

.ledger {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 0 12px;
  margin: 12px 0 0;
}

.ledger:empty { display: none; }

.ledger dt,
.ledger dd {
  padding: 5px 0;
  border-bottom: 1px solid var(--aether-ghost);
  align-self: baseline;
}

.ledger dt {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white-faint);
  overflow: hidden;
  text-overflow: ellipsis;
}

.ledger dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--aether);
  text-align: right;
  white-space: nowrap;
}

.ledger dt:last-of-type,
.ledger dd:last-of-type { border-bottom: none; }

.ledger dd.warn { color: var(--caution); }
.ledger dd.danger { color: var(--alarm); }

/* Fallback shape: if the renderer emits one wrapper element per row instead
   of a flat dt/dd sequence, the rows still read as a ledger. */
.ledger > div {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 5px 0;
  border-bottom: 1px solid var(--aether-ghost);
}

.ledger > div dt,
.ledger > div dd { padding: 0; border-bottom: none; }

/* ============================================================
   Step 1 — dropzone
   ============================================================ */

.dropzone {
  border: 1px dashed var(--aether-faint);
  border-radius: var(--radius);
  padding: 22px 12px;
  text-align: center;
  color: var(--white-faint);
  cursor: pointer;
  transition: border-color 0.15s, color 0.15s, background 0.15s;
}

.dropzone:hover,
.dropzone:focus-visible,
.dropzone.dragover,
.dropzone.drag-over {
  border-color: var(--aether-dim);
  background: var(--aether-ghost);
}

.dz-title {
  font-family: var(--serif);
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0.01em;
  color: var(--aether);
  margin-bottom: 5px;
}

.dz-sub {
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--white-faint);
}

.dropzone.dragover .dz-sub,
.dropzone.drag-over .dz-sub { color: var(--aether-dim); }

/* ============================================================
   Step 3 — face list and boundary-condition editor
   ============================================================ */

.face-list {
  margin-top: 12px;
  max-height: 210px;
  overflow-y: auto;
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  background: var(--recess);
}

.face-list:empty { display: none; }

/* One row per segmented face: name, area, assigned BC tag. The row selector
   is deliberately tag-agnostic (div or button) so the list renderer can use
   whichever it likes. */
.face-list > *,
.face-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 5px 9px;
  border: none;
  border-bottom: 1px solid var(--aether-ghost);
  border-left: 2px solid transparent;
  background: none;
  font-size: 11px;
  color: var(--white-dim);
  cursor: pointer;
  user-select: none;
}

.face-list > *:last-child { border-bottom: none; }

/* a placeholder dropped into the list is a message, not a clickable row */
.face-list > .empty,
.face-list > .hint {
  display: block;
  padding: 8px 9px;
  cursor: default;
  border-left-color: transparent;
}

.face-list > .empty:hover,
.face-list > .hint:hover { background: none; }

.face-list > *:hover,
.face-row:hover { background: var(--aether-ghost); color: var(--white); }

.face-list > .selected,
.face-row.selected {
  background: var(--aether-faint);
  border-left-color: var(--aether);
  color: var(--white);
}

/* first child is the face name whatever it is called */
.face-list > * > :first-child,
.face-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.face-list .face-area,
.face-list .area {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--white-faint);
  white-space: nowrap;
}

/* the assigned boundary condition, as a small tag */
.face-list .tag,
.face-list .bc-tag,
.face-list .face-bc,
.bc-tag {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--aether);
  background: var(--aether-ghost);
  border: 1px solid var(--aether-faint);
  border-radius: 3px;
  padding: 1px 5px;
  white-space: nowrap;
}

/* BC kinds stay inside the warm family; they differ in weight, not in hue */
.bc-tag[data-bc="none"],
.face-list [data-bc="none"] { color: var(--white-faint); background: none; }
.bc-tag[data-bc="flux"],
.face-list [data-bc="flux"] { color: var(--caution); }
.bc-tag[data-bc="radiation"],
.face-list [data-bc="radiation"] { color: var(--caution); }
.bc-tag[data-bc="convection"],
.face-list [data-bc="convection"] { color: var(--white-dim); }

.bc-editor {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--aether-faint);
}

.bc-fields { margin-top: 10px; }

.bc-fields > label { display: block; margin-bottom: 9px; }
.bc-fields > label:last-child { margin-bottom: 0; }
.bc-fields > .btn { margin-top: 2px; }

.bc-editor > .btn { margin-top: 12px; }

/* ---- segmented control ---- */

.pillrow {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
}

.pill {
  flex: 1 1 auto;
  background: none;
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  color: var(--white-faint);
  font-family: var(--sans);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 7px 6px;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

.pill:hover { color: var(--white-dim); }

.pill.active {
  background: var(--aether-faint);
  color: var(--aether);
  border-color: var(--aether-dim);
}

/* ============================================================
   Step 4 — solve log
   ============================================================ */

.log {
  margin: 12px 0 0;
  max-height: 150px;
  overflow-y: auto;
  background: var(--recess);
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  padding: 9px 10px;
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1.65;
  color: var(--white-dim);
  white-space: pre-wrap;
  word-break: break-word;
}

.log:empty { display: none; }

/* ============================================================
   Centre viewport
   ============================================================ */

.viewport {
  position: relative;
  background: var(--void);
  overflow: hidden;
  min-width: 0;
  min-height: 0;
}

/* touch-action:none is load-bearing — without it a vertical drag on a
   touchscreen or trackpad scrolls the page instead of reaching OrbitControls,
   so the part appears to only rotate horizontally. */
#viewport-canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* Overlays float over the canvas, so by default they must not eat orbit
   drags; only their interactive children opt back in. */
.vp-overlay {
  position: absolute;
  display: flex;
  gap: 6px;
  pointer-events: none;
  z-index: 2;
}

.vp-overlay > * { pointer-events: auto; }

.vp-topleft { top: 14px; left: 14px; flex-direction: column; align-items: flex-start; }
.vp-topright { top: 14px; right: 14px; flex-direction: row; align-items: flex-start; }

.chip {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  color: var(--white-dim);
  background: var(--panel);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius-lg);
  padding: 6px 11px;
  max-width: 340px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  pointer-events: none;
}

.chip:empty { display: none; }

.chip .k,
.chip .val { color: var(--aether); }

.vp-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: 24px;
  pointer-events: none;
  z-index: 1;
}

.vp-empty-title {
  font-family: var(--serif);
  font-size: 22px;
  color: var(--white-dim);
}

.vp-empty-sub {
  font-size: 11.5px;
  line-height: 1.6;
  color: var(--white-faint);
  max-width: 320px;
}

/* ---- temperature legend ----
   The strip gradient must stay identical to colormap.js's RAMP_STOPS, or the
   scale quietly disagrees with the part it is labelling. Stops:
   #1C2A33 0%, #4E6B72 26%, #C8B8A2 58%, #C99A6B 82%, #C4614A 100%. */

.legend {
  position: absolute;
  top: 24px;
  right: 24px;
  bottom: 24px;
  width: 116px;
  pointer-events: none;
  z-index: 2;
}

.legend-strip {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 64px;
  width: 16px;
  border: 1px solid var(--aether-faint);
  border-radius: 2px;
  background: linear-gradient(to top,
    #1C2A33 0%,
    #4E6B72 26%,
    #C8B8A2 58%,
    #C99A6B 82%,
    #C4614A 100%);
}

/* Ticks are absolutely positioned by JS, which sets `bottom` as a percentage
   of the strip from colormap.legendTicks(). This box therefore has to span
   exactly the strip's vertical extent. */
.legend-ticks {
  position: absolute;
  left: 24px;
  right: 0;
  top: 0;
  bottom: 64px;
}

.legend-ticks > * {
  position: absolute;
  left: 0;
  transform: translateY(50%);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--white-dim);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

/* short leader from the strip to the number */
.legend-ticks > *::before {
  content: '';
  position: absolute;
  left: -7px;
  top: 50%;
  width: 5px;
  height: 1px;
  background: var(--aether-faint);
}

.legend-unit {
  position: absolute;
  left: 0;
  bottom: 42px;
  color: var(--aether-dim);
}

.legend .slider-row.compact {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  margin: 0;
  pointer-events: auto;
}

.legend .slider-row.compact label { color: var(--aether-dim); }

/* ============================================================
   Right rail: results
   ============================================================ */

.rail-results {
  border-left: 1px solid var(--aether-faint);
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 14px;
}

.panel {
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  background: var(--panel);
  flex: 0 0 auto;
}

.panel-head {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--aether-dim);
  padding: 10px 12px;
  border-bottom: 1px solid var(--aether-faint);
}

.panel-head.collapsible {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  cursor: pointer;
  user-select: none;
}

.panel-head.collapsible:hover { color: var(--aether); }

.chev {
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1;
  color: var(--white-faint);
}

.panel-head.collapsible:hover .chev { color: var(--aether); }

.panel-body { padding: 12px; }

/* headline figure — the one place a number is set in the serif */
.headline {
  font-family: var(--serif);
  font-size: 34px;
  line-height: 1.05;
  color: var(--aether);
  letter-spacing: 0.01em;
}

.headline.warn { color: var(--caution); }
.headline.danger { color: var(--alarm); }

.headline-sub {
  margin-top: 4px;
  color: var(--white-faint);
}

/* verdict band — a judgement, not a number */
.verdict {
  margin-top: 12px;
  padding: 9px 11px;
  border: 1px solid var(--aether-faint);
  border-left-width: 2px;
  border-radius: var(--radius);
  background: var(--aether-ghost);
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--white-dim);
}

.verdict.safe {
  border-left-color: var(--ok);
  color: var(--aether);
}

.verdict.warn {
  border-left-color: var(--caution);
  color: var(--caution);
  background: rgba(201, 154, 107, 0.08);
}

.verdict.danger {
  border-left-color: var(--alarm);
  color: var(--alarm);
  background: rgba(196, 97, 74, 0.08);
}

/* ---- heat flow by face ---- */

.face-heat > div,
.face-heat-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 5px 0;
  border-bottom: 1px solid var(--aether-ghost);
  font-size: 11px;
  color: var(--white-dim);
}

.face-heat > div:last-child,
.face-heat-row:last-child { border-bottom: none; }

.face-heat > div > :first-child {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.face-heat .v,
.face-heat .value,
.face-heat > div > :last-child {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--aether);
  white-space: nowrap;
  text-align: right;
}

/* heat in versus heat out, still inside the warm family */
.face-heat .in { color: var(--caution); }
.face-heat .out { color: var(--aether); }

.face-heat > .empty { border-bottom: none; }

/* ---- notes ---- */

.warn-list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.warn-list li {
  position: relative;
  padding-left: 14px;
  font-size: 11px;
  line-height: 1.55;
  color: var(--white-dim);
}

.warn-list li::before {
  content: '—';
  position: absolute;
  left: 0;
  color: var(--aether-dim);
}

.warn-list li.warn { color: var(--caution); }
.warn-list li.warn::before { color: var(--caution); }
.warn-list li.danger { color: var(--alarm); }
.warn-list li.danger::before { color: var(--alarm); }

/* ---- calculators ---- */

#calc-form {
  display: flex;
  flex-direction: column;
  gap: 9px;
  margin: 12px 0;
}

#calc-form label { display: block; }

#calc-form .select { margin-top: 5px; }

.calc-result {
  margin-top: 12px;
  background: var(--recess);
  border: 1px solid var(--aether-faint);
  border-radius: var(--radius);
  padding: 10px 11px;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.7;
  color: var(--aether);
  white-space: pre-wrap;
  word-break: break-word;
}

.calc-result:empty { display: none; }

.calc-result .cap,
.calc-result .micro { color: var(--white-faint); }

/* ============================================================
   Status bar
   ============================================================ */

.statusbar {
  background: var(--panel-solid);
  border-top: 1px solid var(--aether-faint);
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 0 20px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.06em;
  color: var(--white-faint);
  overflow: hidden;
  white-space: nowrap;
}

.statusbar .dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--white-faint);
  display: inline-block;
  margin-right: 7px;
}

.statusbar .dot.busy { background: var(--aether); animation: pulse-dot 1.2s ease-in-out infinite; }
.statusbar .dot.ok { background: var(--aether); }
.statusbar .dot.err { background: var(--alarm); }
.statusbar .dot.warn { background: var(--caution); }

.statusbar-right {
  margin-left: auto;
  text-transform: none;
  letter-spacing: 0.06em;
  font-family: var(--mono);
  overflow: hidden;
  text-overflow: ellipsis;
}

@keyframes pulse-dot {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

/* ============================================================
   Responsive
   ============================================================ */

/* Below 1200px the results rail drops under the viewport rather than
   squeezing the model into a slot. */
@media (max-width: 1200px) {
  .main {
    grid-template-columns: 320px minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
  }
  .rail-steps { grid-row: 1 / span 2; }
  .viewport { grid-column: 2; grid-row: 1; }
  .rail-results {
    grid-column: 2;
    grid-row: 2;
    border-left: none;
    border-top: 1px solid var(--aether-faint);
    max-height: 36vh;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-start;
  }
  .rail-results .panel { flex: 1 1 260px; min-width: 0; }
}

/* Below 860px everything stacks and the viewport takes a fixed slice of the
   screen, so the stepper stays reachable by scrolling the column. */
@media (max-width: 860px) {
  .main {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto auto auto;
    overflow-y: auto;
  }
  .rail-steps,
  .viewport,
  .rail-results {
    grid-column: 1;
    grid-row: auto;
  }
  .rail-steps {
    border-right: none;
    border-bottom: 1px solid var(--aether-faint);
    max-height: none;
    padding-bottom: 0;
  }
  .viewport { height: 50vh; min-height: 320px; }
  .rail-results {
    max-height: none;
    flex-direction: column;
  }
  .rail-results .panel { flex: 0 0 auto; }
  .topbar { gap: 10px; padding: 0 14px; }
  .topbar-status { display: none; }
  .legend { width: 92px; }
}

@media (max-width: 640px) {
  .wordmark { display: none; }
  .tab { padding: 7px 8px; font-size: 9px; letter-spacing: 0.06em; }
  .prodname { font-size: 17px; }
  .vp-overlay { gap: 4px; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

/* ---- rules for markup app.js generates that the shell does not show ---- */

/* The heat-flow table is four columns, so the flex space-between used for the
   generic .face-heat row would scatter them. Pin the grid instead. */
.face-heat-row {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto auto;
  gap: 10px;
  align-items: baseline;
}

.face-heat-row.head {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--white-faint);
  border-bottom: 1px solid var(--aether-faint);
}

.face-heat-row.head > :last-child { color: var(--white-faint); }

/* Which correlation produced a calculator answer, and whether the inputs were
   inside its validity range. The method line is not decoration: a Nusselt
   number with no correlation named beside it is not a result anyone can check. */
.calc-method {
  margin-top: 9px;
  padding-top: 8px;
  border-top: 1px solid var(--aether-faint);
  font-family: var(--sans);
  font-size: 10.5px;
  line-height: 1.55;
  color: var(--white-faint);
}

.calc-method strong { color: var(--aether); font-weight: 600; }

.calc-ok {
  margin-top: 8px;
  font-family: var(--sans);
  font-size: 10.5px;
  color: var(--ok);
}

.calc-err {
  font-family: var(--sans);
  font-size: 11px;
  line-height: 1.5;
  color: var(--alarm);
}

/* BC tag colours must match the colours viewer.js paints on the faces
   themselves (BC_COLORS in viewer.js) — the tag in the face list and the face in
   the viewport are the same fact shown twice, and if they disagree the list
   stops being a legend for the model. */
.bc-tag[data-bc="fixedT"],
.face-list [data-bc="fixedT"] { color: var(--alarm); }
.bc-tag[data-bc="flux"],
.face-list [data-bc="flux"] { color: var(--caution); }
.bc-tag[data-bc="convection"],
.face-list [data-bc="convection"] { color: #7E9AA2; }
.bc-tag[data-bc="radiation"],
.face-list [data-bc="radiation"] { color: #B3A6BE; }
.bc-tag[data-bc="none"],
.face-list [data-bc="none"] { color: var(--white-faint); background: none; }

/* The boot banner must always be dismissible. It covers the whole viewport, so
   if it ever fires spuriously — a slow CDN fetch that completes a second later —
   an undismissable banner IS the outage it was meant to warn about. */
#boot-dismiss {
  margin-top: 26px;
  align-self: flex-start;
}

/* ============================================================
   Propulsion tab — the centre column is a chart, not a viewport
   ============================================================ */

/* .viewport gives it the recessed ground and the border; this turns it from a
   positioning context for overlays into a simple vertical stack. */
.prop-viewport {
  display: flex;
  flex-direction: column;
  gap: 18px;
  padding: 26px 30px 22px;
  overflow: auto;
}

.prop-headline {
  flex: 0 0 auto;
  border-bottom: 1px solid var(--aether-faint);
  padding-bottom: 16px;
}

/* The chart owns the remaining height. min-height:0 is load-bearing: a flex
   child defaults to min-height:auto, which refuses to shrink below its content
   and pushes the canvas out of the panel instead of letting it size to fit. */
.prop-chart-wrap {
  flex: 1 1 auto;
  min-height: 260px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.chart-title {
  flex: 0 0 auto;
  color: var(--white-faint);
}

#prop-chart,
#comb-chart {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: block;
}

/* Below 860px the page column scrolls, so a chart column must not ALSO scroll inside
   the shared 50vh / 320px viewport box: headline, method note, chart and note are
   stacked and sized by their content, and the chart gets a definite height instead of
   a min-height. Definite matters: the charts size their canvas from the wrapper's
   clientHeight, and a wrapper sized by that same canvas would grow on every redraw. */
@media (max-width: 860px) {
  .prop-viewport { padding: 18px 16px; }
  .prop-pane > .prop-viewport { height: auto; min-height: 0; overflow: visible; }
  .prop-pane .prop-chart-wrap { flex: 0 0 auto; height: 240px; min-height: 0; }
}

/* The solve-readiness line. It states what is still missing, continuously —
   the refusal used to land in the footer status bar, far from the button that
   had just been pressed, which made a correct "no boundary conditions yet"
   indistinguishable from a broken button. */
#solve-req {
  border-left: 2px solid var(--aether-faint);
  padding-left: 9px;
  color: var(--white-dim);
}

#solve-req.blocked {
  border-left-color: var(--caution);
  color: var(--caution);
}

/* Face-row column widths.
   The name was `flex: 1 1 auto; min-width: 0` and the tag `flex: 0 0 auto`, so
   the tag took whatever width it liked and every pixel of shortfall came out of
   the name — a long convection tag collapsed "Face 0 · -Z" to "F". Give the name
   a floor, and let the tag be the thing that ellipsizes instead. */
.face-list > * > :first-child,
.face-row > .face-name {
  min-width: 78px;
}

.face-list .bc-tag,
.face-row .bc-tag {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The area column is the least important of the three; drop it before either of
   the others is compromised. */
@media (max-width: 1320px) {
  .face-row > .face-area { display: none; }
}

/* ============================================================
   Propulsion sub-tabs — three modules in one tab
   ============================================================ */

/* .main is a three-column grid; the propulsion panel needs a header strip above
   three such grids, so it stacks and each pane carries the columns instead. */
.main.stacked {
  display: flex;
  flex-direction: column;
  min-height: 0;
}

.prop-pane {
  flex: 1 1 auto;
  min-height: 0;
  display: grid;
  grid-template-columns: 330px minmax(0, 1fr) 330px;
}

.subtabs {
  flex: 0 0 auto;
  display: flex;
  gap: 2px;
  padding: 0 20px;
  background: linear-gradient(to bottom, rgba(10, 10, 10, 0.9), rgba(10, 10, 10, 0.7));
  border-bottom: 1px solid var(--aether-faint);
}

.subtab {
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  color: var(--white-faint);
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 11px 16px 9px;
  cursor: pointer;
  transition: color 0.18s, border-color 0.18s;
}

.subtab:hover { color: var(--white-dim); }
.subtab.active { color: var(--aether); border-bottom-color: var(--aether); }

/* per-stage input rows in the staging panel */
.stage-row {
  display: grid;
  grid-template-columns: 26px 1fr 1fr;
  gap: 8px;
  align-items: end;
  margin-top: 10px;
}

.stage-tag {
  font-family: var(--mono);
  color: var(--aether);
  padding-bottom: 7px;
}

/* Below 1200px the results rail runs full width under the steps and the chart.
   The shared rule above gives .rail-steps `grid-row: 1 / span 2` (right for the
   Thermal grid, whose results sit in column 2 only); inside a pane whose results
   span BOTH columns that put the lower part of the steps rail underneath the
   results. So a pane's steps take row 1 only, and row 1 takes the height left
   over by the results (capped at 36vh by the shared rule), scrolling inside. */
@media (max-width: 1200px) {
  .prop-pane {
    grid-template-columns: 300px minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) auto;
  }
  .prop-pane > .rail-steps { grid-row: 1; }
  .prop-pane > .rail-results { grid-column: 1 / -1; }
}

/* Below 860px everything stacks and the column scrolls, so the rows go back to
   content height and the steps back to normal flow, exactly as before.
   flex: 0 0 auto is what makes the column scroll. With the default 1 1 auto and
   min-height: 0 the pane shrank to the panel's height, the panel never scrolled, and
   the three auto rows split that height between them; the chart column, which does
   not scroll here (see .prop-pane > .prop-viewport above), then overflowed its row
   and, being position: relative, painted over the results rail below it. Sized by
   its content, the pane is taller than the panel and the panel scrolls instead. */
@media (max-width: 860px) {
  .prop-pane { flex: 0 0 auto; grid-template-columns: minmax(0, 1fr); grid-template-rows: none; }
  .prop-pane > .rail-steps { grid-row: auto; }
  .subtabs { overflow-x: auto; }
  .subtab { padding: 10px 11px 8px; letter-spacing: 0.1em; }
}

/* ============================================================
   Combustion tab — reuses the Propulsion layout (.main.stacked, .subtabs,
   .prop-pane, .prop-viewport); only what that layout has no rule for is here.
   ============================================================ */

/* Symbols inside the uppercase micro-labels and ledger keys keep their case and
   lose the wide tracking: uppercased, γ becomes Γ and c* becomes C*, which are
   different quantities. */
.sym {
  text-transform: none;
  letter-spacing: 0.02em;
}

/* Three stacked panels share the O/F axis, so the sweep needs more height than the
   single altitude plot's 260 px. The pointer reads values off it. */
.prop-chart-wrap.tall { min-height: 380px; }
#comb-chart { cursor: crosshair; }

#panel-combustion label.micro > .select { margin-top: 5px; }

/* A label that wraps to two lines (OXIDISER INLET T K in a 130 px column) would
   otherwise push its input below its neighbour's; align the inputs instead. */
#panel-combustion .custom-grid { align-items: end; }

/* An inlet temperature for a liquid or solid reactant does not enter the problem
   (its stated-state enthalpy does), so the field is shown but plainly inert. */
input[type="number"]:disabled {
  color: var(--white-faint);
  border-style: dashed;
  cursor: not-allowed;
}

/* Equilibrium species as a bar list: name, bar relative to the largest, value. */
.species-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-top: 8px;
}

.species-row {
  display: grid;
  grid-template-columns: minmax(0, 104px) minmax(0, 1fr) 62px;
  gap: 10px;
  align-items: center;
}

.sp-name {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--white-dim);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.sp-name sub { font-size: 0.72em; }
.sp-qual { color: var(--white-faint); font-size: 9.5px; }

.sp-bar {
  height: 6px;
  background: var(--aether-ghost);
  border-radius: 2px;
  overflow: hidden;
}

.sp-bar > i {
  display: block;
  height: 100%;
  background: var(--aether-dim);
  border-radius: 2px;
}

.sp-val {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--aether);
  text-align: right;
  white-space: nowrap;
}

/* the Flame pane's centre column is a list, not a chart; keep it readable width */
.flame-products { max-width: 620px; }

/* the three stacked sweep panels need more height than one chart; min-height:0 undoes
   the 380px floor above, which would otherwise outrank this height */
@media (max-width: 860px) {
  .prop-pane .prop-chart-wrap.tall { height: 340px; min-height: 0; }
}
