/* ============================================================
   VOID ORBITAL — ATLAS
   Design tokens lifted from the Void Orbital house style (Odyssey):
   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.
   ============================================================ */

: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); }

/* 2 px of full aether: the 1 px --aether-dim ring measured 2.85:1 on the void, under the
   3:1 a focus indicator needs (WCAG 1.4.11); step heads, list rows and dropzones take
   keyboard focus now, so the ring is how a keyboard user finds their place */
:focus-visible { outline: 2px solid var(--aether); outline-offset: 2px; }

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

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

.eyebrow,
.sect {
  font-family: var(--sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--aether-dim);
}

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

/* ============================================================
   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);
}

.boot-error #boot-dismiss { margin-top: 6px; }

.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;
  user-select: all;
}

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

/* Top bar, the ONE displayed tab panel, status bar. A hidden panel is display:none
   and so is not a grid item at all; each panel lays itself out inside row 2 (the
   Analysis panel is its own two-row grid: the three panes over the curve panel). */
.app-shell {
  display: grid;
  grid-template-rows: 56px minmax(0, 1fr) 24px;
  /* One column, bounded by the window. Left implicit (auto) it grows to the top bar's
     min-content width, which with the tab bar and a long file name reached 1101 px in a
     1000 px window (WebKit) and pushed Reset and the right rail off screen; bounded, the
     part summary truncates instead (.topbar-status: min-width 0, ellipsis). */
  grid-template-columns: minmax(0, 1fr);
  height: 100vh;
  width: 100vw;
}

.tab-panel {
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

#panel-fea {
  display: grid;
  grid-template-rows: minmax(0, 1fr) 296px;
}

.tab-error {
  padding: 40px;
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.6;
  color: var(--alarm);
  white-space: pre-wrap;
}

/* ---- 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);
}

/* 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: 2px solid var(--aether);
  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
   Atlas 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;              /* a flex item: without this a long file name pushes Reset off the bar */
}

.topbar-status .sep { color: var(--aether-faint); }
.topbar-status .val { color: var(--aether); }
/* text-overflow on the flex container never reaches its items, so at 1000 px the summary
   was cut mid-glyph ("Aluminum 60|", WebKit). Each span ellipsises itself; the separators
   never shrink, and the file name gives way last (only past 60 % of the bar). */
.topbar-status > span { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.topbar-status > .sep { flex-shrink: 0; }
.topbar-status > .val { flex-shrink: 0; max-width: 60%; }

.topbar-spacer { flex: 1; }

/* ---- tool tabs (the house segmented control, topbar variant; as helios/styles.css) ---- */

.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;
  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-primary {
  border-color: var(--aether);
  color: var(--aether);
}

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

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

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

.main {
  display: grid;
  grid-template-columns: 300px 1fr 272px;
  overflow: hidden;
}

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

.rail-steps {
  background: var(--panel-solid);
  border-right: 1px solid var(--aether-faint);
  overflow-y: auto;
  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-num {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--white-faint);
  width: 18px;
  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;
  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); }

.step-summary {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--white-faint);
  max-width: 88px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.step-body {
  padding: 0 16px 18px 44px;
  display: none;
}

.step[data-state="active"] .step-body { display: block; }

/* ---- form controls ---- */

.field { margin-bottom: 13px; }
.field:last-child { margin-bottom: 0; }

.field label {
  display: block;
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--white-faint);
  margin-bottom: 5px;
}
/* a unit inside an uppercase label keeps its case: MPa is not MPA, kPa not KPA, mm not MM */
.field label .unit { text-transform: none; letter-spacing: 0.04em; }

.field select,
.field input[type="text"],
.field input[type="number"] {
  width: 100%;
  background: var(--inset);
  border: 1px solid var(--aether-faint);
  color: var(--white);
  font-family: var(--mono);
  font-size: 12px;
  padding: 7px 8px;
  border-radius: var(--radius);
  outline: none;
}

.field select:focus,
.field input:focus { border-color: var(--aether-dim); }

.field-row { display: flex; gap: 8px; }
.field-row .field { flex: 1; }

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;
}

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

/* ledger rows — the house key/value idiom */

.readout-mini {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 8px;
  padding: 5px 0;
  border-bottom: 1px solid var(--aether-ghost);
  font-size: 11px;
  color: var(--white-faint);
}

.readout-mini .v {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--white);
  white-space: nowrap;
}

/* tab-style segmented control */

.pill-toggle {
  display: flex;
  gap: 4px;
}

.pill-toggle button {
  flex: 1;
  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 3px;
  cursor: pointer;
  user-select: none;
  transition: background 0.15s, color 0.15s, border-color 0.15s;
}

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

.pill-toggle button[aria-pressed="true"] {
  background: var(--aether-faint);
  color: var(--aether);
  border-color: var(--aether-dim);
}

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

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

/* Sample-part loader. Sits BELOW the dropzone rather than inside it: the
   dropzone is itself a click target for the file picker, so a nested button
   would have its click swallowed and open the file dialog instead. */
.btn-sample {
  width: 100%;
  margin-top: 8px;
  letter-spacing: 0.12em;
}

.dropzone strong {
  display: block;
  color: var(--aether);
  font-family: var(--serif);
  font-weight: 400;
  font-size: 15px;
  letter-spacing: 0.01em;
  margin-bottom: 5px;
  text-transform: none;
}

/* ============================================================
   Center viewport
   ============================================================ */

.viewport-wrap {
  position: relative;
  background: var(--void);
  overflow: hidden;
}

/* 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; }

/* shown in a viewport when three.js could not load (app.js makeViewer) */
.viewport-unavailable {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 32px;
  text-align: center;
  color: var(--white-faint);
  font-size: 11.5px;
  line-height: 1.6;
}

.viewport-unavailable .big {
  font-family: var(--serif);
  font-size: 22px;
  color: var(--white-dim);
}

.viewport-unavailable > div:not(.big) { max-width: 440px; }

.viewport-unavailable .why {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--white-faint);
  opacity: 0.8;
  word-break: break-word;
}

/* 3-D-anchored labels (viewer.js): positioned by transform each frame, text only */
.viewer-labels {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

.viewer-label {
  position: absolute;
  left: 0;
  top: 0;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.02em;
  color: var(--white);
  background: rgba(10, 10, 10, 0.72);
  border: 1px solid var(--aether-faint);
  border-radius: 3px;
  padding: 1px 5px;
  white-space: nowrap;
}

.viewer-label.force { color: var(--aether-bright); }
.viewer-label.moment { color: var(--aether-bright); }
.viewer-label.support { color: var(--white-dim); }
.viewer-label.dim { color: var(--aether); border-color: var(--aether-dim); }
.viewer-label.plane { color: var(--aether-dim); }

/* orientation gizmo — triad + axis-locked rotation ring, bottom-left */

.axis-gizmo {
  position: absolute;
  left: 16px;
  bottom: 14px;
  width: 124px;
  display: flex;
  flex-direction: column;
  align-items: center;
  /* The widget sits over the canvas, so it must not eat orbit drags. Only the
     ring and the three axis labels opt back in. */
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

.axis-gizmo.hidden { display: none; }

.gizmo-svg { width: 124px; height: 124px; overflow: visible; touch-action: none; }

.gizmo-ring { fill: none; stroke: var(--aether-faint); stroke-width: 1; }

.axis-gizmo.dragging .gizmo-ring { stroke: var(--aether-dim); stroke-width: 1.5; }

/* fat invisible copy of the ring — a 1px stroke is not grabbable */
.gizmo-ring-hit {
  fill: none;
  stroke: transparent;
  stroke-width: 15;
  pointer-events: stroke;
  cursor: grab;
}

.axis-gizmo.dragging .gizmo-ring-hit { cursor: grabbing; }

.gizmo-axis line { stroke: var(--white-faint); stroke-width: 1.25; }

.gizmo-axis .gizmo-dot {
  fill: var(--panel-solid);
  stroke: var(--white-faint);
  stroke-width: 1;
}

.gizmo-axis .gizmo-label {
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.04em;
  fill: var(--white-dim);
  text-anchor: middle;
  dominant-baseline: central;
}

.gizmo-axis .gizmo-hit { fill: transparent; pointer-events: all; cursor: pointer; }

/* axis points away from the viewer */
.gizmo-axis.behind line,
.gizmo-axis.behind .gizmo-dot { opacity: 0.3; }
.gizmo-axis.behind .gizmo-label { opacity: 0.45; }

.gizmo-axis.active line { stroke: var(--aether); stroke-width: 1.75; }
.gizmo-axis.active .gizmo-dot { stroke: var(--aether); fill: var(--aether-ghost); }
.gizmo-axis.active .gizmo-label { fill: var(--aether-bright); }

/* marks model-frame u in the rotation plane, so it visibly spins with the part */
.gizmo-knob { fill: var(--aether); stroke: var(--void); stroke-width: 1.5; pointer-events: none; }

.gizmo-readout {
  margin-top: -6px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--white-faint);
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.axis-gizmo.dragging .gizmo-readout { color: var(--aether); }

.viewport-hint {
  position: absolute;
  top: 14px;
  left: 14px;
  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);
  padding: 8px 11px;
  border-radius: var(--radius-lg);
  pointer-events: none;
  max-width: 330px;
  line-height: 1.55;
}

.viewport-hint .k { color: var(--aether); }

.viewport-empty {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 8px;
  color: var(--white-faint);
  text-align: center;
}

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

.viewport-empty.hidden { display: none; }

/* stress legend gauge */

.gauge-strip {
  position: absolute;
  top: 22px;
  bottom: 22px;
  right: 50px;               /* room right of the bar for the widest tick, "≥ 0.042 MPa" (~74 px) */
  width: 78px;
  display: flex;
  flex-direction: column;
  align-items: center;
  opacity: 0.4;
  transition: opacity 0.3s;
}

.gauge-strip.live { opacity: 1; }

.gauge-strip .cap,
.gauge-strip .cap-bottom {
  font-family: var(--sans);
  font-size: 9px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--aether-dim);
}

.gauge-strip .cap { margin-bottom: 8px; }
.gauge-strip .cap-bottom { margin-top: 8px; }

.gauge-strip .bar {
  flex: 1;
  width: 20px;
  border-radius: 2px;
  border: 1px solid var(--aether-faint);
  position: relative;
  background: linear-gradient(to top, #1C2A33 0%, #4E6B72 26%, var(--aether) 58%, var(--caution) 82%, var(--alarm) 100%);
}

.gauge-strip .tick {
  position: absolute;
  /* Anchored by its LEFT edge, just past the bar. It was right-anchored 48 px out, so any
     label wider than ~42 px — every sub-10 value, printed to 3 decimals ("0.042 MPa") —
     ran back over the colour bar (WebKit, 1440 x 900; there since before the tab shell). */
  left: calc(100% + 7px);
  transform: translateY(50%);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--white-dim);
  white-space: nowrap;
}

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

.rail-results {
  background: var(--panel-solid);
  border-left: 1px solid var(--aether-faint);
  overflow-y: auto;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.results-empty {
  color: var(--white-faint);
  font-size: 11.5px;
  line-height: 1.6;
}

.readout {
  border-bottom: 1px solid var(--aether-faint);
  padding-bottom: 12px;
}

.readout .cap {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--aether-dim);
  margin-bottom: 4px;
}

.readout .val {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 1.1;
  color: var(--white);
}

.readout .unit {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--aether-dim);
  margin-left: 5px;
}

.readout.danger .val { color: var(--alarm); }
.readout.warn .val { color: var(--caution); }
.readout.safe .val { color: var(--aether); }

/* ============================================================
   Bottom-left: stress-strain + safety factor
   ============================================================ */

.curve-panel {
  background: var(--panel-solid);
  border-top: 1px solid var(--aether-faint);
  display: grid;
  grid-template-columns: minmax(320px, 440px) minmax(180px, 210px) minmax(280px, 350px) minmax(260px, 1fr);
  /* The row is the panel's own height, not its tallest column's content. It used to be
     content-sized: the safety-factor column (348 px of content in a 270 px box at
     1440 x 900) stretched every column past the panel, and overflow:hidden then cut off
     the stress-strain chart's strain axis and the last criteria check (measured in
     WebKit, tools/webkit). Now the chart fits its box and the SF column scrolls. */
  grid-template-rows: minmax(0, 1fr);
  gap: 22px;
  padding: 14px 20px 12px;
  overflow: hidden;
}

.curve-col {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.curve-col-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--aether-faint);
}

.curve-col-head .hint { margin-top: 0; }

.curve-chart-wrap {
  position: relative;
  flex: 1;
  min-height: 0;
  min-width: 0;
}

#stress-strain-canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
}

.curve-reset-zoom {
  position: absolute;
  top: 4px;
  right: 4px;
  padding: 5px 9px;
  font-size: 9px;
}

.curve-empty-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--white-faint);
  font-size: 12px;
  text-align: center;
  pointer-events: none;
}

.curve-margin {
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 14px;
  min-width: 0;
}

.margin-readout .cap {
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--aether-dim);
  margin-bottom: 3px;
}

.margin-readout .val {
  font-family: var(--serif);
  font-size: 24px;
  line-height: 1.15;
  color: var(--white);
}

.margin-readout .val.danger { color: var(--alarm); }
.margin-readout .val.warn { color: var(--caution); }
.margin-readout .val.safe { color: var(--aether); }

.margin-readout .sub {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--white-faint);
  margin-top: 3px;
}

/* safety factor panel */

.sf-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;          /* see .curve-panel: it scrolls when the panel is shorter than it */
  border-left: 1px solid var(--aether-faint);
  padding-left: 20px;
}

.sf-panel .eyebrow {
  margin-bottom: 6px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--aether-faint);
}

#sf-meter { display: block; width: 100%; height: 100px; }

.sf-verdict {
  font-family: var(--serif);
  font-size: 17px;
  line-height: 1.3;
  margin-top: 4px;
  color: var(--white);
}

.sf-verdict.danger { color: var(--alarm); }
.sf-verdict.warn { color: var(--caution); }
.sf-verdict.safe { color: var(--aether); }

.sf-numbers {
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--white-faint);
  margin-top: 4px;
}

.sf-checks {
  margin-top: 10px;
  display: flex;
  flex-direction: column;
  gap: 3px;
}

.sf-check {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11px;
  color: var(--white-faint);
  line-height: 1.4;
}

.sf-check .mark {
  font-family: var(--mono);
  width: 10px;
  flex: none;
  color: var(--white-faint);
}

.sf-check.ok .mark { color: var(--aether); }
.sf-check.warn .mark { color: var(--caution); }
.sf-check.bad .mark { color: var(--alarm); }
.sf-check.ok .txt, .sf-check.warn .txt, .sf-check.bad .txt { color: var(--white-dim); }

.sf-footnote {
  margin-top: 9px;
  font-size: 9.5px;
  line-height: 1.5;
  color: var(--white-faint);
}

.curve-spacer { min-width: 0; }

/* ---- material trade study (fills the reserved right-hand column) ---- */

.trade-panel {
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-left: 1px solid var(--aether-faint);
  padding-left: 20px;
  overflow: hidden;
}

.trade-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  margin-bottom: 6px;
  padding-bottom: 5px;
  border-bottom: 1px solid var(--aether-faint);
}

.trade-sort { flex: 0 0 auto; gap: 3px; }
.trade-sort button { padding: 4px 7px; font-size: 8.5px; }

.trade-empty {
  font-size: 11.5px;
  color: var(--white-faint);
  line-height: 1.6;
  padding-top: 6px;
}

.trade-table {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  margin-right: -6px;
  padding-right: 6px;
}

.trade-row {
  display: grid;
  /* SF holds up to 7 characters: a number (fewer decimals past 100, app.js
     renderTradeStudy) plus its ~ and b markers. At 40 px a gravity-only solve's
     "1786.99~" ran into the Mass column (WebKit, 1440 x 900). */
  grid-template-columns: minmax(0, 1fr) 50px 52px 48px;
  gap: 6px;
  align-items: baseline;
  padding: 4px 0;
  border-bottom: 1px solid var(--aether-ghost);
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--white-dim);
  cursor: pointer;
}

.trade-row:hover { background: var(--aether-ghost); }

.trade-row.head {
  cursor: default;
  color: var(--aether-dim);
  border-bottom-color: var(--aether-faint);
  position: sticky;
  top: 0;
  background: var(--panel-solid);
  font-size: 9px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
}

.trade-row.head:hover { background: var(--panel-solid); }

.trade-row .nm {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--sans);
  font-size: 11px;
}

.trade-row span:not(.nm) { text-align: right; }

.trade-row.fails { color: var(--white-faint); }
.trade-row.fails .sf { color: var(--alarm); }
.trade-row.passes .sf { color: var(--aether); }
.trade-row.current {
  background: var(--aether-faint);
  color: var(--white);
}
.trade-row.current .nm { color: var(--aether); }
.trade-row .approx { color: var(--caution); }

/* convergence chart in the solve step */

#convergence-canvas { display: block; width: 100%; height: 100%; }

#conv-table {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.conv-row {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--white-faint);
  padding: 2px 0;
  border-bottom: 1px solid var(--aether-ghost);
}

.conv-row.head { color: var(--aether-dim); border-bottom-color: var(--aether-faint); }
.conv-row.finest { color: var(--white); }
.conv-row span:first-child { flex: 0 0 34px; }
.conv-row span { text-align: right; flex: 1; }

/* ============================================================
   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);
}

.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); }

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

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

@media (max-width: 1600px) {
  .curve-panel {
    grid-template-columns: minmax(300px, 420px) minmax(170px, 200px) minmax(270px, 330px) minmax(230px, 1fr);
    gap: 18px;
  }
  .trade-panel { padding-left: 16px; }
}

/* below this the trade study drops out rather than crushing the chart */
@media (max-width: 1320px) {
  .curve-panel {
    grid-template-columns: minmax(300px, 460px) minmax(175px, 210px) minmax(280px, 1fr);
  }
  .trade-panel { display: none; }
}

/* Below 1180 px the chart, the margin readouts and the safety factor stay side by side
   in ONE row, a little taller. (This used to stack the safety factor under the chart
   in a 470 px panel: the two rows needed 764 px, so all but the SF heading was cut off,
   and the 3-D viewport was left 250 px tall at 1000 x 800 — measured in WebKit.) */
@media (max-width: 1180px) {
  .curve-panel {
    grid-template-columns: minmax(250px, 1fr) minmax(160px, 190px) minmax(240px, 300px);
    gap: 16px;
  }
  .sf-panel { padding-left: 16px; }
  #panel-fea { grid-template-rows: minmax(0, 1fr) 340px; }
}

/* The top bar's part summary: below ~1000 px only its first item (the file) is kept. The
   others shrank to 0 px while their separators stayed ('Test Pl… · ·', WebKit, 860 px). */
@media (max-width: 1000px) {
  .topbar-status > span:not(:first-child) { display: none; }
}

/* ONE COLUMN THAT SCROLLS AS A WHOLE. The panes used to stay scroll containers inside a
   height-split grid: a scroll container's automatic minimum height is 0, so the stepper and
   results rails collapsed to 0 px rows under the viewport (WebKit, 860 × 900: rows
   '0px 450px 0px', "Run analysis" covered by the canvas) and the curve panel was clipped.
   Here the panel scrolls, the rails are as tall as their content, the viewport is 60 vh. */
@media (max-width: 900px) {
  .app-shell { grid-template-rows: auto minmax(0, 1fr) 24px; }
  .topbar { min-height: 56px; flex-wrap: wrap; row-gap: 8px; padding: 10px 16px; }
  /* a wrapping line places items at their content width, so a long file name pushed Reset
     onto a line of its own: the summary is capped, and Reset keeps to the right */
  .topbar-status { max-width: 200px; }
  #btn-reset { margin-left: auto; }
  .topbar .wordmark { display: none; }   /* the beacon mark stays; 150 px back for the line */
  #panel-fea { display: block; overflow-y: auto; }
  .main {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto 60vh auto;
    overflow: visible;
  }
  .rail-steps, .rail-results {
    border: none;
    border-bottom: 1px solid var(--aether-faint);
    overflow: visible;
    min-height: auto;
  }
  .viewport-wrap { min-height: 320px; }
  .curve-panel { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto; height: auto; overflow: visible; }
  .sf-panel { border-left: none; padding-left: 0; overflow-y: visible; }
  .curve-chart-wrap { min-height: 240px; }
}

/* Phone width: the tabs get a row of their own under the lockup (the bar's min-content was
   737 px at 390, with the Free Body and Tolerance tabs and Reset past the window's edge and
   the body not scrollable); the wordmark and the dividers give way. */
@media (max-width: 600px) {
  .topbar { gap: 10px; padding: 8px 12px; }
  .topbar .wordmark, .topbar .divider { display: none; }
  .topbar-status { max-width: none; flex: 1 1 0; }
  .tabs { order: 10; flex: 1 0 100%; overflow-x: auto; }
  .tab { flex: 1 0 auto; padding: 8px 8px; }
}

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

.trade-row .brit { color: var(--caution); font-size: 8px; vertical-align: super; }

/* ---- boundary-condition lists (constraints, loads) ---- */

.bc-empty {
  font-size: 10.5px;
  color: var(--white-faint);
  line-height: 1.5;
  padding: 3px 0;
}

.bc-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 0;
  border-bottom: 1px solid var(--aether-ghost);
}

.bc-id {
  flex: 0 0 auto;
  font-family: var(--mono);
  font-size: 9.5px;
  color: var(--aether-dim);
  letter-spacing: 0.04em;
}

.bc-txt {
  flex: 1;
  min-width: 0;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--white-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.bc-del {
  flex: 0 0 auto;
  background: none;
  border: 1px solid var(--aether-faint);
  border-radius: 3px;
  color: var(--white-faint);
  font-size: 12px;
  line-height: 1;
  padding: 1px 5px 3px;
  cursor: pointer;
}

.bc-del:hover { border-color: var(--alarm); color: var(--alarm); }
