/* =============================================================================
   Gravitas component language
   -----------------------------------------------------------------------------
   Buttons, panels, form controls and the shared chrome. Everything here lives
   in the `components` layer, which sits after `legacy` in the cascade, so these
   rules win on layer order rather than on `!important`.
   ========================================================================== */

@layer components {
  /* The rules below set `display` on buttons and several containers, which
     beats the user-agent style for [hidden]. Without this, anything the app
     hides by setting the attribute stays on screen: the plot panel's unlabeled
     transform toggle was rendering as an empty pill on every plot step that
     does not define one. */
  [hidden] {
    display: none !important;
  }

  /* ==========================================================================
     Buttons
     ====================================================================== */
  .ui-button,
  button,
  .toggle-button {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    font-weight: var(--weight-medium);
    line-height: 1;
    letter-spacing: 0.01em;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);

    padding: var(--space-2) var(--space-4);
    min-height: 34px;

    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    box-shadow: var(--shadow-sm);

    cursor: pointer;
    user-select: none;
    text-shadow: none;
    transition:
      background var(--duration) var(--ease),
      border-color var(--duration) var(--ease),
      color var(--duration) var(--ease),
      box-shadow var(--duration) var(--ease),
      transform var(--duration-fast) var(--ease);
  }

  .ui-button:hover,
  button:hover,
  .toggle-button:hover {
    background: var(--surface-3);
    border-color: var(--border-strong);
    color: var(--text-primary);
    box-shadow: var(--shadow);
    transform: translateY(-1px);
  }

  .ui-button:active,
  button:active,
  .toggle-button:active {
    background: var(--surface-2);
    box-shadow: var(--shadow-sm);
    transform: translateY(0);
  }

  .ui-button:disabled,
  button:disabled {
    opacity: 0.45;
    cursor: not-allowed;
    transform: none;
    box-shadow: none;
  }

  /* Quiet / icon-only */
  .ui-button.is-quiet,
  button.is-quiet {
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--text-secondary);
  }

  .ui-button.is-quiet:hover,
  button.is-quiet:hover {
    background: var(--surface-2);
    border-color: var(--border-subtle);
    color: var(--text-primary);
  }

  .ui-button.is-danger,
  button.is-danger {
    color: var(--danger);
    border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  }

  .ui-button.is-danger:hover,
  button.is-danger:hover {
    background: color-mix(in srgb, var(--danger) 16%, var(--surface-2));
    border-color: var(--danger);
    color: var(--text-primary);
  }

  /* ==========================================================================
     Toggle buttons - state carried by color AND a label, never color alone
     ====================================================================== */
  .toggle-button {
    position: relative;
    min-width: 62px;
    font-weight: var(--weight-medium);
  }

  /* Tint kept low so the label clears 4.5:1 against the mixed background in
     every theme: at 18% the light theme measured 3.92:1. */
  .toggle-button[data-state='on'] {
    background: color-mix(in srgb, var(--success) 6%, var(--surface-2));
    border-color: color-mix(in srgb, var(--success) 55%, transparent);
    color: var(--success);
  }

  .toggle-button[data-state='on']:hover {
    background: color-mix(in srgb, var(--success) 12%, var(--surface-2));
    border-color: var(--success);
  }

  .toggle-button[data-state='off'] {
    background: var(--surface-2);
    border-color: var(--border);
    color: var(--text-muted);
  }

  .toggle-button[data-state='off']:hover {
    background: var(--surface-3);
    color: var(--text-secondary);
  }

  /* ==========================================================================
     Elements that hide themselves
     -----------------------------------------------------------------------
     HAZARD: the generic button rule above sets `display: inline-flex`, which
     lands in this layer and therefore beats any `display: none` declared in
     the legacy layer. Every button that is meant to be conditionally hidden
     has to restate that here, or it appears everywhere.

     The floating close chips are pinned to the viewport corner for small
     screens where a panel header can scroll out of reach. On desktop the
     header already has its own close control, so showing both put two X
     buttons on top of each other.
     ====================================================================== */
  .floating-close-chip {
    display: none;
    position: fixed;
    top: var(--space-3);
    right: var(--space-3);
    z-index: var(--z-toast);

    width: 34px;
    height: 34px;
    min-height: 34px;
    padding: 0;
    align-items: center;
    justify-content: center;

    font-size: var(--text-sm);
    color: var(--text-primary);
    background: color-mix(in srgb, var(--surface-2) 92%, transparent);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: var(--shadow);
  }

  @media (max-width: 480px) {
    .floating-close-chip {
      display: flex;
    }
  }

  /* ==========================================================================
     Focus - visible for keyboard users, quiet for pointer users
     ====================================================================== */
  :where(
    button,
    .ui-button,
    .toggle-button,
    select,
    input,
    textarea,
    [tabindex]
  ):focus {
    outline: none;
  }

  :where(
    button,
    .ui-button,
    .toggle-button,
    select,
    input,
    textarea,
    [tabindex]
  ):focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
  }

  /* ==========================================================================
     Panels - one surface treatment for every floating container
     ====================================================================== */
  .settings-content,
  .settings-header,
  .settings-footer,
  .bh-masses-header,
  .bh-masses-footer,
  .inspector-header,
  .inspector-content,
  .mobile-instructions-content,
  .preset-info,
  .three-view-container,
  .light-curve-container {
    font-family: var(--font-sans);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
  }

  .three-view-container,
  .light-curve-container {
    background: var(--surface-1);
    border: 1px solid var(--border);
    overflow: hidden;
  }

  .three-view-toolbar,
  .light-curve-toolbar {
    background: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    padding: var(--space-2) var(--space-3);
  }

  .three-view-title,
  .light-curve-title {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.02em;
    color: var(--text-primary);
  }

  .three-view-status,
  .light-curve-status {
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .three-toolbar-btn,
  .light-curve-btn,
  .light-curve-info-btn {
    min-height: 26px;
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    background: transparent;
    border-color: var(--border-subtle);
    color: var(--text-secondary);
    box-shadow: none;
  }

  .three-toolbar-btn:hover,
  .light-curve-btn:hover,
  .light-curve-info-btn:hover {
    background: var(--surface-3);
    color: var(--text-primary);
  }

  .three-view-hint,
  .light-curve-hint {
    font-size: var(--text-2xs);
    color: var(--text-muted);
    padding: var(--space-2) var(--space-3);
    border-top: 1px solid var(--border-subtle);
  }

  /* ==========================================================================
     Settings sections
     ====================================================================== */
  .settings-section {
    margin-bottom: var(--space-4);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    overflow: hidden;
    background: var(--surface-inset);
    transition: border-color var(--duration) var(--ease);
  }

  .settings-section:hover {
    border-color: var(--border);
  }

  .settings-section-header {
    padding: var(--space-3) var(--space-4);
    background: var(--surface-2);
    border-bottom: 1px solid var(--border-subtle);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    transition: background var(--duration) var(--ease);
    user-select: none;
  }

  .settings-section-header:hover {
    background: var(--surface-3);
  }

  .settings-section.collapsed .settings-section-header {
    border-bottom-color: transparent;
  }

  .settings-section-title {
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.01em;
    color: var(--text-primary);
    text-shadow: none;
    margin: 0;
  }

  .settings-section-toggle {
    width: 18px;
    height: 18px;
    flex: none;
    color: var(--text-muted);
    font-size: var(--text-base);
    font-weight: var(--weight-bold);
    display: flex;
    align-items: center;
    justify-content: center;
    transition: transform var(--duration) var(--ease);
  }

  .settings-section.collapsed .settings-section-toggle {
    transform: rotate(-90deg);
  }

  .settings-section-content {
    padding: var(--space-4);
    max-height: 1400px;
    opacity: 1;
    overflow: hidden;
    transition:
      max-height var(--duration-slow) var(--ease),
      opacity var(--duration) var(--ease),
      padding var(--duration-slow) var(--ease);
  }

  .settings-section.collapsed .settings-section-content {
    max-height: 0;
    padding-top: 0;
    padding-bottom: 0;
    opacity: 0;
  }

  .settings-header,
  .settings-footer,
  .settings-content,
  .settings-grid,
  .preset-info {
    font-family: var(--font-sans);
  }

  /* ==========================================================================
     Form controls
     ====================================================================== */
  select,
  input[type='text'],
  input[type='number'],
  input[type='search'] {
    font-family: var(--font-sans);
    font-size: var(--text-base);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--space-2) var(--space-3);
    transition:
      border-color var(--duration) var(--ease),
      background var(--duration) var(--ease);
  }

  select:hover,
  input[type='text']:hover,
  input[type='number']:hover,
  input[type='search']:hover {
    border-color: var(--border-strong);
  }

  /* The track used to be a 4px bar of --surface-3, which against the Midnight
     panel background is very nearly the same color: the control was easy to
     miss entirely, and there was no indication of where in its range the value
     sat. Taller, outlined, and filled up to the current value. --range-fill is
     set from JS; the fallback keeps the track sensible if that never runs. */
  input[type='range'] {
    -webkit-appearance: none;
    appearance: none;
    height: 7px;
    border-radius: var(--radius-full);
    background: linear-gradient(
      to right,
      var(--accent) 0%,
      var(--accent) var(--range-fill, 0%),
      var(--range-track, #2b3245) var(--range-fill, 0%),
      var(--range-track, #2b3245) 100%
    );
    border: 1px solid var(--border-strong);
    outline: none;
  }

  input[type='range']:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
  }

  input[type='range']::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 18px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--accent);
    /* A ring in the panel color separates the thumb from the filled track it
       sits on, which is otherwise the same color. */
    border: 2px solid var(--panel-bg);
    box-shadow:
      0 0 0 1px var(--accent),
      0 1px 4px rgb(0 0 0 / 45%);
    cursor: pointer;
    transition: transform var(--duration-fast) var(--ease);
  }

  input[type='range']::-webkit-slider-thumb:hover {
    transform: scale(1.18);
  }

  input[type='range']::-moz-range-thumb {
    width: 18px;
    height: 18px;
    border-radius: var(--radius-full);
    background: var(--accent);
    border: 2px solid var(--panel-bg);
    box-shadow: 0 0 0 1px var(--accent);
    cursor: pointer;
  }

  /* ==========================================================================
     Utility
     ====================================================================== */
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border: 0;
  }

  .numeric {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.01em;
  }
}

/* =============================================================================
   Application chrome
   -----------------------------------------------------------------------------
   The HUD, the control rail, the tutorial affordance and the transport bar.
   Moved here out of three competing inline <style> blocks in index.html.
   ========================================================================== */

@layer components {
  /* ==========================================================================
     Control rail (right-hand button column)
     ====================================================================== */
  .ui-container {
    /* Revealed by .showUI after the splash. Using a transition rather than an
       entrance animation keeps opacity in the normal cascade, where the
       narrow-screen .is-open rule can still win. */
    opacity: 0;
    transform: translateY(-8px);
    transition:
      opacity var(--duration-slow) var(--ease-out),
      transform var(--duration-slow) var(--ease-out);

    /* The rail can outgrow short viewports once every group is present, so it
       scrolls internally rather than running off the bottom of the screen.
       No transport-bar reservation here: at this width the rail is a column at
       the right edge and the bar is a centered pill that ends well short of it,
       so the two never meet. Reserving the space anyway made the rail scroll
       before its own open section fitted, which is the one thing the accordion
       exists to avoid - see the tablet rule further down, where they do meet. */
    top: var(--space-4);
    right: var(--space-4);
    max-height: calc(100vh - var(--space-8));
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    width: var(--rail-width);

    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    gap: var(--space-1);
    padding: var(--space-3);
    background: color-mix(in srgb, var(--surface-1) 88%, transparent);
    backdrop-filter: blur(18px) saturate(1.2);
    -webkit-backdrop-filter: blur(18px) saturate(1.2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    z-index: var(--z-chrome);
  }

  .ui-container.showUI {
    opacity: 1;
    transform: translateY(0);
  }

  .ui-container .ui-button {
    justify-content: flex-start;
    width: 100%;
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--text-secondary);
    font-weight: var(--weight-normal);
    /* Slightly tighter than the app's default button. Seventeen of these stack
       in one column, so four pixels each is seventy pixels of rail. */
    padding: 6px var(--space-3);
  }

  .ui-container .ui-button:hover {
    background: var(--surface-2);
    border-color: transparent;
    color: var(--text-primary);
    transform: none;
    box-shadow: none;
  }

  .ui-container .ui-button:active {
    background: var(--surface-3);
  }

  /* ==========================================================================
     Numerals that sit in columns
     --------------------------------------------------------------------------
     Proportional figures are right for prose and wrong the moment numbers are
     stacked: a 1 is narrower than a 7, so a live readout shifts sideways as its
     own values change and the eye has to re-find the decimal point on every
     update. Tabular figures give every digit the same advance.

     Applied to the surfaces that show measurements, not to prose, where
     proportional figures still read better. Widget readouts, the speed display
     and the .numeric utility already carried this; these are the readouts that
     had been missed, chiefly the object inspector and the live HUD.
     ====================================================================== */
  #overlayStats,
  .stat-row,
  .stat-value,
  .energy-value-display,
  .inv-wedge-readout,
  .inv-answer-num {
    font-variant-numeric: tabular-nums;
  }

  /* ==========================================================================
     Observing panels: light curve, radial velocity, astrometry
     --------------------------------------------------------------------------
     Three instruments that must read as three views of one rig. They share a
     chrome, and they stack rather than overlap: js/observationLayout.js sets
     each open panel's `bottom` so a second instrument opens above the first
     instead of on top of it.
     ====================================================================== */
  .obs-panel {
    position: fixed;
    left: 20px;
    bottom: 120px;
    width: 420px;
    background: var(--surface-1, rgba(5, 9, 22, 0.95));
    border: 1px solid var(--border-subtle, rgba(0, 180, 255, 0.3));
    border-radius: var(--radius-lg, 12px);
    box-shadow: var(--shadow-lg, 0 12px 48px rgba(0, 0, 0, 0.5));
    backdrop-filter: blur(6px);
    z-index: 160;
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding-bottom: var(--space-2);
    overflow-x: hidden;

    /* Never taller than the space between the panel's own bottom edge and the
       top of the window. Without this an instrument grows upward off the top
       of the screen as its contents grow, and the first thing to leave is the
       toolbar - so the close button becomes unreachable and the panel cannot
       be dismissed at all. The radial velocity panel crossed that line at 900
       tall in a 900px window when its Analyze action was added: 36px of
       header, including the X, sat above the viewport.

       The collapsed rule below is the same problem solved for one case; this
       is the general one, and it holds for a short laptop screen as well as
       for whatever gets added to a panel next. */
    max-height: calc(100vh - 140px);
    overflow-y: auto;
    overscroll-behavior: contain;
  }

  /* Pinned, so scrolling the contents never takes the title and the close
     button away with them. Opaque rather than inherited: the panel's own
     background sits behind a backdrop-filter, and a transparent toolbar would
     have the content scroll visibly underneath it. */
  .obs-panel-toolbar {
    position: sticky;
    top: 0;
    z-index: 1;
    background: var(--surface-1, rgba(5, 9, 22, 0.95));
  }

  /* Collapsed: the title bar and nothing else. Three instruments will not fit
     one above another on an ordinary screen, and a panel reduced to its name is
     more useful than one pushed off the top of the window. */
  .obs-panel.is-collapsed > :not(.obs-panel-toolbar),
  .light-curve-container.is-collapsed > :not(.light-curve-toolbar),
  .three-view-container.is-collapsed > :not(.three-view-toolbar) {
    display: none;
  }
  .obs-panel.is-collapsed,
  .light-curve-container.is-collapsed,
  .three-view-container.is-collapsed {
    height: auto;
    min-height: 0;
    cursor: pointer;
  }
  .obs-panel.is-collapsed .obs-panel-toolbar,
  .light-curve-container.is-collapsed .light-curve-toolbar,
  .three-view-container.is-collapsed .three-view-toolbar {
    border-bottom: none;
  }

  .obs-panel-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
  }
  .obs-panel-meta {
    display: flex;
    align-items: baseline;
    gap: var(--space-2);
    min-width: 0;
  }
  .obs-panel-title {
    font-size: var(--text-sm);
    font-weight: 600;
    color: var(--text-primary);
  }
  .obs-panel-status {
    font-size: var(--text-xs);
    color: var(--text-muted);
    font-variant-numeric: tabular-nums;
  }
  .obs-panel-actions {
    display: flex;
    gap: var(--space-1);
    flex: 0 0 auto;
  }
  .obs-panel-btn {
    background: var(--surface-2);
    color: var(--text-secondary);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm, 4px);
    font-size: var(--text-xs);
    padding: 2px 8px;
    cursor: pointer;
    min-height: 24px;
  }
  .obs-panel-btn:hover {
    color: var(--text-primary);
    border-color: var(--accent);
  }
  .obs-panel-btn:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
  }

  /* The flexible part of the panel. Everything else - readouts, the observer
     controls - has its own intrinsic height, so when the stack is short of
     room and the layout caps a panel, the chart is what gives way and the
     controls stay reachable. */
  .obs-panel-stage {
    position: relative;
    flex: 1 1 150px;
    min-height: 90px;
    margin: 0 var(--space-3);
  }
  .obs-panel-stage canvas {
    width: 100% !important;
    height: 100% !important;
    display: block;
  }
  .obs-panel-hint {
    margin: 0 var(--space-3);
    font-size: var(--text-xs);
    line-height: var(--leading-snug, 1.35);
    color: var(--text-muted);
  }

  /* A scenario that pins its star cannot show a wobble. Saying so is the
     difference between a simplification and a false lesson. */
  .rv-notice {
    margin: 0 var(--space-3);
    padding: var(--space-2);
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-secondary);
    background: var(--surface-2);
    border-left: 3px solid var(--warning, #e0a458);
    border-radius: var(--radius-sm, 4px);
  }

  /* --- Radial velocity readouts ------------------------------------------- */
  .rv-readout {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-1) var(--space-3);
    padding: 0 var(--space-3);
  }
  .rv-readout-cell {
    display: flex;
    flex-direction: column;
    min-width: 0;
  }
  .rv-readout-label {
    font-size: 10px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .rv-readout-value {
    font-size: var(--text-sm);
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
  }

  /* Direction is stated in words. The color is a second cue, never the only
     one: a red line means nothing to a color-blind student or a screen
     reader, and "AWAY FROM US" means the same thing to everybody. */
  .rv-direction {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.04em;
    color: var(--text-muted);
  }
  .rv-direction.is-receding {
    color: var(--danger, #e2725b);
  }
  .rv-direction.is-approaching {
    color: var(--info, #5b9bd5);
  }
  .rv-direction.is-zero {
    color: var(--text-muted);
  }

  /* --- The Doppler line ---------------------------------------------------- */
  .rv-spectrum {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: 0 var(--space-3);
  }
  .rv-spectrum-label,
  .rv-spectrum-note {
    font-size: 10px;
    color: var(--text-muted);
    flex: 0 0 auto;
  }
  .rv-spectrum-note {
    font-style: italic;
  }
  .rv-spectrum-band {
    position: relative;
    flex: 1 1 auto;
    height: 20px;
    border-radius: 3px;
    background: linear-gradient(
      90deg,
      var(--info, #5b9bd5) 0%,
      var(--surface-2) 45%,
      var(--surface-2) 55%,
      var(--danger, #e2725b) 100%
    );
    opacity: 0.5;
  }
  .rv-spectrum-rest {
    position: absolute;
    left: 50%;
    top: 0;
    bottom: 0;
    width: 1px;
    background: var(--text-muted);
  }
  .rv-spectrum-shift {
    position: absolute;
    left: 50%;
    top: 2px;
    bottom: 2px;
    width: 3px;
    border-radius: 2px;
    background: var(--accent);
    transition: transform 90ms linear;
  }

  /* --- Astrometry ---------------------------------------------------------- */
  /* The flexible region of this panel, for the same reason .obs-panel-stage is
     the flexible region of the radial-velocity one: when the layout caps the
     panel to fit the stack, the sky plot gives way and the observer controls
     below it keep their height. Without this the cap clipped the inclination
     slider clean off. */
  .astrometry-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-3);
    padding: 0 var(--space-3);
    align-items: start;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
  }
  /* Equal scales on both axes: the sky path is a shape, and a box that
     stretched it would turn a face-on circle into an ellipse and destroy the
     one thing the panel exists to show. */
  .astrometry-stage {
    position: relative;
    aspect-ratio: 1 / 1;
    min-height: 96px;
    max-height: 100%;
  }
  .astrometry-stage canvas {
    width: 100%;
    height: 100%;
    display: block;
  }
  .astrometry-caption {
    position: absolute;
    left: 0;
    bottom: -14px;
    font-size: 10px;
    color: var(--text-muted);
  }
  .astrometry-readout {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
  }
  .astrometry-distance {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3) 0;
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .astrometry-distance-input {
    width: 82px;
    min-height: 26px;
    background: var(--surface-2);
    color: var(--text-primary);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm, 4px);
    padding: 2px 6px;
    font-variant-numeric: tabular-nums;
  }
  .astrometry-distance-input:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
  }
  .astrometry-distance-note {
    font-size: 10px;
    font-style: italic;
    color: var(--text-muted);
    flex: 1 1 100%;
  }

  @media (max-width: 620px) {
    .astrometry-body {
      grid-template-columns: minmax(0, 1fr);
    }
  }

  /* --- The shared observer control ---------------------------------------- */
  .obs-controls {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3) 0;
    border-top: 1px solid var(--border-subtle);
  }
  .obs-control-row {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 2px var(--space-2);
    align-items: center;
  }
  .obs-control-label {
    font-size: var(--text-xs);
    color: var(--text-secondary);
    display: flex;
    justify-content: space-between;
    gap: var(--space-2);
    grid-column: 1 / -1;
  }
  .obs-control-value {
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
  }
  .obs-control-slider {
    grid-column: 1;
    width: 100%;
    min-height: 24px;
  }
  .obs-control-slider:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
  }
  .obs-control-help {
    grid-column: 1 / -1;
    margin: 0;
    font-size: 10px;
    line-height: 1.35;
    color: var(--text-muted);
  }

  /* Inclination cannot be shown honestly by the 2-D observer arrow on the
     simulation, so it gets its own picture: a disc squashed by cos i. */
  .obs-tilt {
    grid-column: 2;
    width: 26px;
    height: 26px;
    display: grid;
    place-items: center;
  }
  .obs-tilt > span {
    display: block;
    width: 22px;
    height: 22px;
    border: 1.5px solid var(--accent);
    border-radius: 50%;
    transform-origin: center;
  }

  @media (max-width: 900px) {
    .obs-panel {
      left: 10px;
      right: 10px;
      width: auto;
    }
    .rv-readout {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* ==========================================================================
     HUD overlay (top-left readout)
     ====================================================================== */
  #overlay {
    z-index: var(--z-overlay);
  }

  #overlayStats,
  #overlay .overlay-panel {
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    line-height: var(--leading-normal);
    color: var(--hud-text);
    background: color-mix(in srgb, var(--hud-surface) 82%, transparent);
    backdrop-filter: blur(16px) saturate(1.2);
    -webkit-backdrop-filter: blur(16px) saturate(1.2);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow);
    padding: var(--space-3) var(--space-4);
  }

  #overlayStats b,
  #overlayStats strong {
    color: var(--hud-text-strong);
    font-weight: var(--weight-semibold);
  }

  /* ==========================================================================
     Scenario info card
     ====================================================================== */
  /* The wrapper positions; .scenario-info-content carries the surface. Styling
     both produced two nested rounded boxes, one inset from the other. */
  #scenarioInfoBox,
  .scenario-info-box {
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
    z-index: var(--z-panel);

    /* Center in the band between the HUD and the rail, not in the viewport,
       so the card cannot sit on top of the readout panel. */
    --side-left: calc(var(--hud-width) + var(--panel-inset) + var(--space-3));
    --side-right: calc(var(--rail-width) + var(--panel-inset) + var(--space-3));
    width: min(720px, calc(100vw - var(--side-left) - var(--side-right)));
    transform: translateX(
      calc(-50% + (var(--side-left) - var(--side-right)) / 2)
    );
  }

  /* Below the rail's breakpoint the HUD and rail no longer flank it. */
  @media (max-width: 1024px) {
    #scenarioInfoBox,
    .scenario-info-box {
      --side-left: var(--space-4);
      --side-right: var(--space-4);
      width: min(720px, calc(100vw - var(--space-8)));
      transform: translateX(-50%);
    }
  }

  .scenario-info-content {
    width: 100%;
    max-width: none;
    padding: var(--space-4) var(--space-5);
    background: color-mix(in srgb, var(--surface-1) 92%, transparent);
    backdrop-filter: blur(20px) saturate(1.2);
    -webkit-backdrop-filter: blur(20px) saturate(1.2);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  .scenario-info-body p {
    margin: 0;
    color: var(--text-secondary);
  }

  .scenario-info-close {
    min-height: 26px;
    min-width: 26px;
    padding: 0;
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--text-muted);
    font-size: var(--text-md);
    line-height: 1;
  }

  .scenario-info-close:hover {
    background: var(--surface-2);
    border-color: transparent;
    color: var(--text-primary);
    transform: none;
  }

  .scenario-info-title,
  #scenarioInfoBox h3 {
    color: var(--accent);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.01em;
    margin: 0 0 var(--space-2);
  }

  /* ==========================================================================
     Tutorial affordance
     ====================================================================== */
  .tutorial-btn {
    position: fixed;
    left: var(--space-4);
    bottom: var(--space-4);
    z-index: var(--z-chrome);
    width: 40px;
    height: 40px;
    min-height: 40px;
    padding: 0;
    border-radius: var(--radius-full);
    font-size: var(--text-md);
    font-weight: var(--weight-bold);
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-slow) var(--ease-out);
  }

  .tutorial-btn.showUI {
    opacity: 1;
    pointer-events: auto;
  }

  .tutorial-popup {
    position: fixed;
    left: var(--space-4);
    bottom: calc(var(--space-10) + var(--space-6));
    width: min(360px, calc(100vw - var(--space-8)));
    max-height: 80vh;
    background: var(--surface-1);
    color: var(--text-primary);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
    font-family: var(--font-sans);
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    z-index: var(--z-modal);
    touch-action: none;
    user-select: none;
  }

  @media (max-width: 600px) {
    .tutorial-btn {
      left: var(--space-3);
      bottom: var(--space-3);
    }

    .tutorial-popup {
      left: var(--space-3);
      width: calc(100vw - var(--space-6));
      max-height: 70vh;
    }
  }

  /* ==========================================================================
     Speed control cluster
     ====================================================================== */
  .speed-control,
  .speed-controls {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin: var(--space-1) 0;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    padding: var(--space-1);
  }

  .ui-container::-webkit-scrollbar {
    width: 6px;
  }

  .ui-container::-webkit-scrollbar-thumb {
    background: var(--surface-3);
    border-radius: var(--radius-full);
  }

  /* Short viewports: shrink the rail rather than clipping it */
  @media (max-height: 720px) {
    .ui-container .ui-button {
      min-height: 30px;
      padding: var(--space-1) var(--space-3);
      font-size: var(--text-xs);
    }

    .rail-group + .rail-group {
      margin-top: var(--space-2);
      padding-top: var(--space-2);
    }
  }

  /* The two buttons mirror each other so the arrows point outward: "rewind
     Slow" on the left, "Fast forward" on the right. That only reads as a pair
     when both stay on one line and take the same width - at the rail's width
     "Slow" was wrapping under its own arrow while "Fast" stayed beside its own,
     which made the two look like different kinds of control. */
  .speed-control .ui-button,
  .speed-controls .ui-button {
    flex: 1 1 0;
    justify-content: center;
    align-items: center;
    gap: 4px;
    min-height: 28px;
    padding: var(--space-1) var(--space-1);
    font-size: var(--text-xs);
    white-space: nowrap;
  }

  #speedDisplay,
  .speed-display {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    font-size: var(--text-xs);
    color: var(--accent);
    min-width: 44px;
    text-align: center;
  }
}

/* =============================================================================
   New surfaces: rail groups, transport bar, toasts, shortcut sheet
   ========================================================================== */

@layer components {
  /* ==========================================================================
     Narrow-screen menu
     -----------------------------------------------------------------------
     The hamburger opens the control rail itself rather than a second copy of
     it. It must stay hidden on desktop - the generic button rule below sets
     display:inline-flex, which would otherwise override the legacy
     display:none and leave the hamburger sitting on top of the rail.
     ====================================================================== */
  .mobile-menu-toggle {
    display: none;
    position: fixed;
    top: var(--space-4);
    right: var(--space-4);
    z-index: calc(var(--z-chrome) + 1);

    width: 42px;
    height: 42px;
    min-height: 42px;
    padding: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;

    background: color-mix(in srgb, var(--surface-1) 90%, transparent);
    backdrop-filter: blur(18px) saturate(1.2);
    -webkit-backdrop-filter: blur(18px) saturate(1.2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
  }

  .mobile-menu-toggle .hamburger {
    display: block;
    width: 18px;
    height: 2px;
    margin: 0;
    border-radius: var(--radius-full);
    background: var(--text-secondary);
    transition:
      transform var(--duration) var(--ease),
      opacity var(--duration) var(--ease),
      background var(--duration) var(--ease);
  }

  .mobile-menu-toggle:hover .hamburger {
    background: var(--text-primary);
  }

  .mobile-menu-toggle.active .hamburger:nth-child(1) {
    transform: translateY(7px) rotate(45deg);
  }

  .mobile-menu-toggle.active .hamburger:nth-child(2) {
    opacity: 0;
  }

  .mobile-menu-toggle.active .hamburger:nth-child(3) {
    transform: translateY(-7px) rotate(-45deg);
  }

  @media (max-width: 1024px) {
    .mobile-menu-toggle {
      display: flex;
    }

    /* The rail becomes the dropdown the hamburger opens. */
    .ui-container {
      display: flex;
      top: calc(var(--space-4) + 52px);
      right: var(--space-3);
      left: var(--space-3);
      width: auto;
      /* Here the rail is a full-width sheet and the transport bar is drawn on
         top of its lower edge, so whichever control landed in the last fifty
         pixels was underneath the playback controls. This is the width where
         the reservation is needed and the only one where it is. */
      max-height: calc(100vh - var(--space-10) - 64px - var(--transport-space));

      opacity: 0;
      visibility: hidden;
      transform: translateY(-8px) scale(0.985);
      transform-origin: top right;
      pointer-events: none;
      transition:
        opacity var(--duration) var(--ease),
        transform var(--duration) var(--ease-out),
        visibility 0s linear var(--duration);
    }

    /* Higher specificity than .ui-container.showUI so the menu state wins */
    .ui-container.is-open,
    .ui-container.showUI.is-open {
      opacity: 1;
      visibility: visible;
      transform: translateY(0) scale(1);
      pointer-events: auto;
      transition-delay: 0s;
    }

    /* .showUI alone must not reveal the rail on narrow screens - only the
       hamburger does. */
    .ui-container.showUI:not(.is-open) {
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
    }

    /* Two columns of controls on tablets, so the sheet is not a long scroll */
    .rail-group > [role='group'] {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
      gap: var(--space-1);
    }

    .rail-field,
    .rail-toggle,
    .rail-primary {
      grid-column: 1 / -1;
    }
  }

  @media (max-width: 480px) {
    .rail-group > [role='group'] {
      grid-template-columns: 1fr;
    }
  }

  /* ==========================================================================
     Control rail grouping
     ====================================================================== */
  .rail-group {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
  }

  .rail-group + .rail-group {
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
  }

  .rail-group > [role='group'] {
    display: flex;
    flex-direction: column;
    gap: 2px;
  }

  .rail-label {
    margin: 0 0 var(--space-1);
    padding: 0 var(--space-3);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* A section header that opens and shuts its group. Styled as the label it
     replaced rather than as a button: it is a hinge on a heading, and giving it
     button chrome would put four more boxes in a rail this change exists to
     calm down. */
  .rail-section-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    width: 100%;
    padding: 2px var(--space-3);
    font-family: inherit;
    text-align: left;
    background: none;
    border: 0;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition: color var(--duration) var(--ease);
  }

  .rail-section-toggle:hover,
  .rail-section-toggle:focus-visible {
    color: var(--text-secondary);
  }

  .rail-chevron {
    flex: none;
    width: 7px;
    height: 7px;
    border-right: 1.5px solid currentColor;
    border-bottom: 1.5px solid currentColor;
    /* Points down when the section is open, right when it is shut. The extra
       translate keeps the glyph optically centered as it turns. */
    transform: translateY(-2px) rotate(45deg);
    transition: transform var(--duration) var(--ease);
  }

  .rail-section-toggle[aria-expanded='false'] .rail-chevron {
    transform: translateX(-2px) rotate(-45deg);
  }

  @media (prefers-reduced-motion: reduce) {
    .rail-chevron {
      transition: none;
    }
  }

  /* --- Rail: the Learn group ------------------------------------------------
     Text links, not buttons. Everything else in the rail acts on the
     simulation; these navigate away from it, and giving them button chrome
     would put four more full-width controls in front of the ones that matter.
     The group sits last for the same reason. */
  .rail-group-learn {
    margin-top: var(--space-1);
  }

  .rail-links {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
    padding: 0 var(--space-3);
  }

  .rail-link {
    padding: var(--space-1) 0;
    font: inherit;
    font-size: var(--text-xs);
    line-height: 1.4;
    text-align: left;
    text-decoration: none;
    color: var(--text-secondary);
    background: none;
    border: 0;
    border-bottom: 1px solid transparent;
    cursor: pointer;
    transition:
      color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .rail-link:hover,
  .rail-link:focus-visible {
    color: var(--accent);
    border-bottom-color: currentColor;
  }

  .rail-field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }

  .rail-field select {
    flex: 1;
    min-width: 0;
    padding: var(--space-1) var(--space-2);
    font-size: var(--text-xs);
  }

  .rail-toggle {
    margin: var(--space-1) var(--space-3) 0;
    width: calc(100% - var(--space-6));
  }

  /* The primary action stays put while the groups above it scroll. The rail
     holds seventeen controls and is taller than a laptop screen with everything
     open; without this, "Add Stars" was the one item that fell off the bottom,
     which is exactly the wrong one to lose. */
  .rail-primary {
    position: sticky;
    bottom: calc(var(--space-3) * -1);
    z-index: 1;
    margin-top: var(--space-2);
    padding-block: var(--space-2);
    /* Opaque, so rows scrolling underneath do not show through it. The rail's
       own surface is translucent over the canvas; this matches what sits
       behind it rather than the canvas itself. */
    background: color-mix(in srgb, var(--surface-1) 96%, transparent);
  }

  /* ==========================================================================
     Transport bar
     ====================================================================== */
  .timeline-bar {
    position: fixed;
    left: 50%;
    bottom: var(--space-4);
    transform: translateX(-50%);
    z-index: var(--z-timeline);

    display: flex;
    align-items: center;
    gap: var(--space-2);
    width: min(680px, calc(100vw - var(--space-8)));
    padding: var(--space-2) var(--space-3);

    background: color-mix(in srgb, var(--surface-1) 88%, transparent);
    backdrop-filter: blur(18px) saturate(1.2);
    -webkit-backdrop-filter: blur(18px) saturate(1.2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow-lg);

    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration-slow) var(--ease-out);
  }

  .timeline-bar.showUI {
    opacity: 1;
    pointer-events: auto;
  }

  /* Centered in the band left of the rail, not on the viewport.
     
     The rail is docked over the right two hundred pixels, so a viewport-centered
     bar sits visibly right of the working area's center, and its right end runs
     into the only corner the footer can occupy. Shifting it by half the rail
     puts it where the eye expects it and opens that corner. The scenario card
     above already frames itself against the same band. */
  @media (min-width: 1025px) {
    .timeline-bar {
      transform: translateX(
        calc(-50% - (var(--rail-width) + var(--space-4)) / 2)
      );
    }
  }

  .timeline-bar.is-scrubbing {
    border-color: var(--warning);
    box-shadow:
      var(--shadow-lg),
      0 0 0 1px color-mix(in srgb, var(--warning) 45%, transparent);
  }

  .timeline-btn {
    flex: none;
    min-height: 30px;
    min-width: 30px;
    padding: 0 var(--space-2);
    font-size: var(--text-xs);
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--text-secondary);
  }

  .timeline-btn:hover {
    background: var(--surface-2);
    border-color: transparent;
    color: var(--text-primary);
    transform: none;
  }

  .timeline-scrubber {
    flex: 1;
    min-width: 0;
    margin: 0 var(--space-1);
  }

  .timeline-scrubber:disabled {
    opacity: 0.35;
  }

  .timeline-time {
    flex: none;
    min-width: 68px;
    text-align: right;
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }

  .timeline-live {
    color: var(--warning);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.05em;
  }

  @media (max-width: 640px) {
    .timeline-bar {
      width: calc(100vw - var(--space-4));
      bottom: var(--space-2);
      gap: var(--space-1);
      padding: var(--space-2);
    }

    .timeline-time {
      min-width: 52px;
      font-size: var(--text-2xs);
    }
  }

  /* ==========================================================================
     Toast
     ====================================================================== */
  .toast {
    position: fixed;
    left: 50%;
    bottom: calc(var(--space-10) + var(--space-8));
    transform: translate(-50%, 8px);
    z-index: var(--z-toast);

    padding: var(--space-2) var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow-lg);

    opacity: 0;
    pointer-events: none;
    transition:
      opacity var(--duration) var(--ease),
      transform var(--duration) var(--ease-out);
  }

  .toast.is-visible {
    opacity: 1;
    transform: translate(-50%, 0);
  }

  /* ==========================================================================
     Shortcut cheatsheet
     ====================================================================== */
  .shortcut-overlay {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: grid;
    place-items: center;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--surface-0) 78%, transparent);
    backdrop-filter: blur(6px);
    -webkit-backdrop-filter: blur(6px);
  }

  .shortcut-overlay[hidden] {
    display: none;
  }

  .shortcut-sheet {
    width: min(720px, 100%);
    max-height: 85vh;
    display: flex;
    flex-direction: column;
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
  }

  .shortcut-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-4) var(--space-5);
    border-bottom: 1px solid var(--border-subtle);
  }

  .shortcut-header h2 {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .shortcut-body {
    padding: var(--space-5);
    overflow-y: auto;
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--space-5);
  }

  .shortcut-group h3 {
    margin: 0 0 var(--space-3);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.09em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .shortcut-group dl {
    margin: 0;
  }

  .shortcut-row {
    display: flex;
    align-items: baseline;
    gap: var(--space-3);
    padding: var(--space-1) 0;
  }

  .shortcut-row dt {
    flex: none;
    min-width: 92px;
  }

  .shortcut-row dd {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--text-secondary);
  }

  .shortcut-overlay kbd {
    display: inline-block;
    padding: 2px var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-bottom-width: 2px;
    border-radius: var(--radius-sm);
    white-space: nowrap;
  }

  .shortcut-footer {
    padding: var(--space-3) var(--space-5);
    border-top: 1px solid var(--border-subtle);
    font-size: var(--text-xs);
    color: var(--text-muted);
  }

  /* ==========================================================================
     Scenario search
     ====================================================================== */
  .scenario-search-wrap {
    position: relative;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
  }

  #scenarioSearch {
    width: 100%;
    padding-left: var(--space-8);
  }

  .scenario-search-icon {
    position: absolute;
    left: calc(var(--space-4) + var(--space-2));
    top: 50%;
    transform: translateY(-50%);
    color: var(--text-muted);
    pointer-events: none;
    font-size: var(--text-sm);
  }

  .scenario-search-empty {
    padding: var(--space-6) var(--space-4);
    text-align: center;
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
}

/* =============================================================================
   Overlay panel: a single card holding the sonification toggle and the HUD
   readout, instead of two loose blocks with a floating minimize button.
   ========================================================================== */

@layer components {
  #overlay {
    position: fixed;
    top: var(--space-4);
    left: var(--space-4);
    z-index: var(--z-overlay);
    width: min(var(--hud-width), calc(100vw - var(--space-8)));

    display: flex;
    flex-direction: column;

    background: color-mix(in srgb, var(--hud-surface) 84%, transparent);
    backdrop-filter: blur(18px) saturate(1.2);
    -webkit-backdrop-filter: blur(18px) saturate(1.2);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
    overflow: hidden;
  }

  /* The card already provides the surface: the inner blocks must not repeat it */
  #overlay #overlayStats,
  #overlay .overlay-stats {
    background: transparent;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    padding: var(--space-3) var(--space-4);
  }

  /* The collapse control is a labeled chip, not a bare dash: a lone "−" gave
     no indication the readout could be collapsed, and once collapsed the panel
     became an unlabeled square with no hint of how to get it back. */
  .overlay-minimize-btn {
    position: absolute;
    top: var(--space-2);
    right: var(--space-2);
    z-index: 1;

    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);

    /* The legacy rule pins this to a 28px square, which clipped the label to
       a sliver. The chip has to size to its own text. */
    width: auto;
    min-width: 0;
    height: 24px;
    min-height: 24px;
    padding: 0 var(--space-3);

    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    line-height: 1;
    letter-spacing: 0.02em;
    color: var(--text-muted);

    white-space: nowrap;
    flex: none;

    background: color-mix(in srgb, var(--hud-text-strong) 8%, transparent);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-full);
    box-shadow: none;
  }

  .overlay-minimize-btn:hover {
    background: color-mix(in srgb, var(--hud-text-strong) 16%, transparent);
    border-color: var(--hud-border);
    color: var(--hud-text-strong);
    transform: none;
  }

  #overlay.minimized .sonification-panel,
  #overlay.minimized #overlayStats {
    display: none;
  }

  /* Collapsed: the panel shrinks to the labeled chip itself, so the whole
     thing reads as "Show readout" and is clickable anywhere. */
  /* Collapsed the panel shrink-wraps its chip, so the label must drive the
     width rather than being squeezed by a fixed one. */
  /* Collapsed the panel shrink-wraps its chip. align-items must be flex-start:
     the default `stretch` sized the button to the container instead of the
     other way round, squeezing the label to a single character per line. */
  #overlay.minimized {
    width: auto;
    max-width: calc(100vw - var(--space-8));
    min-width: 0;
    align-items: flex-start;
    cursor: pointer;
    background: color-mix(in srgb, var(--hud-surface) 88%, transparent);
  }

  #overlay.minimized:hover {
    border-color: var(--border-strong);
  }

  #overlay.minimized .overlay-minimize-btn {
    position: static;
    margin: var(--space-2);
    height: 28px;
    width: auto;
    flex: 0 0 auto;
    color: var(--hud-text-strong);
  }

  /* Section rules inside the readout, so the three blocks read as a list */
  #overlayStats > div + div,
  #overlayStats > p + p {
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    border-top: 1px solid var(--hud-border);
  }

  @media (max-width: 640px) {
    #overlay {
      top: var(--space-2);
      left: var(--space-2);
      width: calc(100vw - var(--space-4));
      font-size: var(--text-2xs);
    }
  }
}

/* =============================================================================
   Accessibility
   ========================================================================== */

@layer components {
  .skip-link {
    position: fixed;
    top: var(--space-2);
    left: var(--space-2);
    z-index: calc(var(--z-splash) + 1);
    padding: var(--space-2) var(--space-4);
    background: var(--accent);
    color: var(--accent-contrast);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    border-radius: var(--radius);
    text-decoration: none;
    transform: translateY(-200%);
    transition: transform var(--duration) var(--ease);
  }

  /* White rather than var(--focus-ring): the skip link's own background is
     the accent color, and an accent ring on it would be invisible. */
  .skip-link:focus-visible {
    transform: translateY(0);
    outline: 2px solid var(--text-primary);
    outline-offset: 2px;
  }

  /* Motion. The simulation itself is the content, so it keeps moving, but the
     decorative chrome animations, the splash and the transitions all stop. */
  @media (prefers-reduced-motion: reduce) {
    *,
    *::before,
    *::after {
      animation-duration: 0.01ms !important;
      animation-iteration-count: 1 !important;
      transition-duration: 0.01ms !important;
      scroll-behavior: auto !important;
    }

    .ui-button:hover,
    button:hover,
    .toggle-button:hover {
      transform: none;
    }
  }

  /* Windows high-contrast / forced-colors: let the OS palette through rather
     than fighting it with backdrop filters and translucent surfaces. */
  @media (forced-colors: active) {
    .ui-container,
    #overlay,
    .timeline-bar,
    .shortcut-sheet,
    .toast {
      backdrop-filter: none;
      -webkit-backdrop-filter: none;
      border: 1px solid CanvasText;
    }

    .ui-button,
    button,
    .toggle-button {
      border: 1px solid ButtonText;
    }
  }
}

/* =============================================================================
   Panel header controls
   ========================================================================== */

@layer components {
  .inspector-close,
  .inspector-delete,
  #closeScenarioList,
  .scenario-info-close {
    width: 30px;
    height: 30px;
    min-height: 30px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;

    font-size: var(--text-md);
    line-height: 1;
    color: var(--text-muted);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    box-shadow: none;
  }

  .inspector-close:hover,
  #closeScenarioList:hover,
  .scenario-info-close:hover {
    background: var(--surface-2);
    border-color: var(--border-subtle);
    color: var(--text-primary);
    transform: none;
  }

  .inspector-delete:hover {
    background: color-mix(in srgb, var(--danger) 14%, var(--surface-2));
    border-color: color-mix(in srgb, var(--danger) 45%, transparent);
    transform: none;
  }

  .inspector-controls {
    display: flex;
    align-items: center;
    gap: var(--space-1);
  }
}

/* =============================================================================
   Solar-unit notation
   -----------------------------------------------------------------------------
   Astronomical convention subscripts the Sun symbol: M⊙, R⊙, L⊙.
   ========================================================================== */

@layer components {
  .solar-sub {
    font-size: 0.72em;
    line-height: 1;
    vertical-align: baseline;
    position: relative;
    top: 0.28em;
    margin-left: 0.02em;
  }
}

/* =============================================================================
   Attribution
   -----------------------------------------------------------------------------
   Previously white text at 0.7 opacity inside a container faded to 0.45: an
   effective 0.31 alpha, which vanished against the darker themes. It now sits
   on its own tinted chip and takes its colors from the theme.
   ========================================================================== */

@layer components {
  .attribution {
    position: fixed;
    /* Tucked into the corner, below the control rail and to the right of the
       transport bar, so it reads as a footer rather than as another piece of
       chrome floating in the working area. */
    right: var(--space-3);
    bottom: var(--space-3);
    z-index: var(--z-overlay);

    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-3);

    font-family: var(--font-sans);
    font-size: var(--text-2xs);
    line-height: 1.4;
    color: var(--hud-text);
    white-space: nowrap;

    background: color-mix(in srgb, var(--hud-surface) 78%, transparent);
    backdrop-filter: blur(14px) saturate(1.15);
    -webkit-backdrop-filter: blur(14px) saturate(1.15);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-full);

    pointer-events: none;
    user-select: none;
    opacity: 0;
    transition: opacity var(--duration-slow) var(--ease-out);
  }

  .attribution.showUI {
    opacity: 1;
    pointer-events: auto;
  }

  .attribution a {
    color: var(--accent);
    text-decoration: none;
    border-bottom: 1px solid transparent;
    pointer-events: auto;
    transition:
      color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .attribution a:hover,
  .attribution a:focus-visible {
    color: var(--accent-strong);
    border-bottom-color: currentColor;
  }

  .attribution-sep {
    color: var(--text-muted);
    opacity: 0.7;
  }

  /* --- Theme dock -----------------------------------------------------------
     Docked in the footer pill, left of the copyright. Closed it is a swatch and
     a word; the list opens upward, because there is nothing below it. */
  .theme-dock {
    position: relative;
    display: flex;
    pointer-events: auto;
  }

  .theme-dock-btn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    /* Padding, not zero: collapsed to a swatch this is a 14px-wide control, and
       a 14px target is not one most people can hit. The negative margin keeps
       the extra area from pushing the row wider. */
    padding: var(--space-2);
    margin: calc(var(--space-2) * -1) 0 calc(var(--space-2) * -1)
      calc(var(--space-2) * -1);
    font: inherit;
    color: var(--hud-text);
    background: none;
    border: 0;
    cursor: pointer;
    transition: color var(--duration) var(--ease);
  }

  .theme-dock-btn:hover,
  .theme-dock-btn:focus-visible {
    color: var(--accent);
  }

  /* The swatch is the theme itself: page ground, panel and accent, in the three
     bands the interface actually uses. It recolors with the tokens, so it
     cannot fall out of step with what it is describing. */
  .theme-dock-swatch,
  .theme-menu-swatch {
    flex: none;
    width: 14px;
    height: 14px;
    border-radius: var(--radius-full);
    border: 1px solid var(--border);
    background: linear-gradient(
      135deg,
      var(--surface-0) 0 40%,
      var(--surface-2) 40% 70%,
      var(--accent) 70% 100%
    );
  }

  .theme-menu {
    position: absolute;
    right: 0;
    bottom: calc(100% + var(--space-3));
    z-index: 1;

    display: flex;
    flex-direction: column;
    gap: 2px;
    width: max-content;
    min-width: 13rem;
    padding: var(--space-2);

    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
  }

  .theme-menu-item {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3);
    font: inherit;
    font-size: var(--text-xs);
    text-align: left;
    color: var(--text-secondary);
    background: none;
    border: 0;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition:
      color var(--duration) var(--ease),
      background-color var(--duration) var(--ease);
  }

  .theme-menu-item:hover,
  .theme-menu-item:focus-visible {
    color: var(--text-primary);
    background: var(--surface-2);
  }

  .theme-menu-item.is-active {
    color: var(--text-primary);
    background: var(--accent-soft);
  }

  /* Each row previews its own theme rather than the active one, which is the
     only way a swatch helps before you have switched. */
  .theme-menu-swatch[data-swatch='midnight'] {
    background: linear-gradient(
      135deg,
      #000105 0 40%,
      #0d111c 40% 70%,
      #22d3ee 70% 100%
    );
  }
  .theme-menu-swatch[data-swatch='deep'] {
    background: linear-gradient(
      135deg,
      #070a16 0 40%,
      #161a2c 40% 70%,
      #38bdf8 70% 100%
    );
  }
  .theme-menu-swatch[data-swatch='observatory'] {
    background: linear-gradient(
      135deg,
      #0a0304 0 40%,
      #221310 40% 70%,
      #fb7185 70% 100%
    );
  }
  .theme-menu-swatch[data-swatch='daylight'] {
    background: linear-gradient(
      135deg,
      #eef2f9 0 40%,
      #f1f5fb 40% 70%,
      #0284c7 70% 100%
    );
  }

  .theme-menu-text {
    display: flex;
    flex-direction: column;
    gap: 1px;
  }

  .theme-menu-label {
    font-weight: var(--weight-semibold);
  }

  .theme-menu-hint {
    font-size: var(--text-2xs);
    line-height: 1.4;
    color: var(--text-muted);
  }

  /* Where the footer sits once the control rail is docked.

     The rail is docked against the right edge and painted above the footer, so
     a footer anchored to the viewport edge runs underneath it and its last
     items are unreachable. It anchors to the rail's inner edge instead, which
     leaves it sharing the bottom strip with the transport bar.

     That strip is the constraint on what may live here. Between the transport
     bar and the rail there is room for a short line and nothing more, which is
     why the documentation links moved to the rail: with seven items this had to
     be lifted above the scrubber, where a footer stops reading as a footer and
     starts reading as a panel someone forgot to close. */
  /* The rail's width is no longer reserved here. It was a fixed inset for a
     rail that only reaches this corner on a short window, and it cost every
     wide window a quarter of its width; css/chrome.css sets the inset from a
     measurement of where the rail actually ends. */

  /* The transport bar is centered on the viewport while the rail takes the right
     two hundred pixels, so the gap between them closes as the window narrows.
     Rather than move the line somewhere else at a breakpoint, it tightens:
     smaller, closer, and without the separator dots, which is enough to keep it
     clear of the elapsed-time readout down to the width where the rail stops
     being docked at all. */
  @media (min-width: 1321px) and (max-width: 1599px) {
    .attribution {
      gap: var(--space-1);
      padding: 2px var(--space-2);
      font-size: 10px;
    }

    .attribution .attribution-sep {
      display: none;
    }

    /* The last pixels. The symbol carries the notice and the swatch carries the
       theme; the words beside them are what a reader can lose without losing
       the meaning, and both come back as soon as there is room. */
    .attribution-year,
    .theme-dock-name {
      display: none;
    }
  }

  /* Below the rail's breakpoint there is no rail to clear, so the footer goes
     back to the viewport edge. */
  @media (max-width: 1024px) {
    .attribution {
      right: var(--space-2);
    }
  }

  /* The transport bar is a fixed 680px, so below about 1250px it fills the
     strip and there is no room for a footer beside it. Only then does the line
     lift clear of the bar: it is the last resort, not the default, because a
     footer floating above the scrubber reads as a panel someone forgot to close
     rather than as a footer. */
  @media (max-width: 1320px) {
    .attribution {
      bottom: calc(var(--space-10) + var(--space-6));
      font-size: 10px;
      padding: 2px var(--space-2);
      gap: var(--space-1);
    }
  }
}

/* =============================================================================
   Guided tour
   ========================================================================== */

@layer components {
  .tutorial-popup {
    position: fixed;
    left: var(--space-4);
    bottom: calc(var(--space-10) + var(--space-8));
    width: min(400px, calc(100vw - var(--space-8)));
    max-height: 78vh;
    z-index: var(--z-modal);

    display: flex;
    flex-direction: column;
    overflow: hidden;

    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    font-family: var(--font-sans);
    user-select: none;
  }

  .tutorial-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--space-3) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
    background: var(--surface-2);
  }

  .tutorial-eyebrow {
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .tutorial-close {
    width: 26px;
    height: 26px;
    min-height: 26px;
    padding: 0;
    font-size: var(--text-xs);
    background: transparent;
    border-color: transparent;
    box-shadow: none;
    color: var(--text-muted);
  }

  .tutorial-close:hover {
    background: var(--surface-3);
    border-color: transparent;
    color: var(--text-primary);
    transform: none;
  }

  .tutorial-body {
    padding: var(--space-5);
    overflow-y: auto;
  }

  .tutorial-step-count {
    margin: 0 0 var(--space-2);
    font-size: var(--text-2xs);
    font-weight: var(--weight-medium);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--accent);
  }

  .tutorial-title {
    margin: 0 0 var(--space-3);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: var(--leading-tight);
    color: var(--text-primary);
  }

  .tutorial-text {
    margin: 0;
    font-size: var(--text-base);
    line-height: var(--leading-normal);
    color: var(--text-secondary);
  }

  .tutorial-tip {
    display: flex;
    gap: var(--space-2);
    margin: var(--space-4) 0 0;
    padding: var(--space-3);

    font-size: var(--text-xs);
    line-height: var(--leading-normal);
    color: var(--text-secondary);

    background: var(--accent-soft);
    border-left: 2px solid var(--accent);
    border-radius: var(--radius-sm);
  }

  .tutorial-dots {
    display: flex;
    gap: 6px;
    margin-top: var(--space-5);
  }

  .tutorial-dots span {
    width: 6px;
    height: 6px;
    border-radius: var(--radius-full);
    background: var(--surface-3);
    transition: background var(--duration) var(--ease);
  }

  .tutorial-dots span.is-done {
    background: var(--accent-deep);
  }

  .tutorial-dots span.is-current {
    width: 18px;
    background: var(--accent);
  }

  .tutorial-footer {
    display: flex;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border-subtle);
    background: var(--surface-2);
  }

  .tutorial-footer .ui-button {
    min-width: 88px;
    justify-content: center;
  }

  /* Spotlight: four scrim panels leave the highlighted control undimmed. */
  .tutorial-scrim {
    position: fixed;
    z-index: calc(var(--z-modal) - 2);
    background: color-mix(in srgb, var(--surface-0) 72%, transparent);
    pointer-events: none;
  }

  .tutorial-ring {
    position: fixed;
    z-index: calc(var(--z-modal) - 1);
    border: 2px solid var(--accent);
    box-shadow:
      0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent),
      0 0 28px color-mix(in srgb, var(--accent) 45%, transparent);
    pointer-events: none;
  }

  @media (max-width: 600px) {
    .tutorial-popup {
      left: var(--space-3);
      right: var(--space-3);
      width: auto;
      bottom: calc(var(--space-10) + var(--space-6));
      max-height: 70vh;
    }
  }
}

/* =============================================================================
   Small screens
   -----------------------------------------------------------------------------
   The phone layout is not the desktop layout scaled down. Three fixed regions
   compete for a 375px-wide screen, so each gets an explicit place:

     top-left      collapsed readout chip (tap to expand)
     top-right     menu button
     bottom        transport bar, full width, above the home indicator

   Everything else either folds into the menu or moves out of the way. Touch
   targets are 44px, the iOS minimum, rather than the 26-30px the desktop
   chrome uses.
   ========================================================================== */

@layer components {
  @media (max-width: 720px) {
    /* --- Safe areas: content must clear the notch and home indicator ------ */
    .timeline-bar {
      left: var(--space-2);
      right: var(--space-2);
      bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
      width: auto;
      transform: none;
      gap: var(--space-1);
      padding: var(--space-2) var(--space-2);
    }

    .timeline-btn {
      min-width: 44px;
      min-height: 44px;
      font-size: var(--text-sm);
    }

    .timeline-scrubber {
      margin: 0 var(--space-1);
    }

    /* A range input needs a much bigger thumb to be draggable with a finger */
    .timeline-scrubber::-webkit-slider-thumb {
      width: 22px;
      height: 22px;
    }

    .timeline-scrubber::-moz-range-thumb {
      width: 22px;
      height: 22px;
    }

    .timeline-time {
      min-width: 46px;
      font-size: var(--text-2xs);
    }

    /* --- Readout: collapsed by default, tap to expand -------------------- */
    #overlay {
      top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
      left: calc(var(--space-2) + env(safe-area-inset-left, 0px));
      right: auto;
      width: auto;
      max-width: calc(100vw - 76px - var(--space-4));
      max-height: calc(100vh - 180px);
      overflow-y: auto;
      font-size: var(--text-2xs);
    }

    .overlay-minimize-btn {
      min-height: 34px;
      height: 34px;
      padding: 0 var(--space-3);
      font-size: var(--text-xs);
    }

    #overlay.minimized .overlay-minimize-btn {
      height: 36px;
    }

    /* --- Menu button clears the notch ------------------------------------ */
    .mobile-menu-toggle {
      top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
      right: calc(var(--space-2) + env(safe-area-inset-right, 0px));
      width: 44px;
      height: 44px;
      min-height: 44px;
    }

    .ui-container {
      top: calc(var(--space-2) + 52px + env(safe-area-inset-top, 0px));
      left: var(--space-2);
      right: var(--space-2);
      max-height: calc(100vh - 200px - env(safe-area-inset-top, 0px));
      padding: var(--space-2);
    }

    .ui-container .ui-button,
    .rail-toggle {
      min-height: 44px;
      font-size: var(--text-base);
    }

    .rail-field {
      padding: var(--space-2) var(--space-3);
    }

    .rail-field select {
      min-height: 40px;
    }

    /* --- Tutorial button: was sitting on top of the transport bar --------- */
    .tutorial-btn {
      left: auto;
      right: calc(var(--space-2) + env(safe-area-inset-right, 0px));
      bottom: calc(
        var(--transport-reserve, 76px) + env(safe-area-inset-bottom, 0px)
      );
      width: 44px;
      height: 44px;
      min-height: 44px;
    }

    .tutorial-popup {
      left: var(--space-2);
      right: var(--space-2);
      width: auto;
      bottom: calc(
        var(--transport-reserve, 76px) + env(safe-area-inset-bottom, 0px)
      );
      max-height: 62vh;
    }

    /* --- Attribution: out of the transport bar's way --------------------- */
    .attribution {
      left: calc(var(--space-2) + env(safe-area-inset-left, 0px));
      right: auto;
      bottom: calc(
        var(--transport-reserve, 76px) + env(safe-area-inset-bottom, 0px)
      );
      font-size: 10px;
      padding: 3px var(--space-2);
      max-width: calc(100vw - 76px);
      overflow: hidden;
      text-overflow: ellipsis;
    }

    /* --- Modals become full-height sheets --------------------------------
       #scenarioListContent is a flex child of its modal, not a fixed element,
       so left/right do nothing for it: it needs width/height instead, and
       min-width must be released or a 320px floor overflows a 375px screen. */
    #scenarioListModal:not(.hidden) {
      padding: calc(var(--space-2) + env(safe-area-inset-top, 0px))
        var(--space-2) calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
      align-items: stretch;
      justify-content: stretch;
    }

    #scenarioListContent {
      width: 100%;
      min-width: 0;
      max-width: none;
      max-height: 100%;
      padding: var(--space-4) var(--space-3);
      border-radius: var(--radius-lg);
    }

    #objectInspector,
    #settingsPanel {
      top: calc(var(--space-2) + env(safe-area-inset-top, 0px));
      left: var(--space-2);
      right: var(--space-2);
      bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
      width: auto;
      max-width: none;
      max-height: none;
      transform: none;
      border-radius: var(--radius-lg);
    }

    #objectInspector.visible {
      transform: none;
    }

    .inspector-tab,
    .scenario-list-item,
    .settings-section-header,
    #scenarioSearch,
    input[type='text'],
    input[type='number'],
    input[type='search'],
    select {
      min-height: 44px;
    }

    /* A 4px range track is unhittable with a fingertip */
    input[type='range'] {
      height: 6px;
    }

    input[type='range']::-webkit-slider-thumb {
      width: 24px;
      height: 24px;
    }

    input[type='range']::-moz-range-thumb {
      width: 24px;
      height: 24px;
    }

    .settings-footer,
    .bh-masses-footer {
      flex-wrap: wrap;
      gap: var(--space-2);
    }

    .settings-footer .ui-button,
    .bh-masses-footer .ui-button {
      flex: 1 1 40%;
      min-height: 44px;
      justify-content: center;
    }

    /* Scenario cards: one column, comfortable tap height */
    #scenarioListItems {
      grid-template-columns: 1fr;
    }

    /* --- Scenario info card sits under the top chrome -------------------- */
    #scenarioInfoBox,
    .scenario-info-box {
      top: calc(64px + env(safe-area-inset-top, 0px));
      left: var(--space-2);
      right: var(--space-2);
      width: auto;
      transform: none;
    }
  }

  /* Placement is armed by a long press on touch devices; this confirms it. */
  .placement-armed-hint {
    position: fixed;
    left: 50%;
    top: calc(var(--space-8) + env(safe-area-inset-top, 0px));
    transform: translateX(-50%);
    z-index: var(--z-toast);

    padding: var(--space-2) var(--space-4);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--accent-contrast);
    background: var(--accent);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow-lg);

    opacity: 0;
    pointer-events: none;
    transition: opacity var(--duration) var(--ease);
  }

  .placement-armed-hint.is-visible {
    opacity: 1;
  }
}

/* =============================================================================
   Scenario browser surface
   ========================================================================== */

@layer components {
  #scenarioListContent {
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }

  .scenario-list-item {
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    cursor: pointer;
    transition:
      background var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .scenario-list-item:hover,
  .scenario-list-item:focus-visible {
    background: var(--surface-3);
    border-color: var(--border-strong);
  }
}

/* =============================================================================
   Share dialog
   -----------------------------------------------------------------------------
   Built on the tokens rather than following the older modals, which hardcode
   their gradients and so ignore three of the four themes.
   ========================================================================== */
@layer components {
  #shareModal {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--bg-color) 88%, transparent);
    backdrop-filter: blur(8px);
  }
  #shareModal.hidden {
    display: none;
  }

  #shareContent {
    position: relative;
    width: min(560px, 100%);
    max-height: min(90vh, 720px);
    overflow-y: auto;
    padding: var(--space-6);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }

  .share-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
  }
  .share-header h3 {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    letter-spacing: -0.01em;
  }
  .share-header .ui-button {
    flex: none;
    min-width: 0;
    padding: 2px 10px;
    font-size: var(--text-lg);
    line-height: 1.1;
  }

  .share-intro {
    margin: 0 0 var(--space-5);
    color: var(--text-secondary);
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
  }

  /* --- Which payload ------------------------------------------------------ */
  .share-kind {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
  }
  .share-kind-option {
    display: flex;
    flex-direction: column;
    gap: 3px;
    padding: var(--space-3);
    text-align: left;
    cursor: pointer;
    background: var(--button-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    color: var(--text-secondary);
    font-family: inherit;
    transition:
      border-color var(--duration-fast) var(--ease),
      background var(--duration-fast) var(--ease);
  }
  .share-kind-option:hover {
    background: var(--button-hover-bg);
    border-color: var(--border-strong);
  }
  .share-kind-option.is-active {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--text-primary);
  }
  .share-kind-name {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }
  .share-kind-note {
    font-size: var(--text-xs);
    line-height: 1.35;
    color: var(--text-muted);
  }

  /* --- Options ------------------------------------------------------------ */
  .share-check {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-4);
    font-size: var(--text-sm);
    color: var(--text-secondary);
    cursor: pointer;
  }
  .share-check input {
    accent-color: var(--accent);
    width: 15px;
    height: 15px;
    cursor: pointer;
  }

  /* --- The link ----------------------------------------------------------- */
  .share-link-row {
    display: flex;
    gap: var(--space-2);
    align-items: stretch;
  }
  #shareUrl {
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    /* The payload is one long unbroken token; without this it is impossible to
       tell a truncated link from a complete one. */
    text-overflow: ellipsis;
  }
  #shareUrl:focus {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }
  .share-link-row .ui-button {
    flex: none;
    min-width: 74px;
  }

  .share-meta {
    margin: var(--space-2) 0 0;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .share-meta.is-warning {
    color: var(--warning);
  }
  .share-warning {
    margin: var(--space-2) 0 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--warning) 12%, transparent);
    border-left: 2px solid var(--warning);
    border-radius: var(--radius-sm);
  }

  /* --- Seed --------------------------------------------------------------- */
  .share-seed-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-5);
    padding-top: var(--space-4);
    border-top: 1px solid var(--border-subtle);
  }
  .share-seed-row[hidden] {
    display: none;
  }
  .share-seed-row label {
    font-size: var(--text-sm);
    color: var(--text-secondary);
  }
  #shareSeed {
    flex: 1 1 auto;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-primary);
  }
  #shareSeed:focus {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }
  .share-seed-row .ui-button {
    flex: none;
    min-width: 60px;
  }
  .share-hint {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--text-muted);
  }

  @media (max-width: 560px) {
    #shareContent {
      padding: var(--space-4);
    }
    .share-kind {
      grid-template-columns: 1fr;
    }
    /* Below 480px the floating close chip appears; showing the header button
       as well leaves two X controls a few pixels apart, which is exactly the
       duplication the rest of the interface was cleaned up to avoid. */
    @media (max-width: 480px) {
      .share-header .ui-button {
        display: none;
      }
    }
    /* Stacked, the Copy button gets a full-width target instead of a 74px one
       squeezed against a field that already cannot show the whole link. */
    .share-link-row {
      flex-wrap: wrap;
    }
    .share-link-row .ui-button {
      width: 100%;
    }
  }
}

@layer components {
  .share-note {
    margin: var(--space-2) 0 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--text-secondary);
    background: var(--surface-2);
    border-left: 2px solid var(--accent);
    border-radius: var(--radius-sm);
  }
  .share-note[hidden] {
    display: none;
  }
}

/* =============================================================================
   Investigations
   -----------------------------------------------------------------------------
   The lesson panel is docked rather than modal: a student has to read a step
   and drive the simulation at the same time, so the panel must never be the
   thing standing between them and the canvas.
   ========================================================================== */
@layer components {
  /* --- Lesson browser ----------------------------------------------------- */
  #investigationBrowser {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--bg-color) 88%, transparent);
    backdrop-filter: blur(8px);
  }
  #investigationBrowser.hidden {
    display: none;
  }

  /* Narrower than the scenario gallery's 1180px on purpose. Two wide landscape
     cards is the layout; three would make them scenario tiles with extra text,
     which is the distinction this panel exists to draw. */
  #investigationBrowserContent {
    position: relative;
    width: min(1040px, calc(100vw - var(--space-8)));
    max-height: min(88vh, 900px);
    padding: var(--space-6) var(--space-6) 0;

    display: flex;
    flex-direction: column;
    /* The header and the count line stay put; only the grid scrolls, the same
       way the gallery keeps its filters reachable. */
    overflow: hidden;

    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }

  .inv-browser-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
    margin-bottom: var(--space-4);
  }
  .inv-browser-header h3 {
    margin: 0 0 var(--space-1);
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    letter-spacing: -0.01em;
  }
  .inv-browser-sub {
    margin: 0;
    max-width: 66ch;
    font-size: var(--text-sm);
    line-height: var(--leading-normal);
    color: var(--text-secondary);
  }
  .inv-browser-header .ui-button {
    flex: none;
    min-width: 0;
    padding: 2px 10px;
    font-size: var(--text-lg);
  }

  /* --- Count line ---------------------------------------------------------- */
  /* Echoes the gallery's .sc-count so the two panels read as one family, with
     the shared level appended in sentence case: it is a fact about the set, not
     a pill worth repeating on all eight cards. */
  .inv-browser-count-row {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: var(--space-1) var(--space-3);
    margin: 0 0 var(--space-3);
  }
  .inv-count {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-count-level {
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .inv-count-level[hidden] {
    display: none;
  }

  /* --- Search and filters ---------------------------------------------------- */
  /* A search box on its own row and four menus below it. Native <select>s
     rather than custom popovers: they are keyboard-operable and screen-reader
     legible without any work from us, and on a phone they open the platform's
     own picker, which is a better control than anything drawn here. */

  .inv-browser-tools {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin: 0 0 var(--space-4);
  }

  .inv-search-input {
    width: 100%;
    box-sizing: border-box;
    padding: var(--space-2) var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: rgb(0 0 0 / 18%);
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
  }
  .inv-search-input:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 1px;
    border-color: var(--accent-line);
  }
  .inv-search-input::placeholder {
    color: var(--text-muted);
  }

  .inv-filters {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2) var(--space-3);
  }
  .inv-filter {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    /* Each menu takes an equal share of the row and shrinks together, so a long
       translated option label does not push the last one onto its own line. */
    flex: 1 1 11rem;
  }
  .inv-filter > span {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-filter select {
    width: 100%;
    min-width: 0;
    padding: var(--space-1) var(--space-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: rgb(0 0 0 / 18%);
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
  }
  .inv-filter select:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }
  .inv-filter-clear {
    flex: 0 0 auto;
    font-size: var(--text-xs);
  }
  .inv-filter-clear[hidden] {
    display: none;
  }

  /* --- Nothing matched ------------------------------------------------------- */

  .inv-empty {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-3);
    margin: var(--space-5) 0 0;
    padding: var(--space-4);
    border: 1px dashed var(--border);
    border-radius: var(--radius-md);
    color: var(--text-muted);
    font-size: var(--text-sm);
  }
  .inv-empty[hidden] {
    display: none;
  }
  .inv-empty button[hidden] {
    display: none;
  }

  /* --- The curated orders ---------------------------------------------------- */
  /* Shown above the grid when nothing is filtered, and taken away the moment a
     reader starts searching: "where do I start" and "where is the transit one"
     are different questions and only one of them is being asked at a time. */

  .inv-sequences[hidden],
  .inv-all-heading[hidden] {
    display: none;
  }
  .inv-sequences h4,
  .inv-all-heading {
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-all-heading {
    margin: var(--space-6) 0 var(--space-3);
  }
  .inv-seq-intro {
    margin: 0 0 var(--space-4);
    max-width: 60ch;
    font-size: var(--text-sm);
    color: var(--text-muted);
  }
  .inv-seq-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(19rem, 1fr));
    gap: var(--space-4);
  }
  .inv-seq {
    padding: var(--space-4);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: rgb(255 255 255 / 2%);
  }
  .inv-seq-title {
    margin: 0 0 var(--space-1);
    font-size: var(--text-base);
    color: var(--text);
  }
  .inv-seq-blurb {
    margin: 0 0 var(--space-3);
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .inv-seq-steps {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .inv-seq-step {
    display: grid;
    grid-template-columns: 1.6rem 1fr;
    gap: var(--space-2);
    align-items: start;
  }
  .inv-seq-num {
    display: grid;
    place-items: center;
    width: 1.6rem;
    height: 1.6rem;
    border: 1px solid var(--border);
    border-radius: 50%;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }
  .inv-seq-step.is-complete .inv-seq-num {
    border-color: var(--accent-line);
    color: var(--accent);
  }
  .inv-seq-body {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }
  .inv-seq-open {
    /* A link in behavior, a button in fact: it fetches a lesson rather than
       navigating, so it must not claim to be an address. */
    align-self: flex-start;
    padding: 0;
    border: 0;
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
    text-align: left;
    text-decoration: underline;
    text-underline-offset: 3px;
    text-decoration-color: var(--border-strong);
    cursor: pointer;
  }
  .inv-seq-open:hover,
  .inv-seq-open:focus-visible {
    color: var(--accent);
    text-decoration-color: currentcolor;
  }
  .inv-seq-open:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 2px;
    border-radius: 2px;
  }
  .inv-seq-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-seq-fit.is-demo,
  .inv-seq-done {
    color: var(--accent);
  }
  .inv-seq-why,
  .inv-seq-needs {
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .inv-seq-needs {
    font-style: italic;
  }
  .inv-seq-assign {
    align-self: flex-start;
    margin-top: 2px;
    font-size: var(--text-xs);
  }

  /* --- The grid ------------------------------------------------------------- */

  .inv-browser-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: var(--space-6);
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
  }

  /* 27rem, against the gallery's 15.5rem: a lesson card is landscape, with the
     thumbnail beside the text rather than above it, and it needs the width for
     a subtitle, a summary and a progress row to sit legibly side by side. */
  .inv-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(27rem, 1fr));
    gap: var(--space-4);
  }

  /* --- The card ------------------------------------------------------------- */

  .inv-card {
    display: flex;
    flex-direction: row;
    /* The generic button rule centers both axes. In this card that would center
       every line of text; restated rather than left to inherit. */
    align-items: stretch;
    justify-content: flex-start;
    gap: 0;
    width: 100%;
    padding: 0;
    overflow: hidden;
    text-align: left;
    cursor: pointer;
    font-family: inherit;
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    transition:
      transform var(--duration) var(--ease-out),
      border-color var(--duration) var(--ease),
      box-shadow var(--duration) var(--ease);
  }

  .inv-card:hover,
  .inv-card:focus-visible {
    transform: translateY(-2px);
    border-color: var(--accent-line);
    box-shadow: var(--shadow);
  }

  /* --- Thumbnail ------------------------------------------------------------ */
  /* A wrapper around the shared .sc-card-shot block, which is what the gallery
     and the front door use: same lazy load, same titled fallback when a capture
     is missing. The wrapper exists only to hang the two overlays on. */
  .inv-card-shot {
    position: relative;
    flex: 0 0 40%;
    /* Never shrink, and never be sized by the text beside it. A flex item with
       only an aspect ratio collapses to a sliver when its sibling is tall. */
    align-self: stretch;
    min-width: 8.5rem;
    max-width: 13rem;
    border-right: 1px solid var(--border-subtle);
  }

  /* The shot fills the card's full height here rather than holding a 16/9 box:
     a landscape card is as tall as its text, and letterboxing the capture would
     leave two gray bands. The aspect ratio the gallery sets has to be undone. */
  .inv-card .sc-card-shot {
    position: absolute;
    inset: 0;
    aspect-ratio: auto;
    border-bottom: none;
  }

  /* The lesson's place in the list, set into the corner of the capture. The
     gallery's tiles carry no such number, and that is the difference being
     drawn: these are ordered, and working down them in order is the intent. */
  .inv-card-index {
    position: absolute;
    top: 0;
    left: 0;
    padding: 3px var(--space-2) 4px;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: var(--accent-contrast);
    background: color-mix(in srgb, var(--accent) 88%, transparent);
    border-bottom-right-radius: var(--radius-sm);
  }

  /* Borrowed from every video thumbnail ever made, because it answers the
     question a student actually has about a guided lesson: how long is this?
     On the HUD tokens rather than the panel ones: it sits on a capture of the
     starfield, which stays dark in every theme including Daylight, so panel
     text colors would put dark type on a dark image. */
  .inv-card-clock {
    position: absolute;
    right: var(--space-2);
    bottom: var(--space-2);
    padding: 2px var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    line-height: 1.5;
    white-space: nowrap;
    color: var(--hud-text-strong);
    background: color-mix(in srgb, var(--hud-surface) 82%, transparent);
    border: 1px solid var(--hud-border);
    border-radius: var(--radius-sm);
    backdrop-filter: blur(4px);
  }

  /* --- Card text ------------------------------------------------------------ */

  .inv-card-body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--space-2);
    min-width: 0;
    padding: var(--space-4);
  }

  /* Always present, even with nothing in it. Only some lessons belong to a
     series, and letting the row collapse on the others started their titles a
     pill's height above their neighbors' - a ragged top edge across every row
     of the grid, for the sake of 20px. */
  .inv-card-flags {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1) var(--space-2);
    min-height: 1.35rem;
  }

  .inv-flag {
    padding: 1px var(--space-2);
    font-size: var(--text-2xs);
    line-height: 1.6;
    color: var(--text-muted);
    background: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-full);
    white-space: nowrap;
  }

  /* The one flag worth an accent: it says this lesson has lessons either side
     of it, which is the strongest signal that this list is a course. */
  .inv-flag.is-series {
    color: var(--text-secondary);
    border-color: var(--accent-line);
    background: color-mix(in srgb, var(--accent) 10%, transparent);
  }
  .inv-flag.is-series b {
    font-family: var(--font-mono);
    font-weight: var(--weight-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--accent);
  }

  .inv-card-title {
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    line-height: 1.3;
    color: var(--text-primary);
  }

  /* The line the gallery's tiles have no room for. It says what the student
     will do, which is a different promise from what the lesson is about. */
  .inv-card-sub {
    font-size: var(--text-sm);
    line-height: 1.4;
    color: var(--text-secondary);
  }

  .inv-card-summary {
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-muted);
    /* Clamped, not rewritten: the full summary stays in the catalog and reaches
       the card's title attribute. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
  }

  .inv-card-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1) var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* --- Footer: progress, and what the card promises -------------------------- */

  .inv-card-foot {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    /* Pinned to the bottom so the call to action lines up across a row however
       differently the summaries wrap. */
    margin-top: auto;
    padding-top: var(--space-1);
  }

  .inv-card-progress {
    display: flex;
    align-items: center;
    gap: var(--space-3);
  }

  .inv-card-bar {
    flex: 1 1 auto;
    height: 4px;
    background: var(--surface-3);
    border-radius: var(--radius-full);
    overflow: hidden;
  }
  .inv-card-bar > span {
    display: block;
    height: 100%;
    background: var(--accent);
    border-radius: var(--radius-full);
  }

  .inv-card-status {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    white-space: nowrap;
    color: var(--text-muted);
  }
  .inv-card-status.is-done {
    color: var(--accent);
  }

  /* Not a nested button - the card is the control - but it looks like the thing
     you are about to press, and it says which of the three things it does:
     start, resume at the step you left off at, or reread a finished one. */
  .inv-card-cta {
    align-self: flex-start;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    color: var(--accent);
    transition: transform var(--duration) var(--ease-out);
  }
  .inv-card:hover .inv-card-cta,
  .inv-card:focus-visible .inv-card-cta {
    transform: translateX(2px);
  }

  /* A finished lesson keeps its accent on the bar and the tick, and nothing
     else: it should read as done, not as the loudest card in the grid. */
  .inv-card.is-complete .inv-card-shot {
    border-right-color: var(--accent-line);
  }

  /* --- Narrow screens -------------------------------------------------------- */

  @media (max-width: 760px) {
    .inv-grid {
      grid-template-columns: 1fr;
    }
    .inv-seq-list {
      grid-template-columns: 1fr;
    }
    /* Two menus per row rather than four squeezed into one: below this width
       an 11rem menu and a Spanish option label do not fit side by side. */
    .inv-filter {
      flex-basis: calc(50% - var(--space-3));
    }
    /* One card per row has no neighbor to line up with, so the reserved flag
       row is just a gap above the title. */
    .inv-card-flags.is-empty {
      display: none;
    }
  }

  /* Below this the split card stops working: the thumbnail is either too narrow
     to show anything or steals the width the text needs, so the card stacks and
     becomes a taller version of a gallery tile. */
  @media (max-width: 560px) {
    .inv-card {
      flex-direction: column;
    }
    .inv-card-shot {
      flex: 0 0 auto;
      max-width: none;
      aspect-ratio: 16 / 9;
      border-right: none;
      border-bottom: 1px solid var(--border-subtle);
    }
    .inv-card.is-complete .inv-card-shot {
      border-bottom-color: var(--accent-line);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .inv-card:hover,
    .inv-card:focus-visible {
      transform: none;
    }
    .inv-card:hover .inv-card-cta,
    .inv-card:focus-visible .inv-card-cta {
      transform: none;
    }
  }

  /* --- The working panel -------------------------------------------------- */
  .investigation-panel {
    position: fixed;
    top: var(--space-3);
    left: var(--space-3);
    /* Clears the transport bar, which is fixed to the bottom at a higher
       z-index: overlapping it would put the Next button underneath the
       scrubber - and the bar wins the hit test, so the button stops working
       rather than merely looking wrong.
       This was 88px, written as "the bar's 48px plus its 16px offset plus a
       gap". The bar is only 48px tall while it fits on one row; between about
       640px and 1000px of window it wraps to 62px and 88px was short.
       js/bottomDock.js measures it and publishes the answer. The constant
       remains as the fallback for the frame before the first measurement. */
    bottom: var(--transport-reserve, 88px);
    z-index: var(--z-panel);
    display: flex;
    flex-direction: column;
    width: min(396px, 40vw);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
    overflow: hidden;
  }
  .investigation-panel[hidden] {
    display: none;
  }

  .inv-header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-4) var(--space-4) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
  }
  .inv-header h2 {
    margin: 0;
    font-size: var(--text-md);
    font-weight: var(--weight-semibold);
    letter-spacing: -0.01em;
  }
  .inv-header p {
    margin: 2px 0 0;
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-muted);
  }
  .inv-header .ui-button {
    flex: none;
    min-width: 0;
    padding: 0 8px;
    font-size: var(--text-lg);
  }

  .inv-progress {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4);
    border-bottom: 1px solid var(--border-subtle);
  }
  .inv-progress-track {
    flex: 1;
    height: 4px;
    background: var(--surface-2);
    border-radius: var(--radius-full);
    overflow: hidden;
  }
  #investigationProgressBar {
    height: 100%;
    width: 0;
    background: var(--accent);
    transition: width var(--duration) var(--ease);
  }
  #investigationProgressText {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    /* As above: small text carrying real information needs the 4.5:1 tone. */
    color: var(--text-secondary);
  }

  .inv-body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: var(--space-4);
  }

  .inv-step-count {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    /* --text-muted measures 4.3:1 here, just under the 4.5:1 AA needs at this
       size. Secondary clears it, and this text tells a student where they are
       in the lesson, which is not decoration. */
    color: var(--text-secondary);
  }
  .inv-step-kind {
    padding: 1px 7px;
    font-size: 0.62rem;
    color: var(--accent);
    background: var(--accent-soft);
    border-radius: var(--radius-full);
  }
  .inv-step-title {
    margin: 0 0 var(--space-3);
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    line-height: 1.22;
    letter-spacing: -0.012em;
  }
  .inv-step-body p {
    margin: 0 0 var(--space-3);
    font-size: var(--text-sm);
    line-height: 1.62;
    color: var(--text-secondary);
  }
  .inv-step-body strong {
    color: var(--text-primary);
    font-weight: var(--weight-semibold);
  }

  .inv-tip {
    margin: 0 0 var(--space-4);
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
    background: var(--surface-2);
    border-left: 2px solid var(--accent);
    border-radius: var(--radius-sm);
  }

  .inv-prompt {
    margin: var(--space-4) 0 var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    line-height: 1.45;
    color: var(--text-primary);
  }

  /* --- Multiple choice ---------------------------------------------------- */
  .inv-options {
    display: grid;
    gap: var(--space-2);
  }
  .inv-option {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    text-align: left;
    font-family: inherit;
    font-size: var(--text-sm);
    line-height: 1.45;
    cursor: pointer;
    color: var(--text-secondary);
    background: var(--button-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    transition:
      border-color var(--duration-fast) var(--ease),
      background var(--duration-fast) var(--ease);
  }
  .inv-option:hover:not(:disabled) {
    background: var(--button-hover-bg);
    border-color: var(--border-strong);
  }
  .inv-option:disabled {
    cursor: default;
  }
  .inv-option-mark {
    flex: none;
    width: 19px;
    height: 19px;
    display: grid;
    place-items: center;
    font-family: var(--font-mono);
    font-size: 0.66rem;
    color: var(--text-muted);
    background: var(--surface-2);
    border-radius: var(--radius-full);
  }
  /* The chosen answer and the right answer are marked differently on purpose:
     a student who picked wrongly needs to see both at once. */
  .inv-option.is-chosen {
    border-color: var(--border-strong);
    color: var(--text-primary);
  }
  .inv-option.is-correct {
    border-color: var(--accent);
    background: var(--accent-soft);
    color: var(--text-primary);
  }
  .inv-option.is-correct .inv-option-mark {
    color: var(--accent-contrast);
    background: var(--accent);
  }
  .inv-option.is-wrong {
    border-color: var(--danger);
    background: color-mix(in srgb, var(--danger) 10%, transparent);
  }

  /* A committed prediction that has not been marked yet. Deliberately not
     colored right or wrong: the point of holding it is that neither is known
     until the experiment has run. */
  .inv-option.is-held {
    border-style: dashed;
  }
  .inv-held {
    margin: var(--space-3) 0 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
    border-left: 2px dashed var(--border-strong);
  }

  /* The verdict on a held prediction, shown on the step where the result
     arrives rather than on the one the reader has already left. */
  .inv-verdict {
    margin: var(--space-3) 0 0;
    padding: var(--space-3);
    border-radius: var(--radius-sm);
    border-left: 3px solid var(--border-strong);
    background: var(--surface-2);
  }
  .inv-verdict.is-right {
    border-left-color: var(--accent);
  }
  .inv-verdict.is-wrong {
    border-left-color: var(--danger);
  }
  .inv-verdict-head {
    margin: 0;
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .inv-verdict-mark {
    margin: var(--space-1) 0 0;
    font-size: var(--text-sm);
    color: var(--text-primary);
  }
  .inv-verdict .inv-because {
    background: none;
    padding: var(--space-2) 0 0;
  }

  .inv-because {
    margin: var(--space-3) 0 0;
    padding: var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-secondary);
    background: var(--surface-2);
    border-radius: var(--radius-sm);
  }

  /* --- Short-answer model answer ------------------------------------------ */
  .inv-short-reveal {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    align-items: flex-start;
    margin-top: var(--space-3);
  }
  .inv-short-hint {
    font-size: var(--text-xs);
    color: var(--text-tertiary, var(--text-secondary));
  }
  .inv-because.is-model {
    align-self: stretch;
    margin-top: 0;
    border-left: 3px solid var(--accent);
  }

  /* --- Written and numeric answers ---------------------------------------- */
  .inv-answer {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    font-family: inherit;
    font-size: var(--text-sm);
    line-height: 1.5;
    resize: vertical;
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }
  .inv-numeric {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }
  .inv-answer-num {
    flex: 1;
    min-width: 0;
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }
  .inv-unit {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .inv-answer:focus,
  .inv-answer-num:focus,
  .inv-field input:focus,
  #investigationName:focus {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }

  .inv-feedback {
    margin: var(--space-2) 0 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.5;
    border-radius: var(--radius-sm);
  }
  .inv-feedback.is-right {
    color: var(--text-primary);
    background: var(--accent-soft);
    border-left: 2px solid var(--accent);
  }
  .inv-feedback.is-wrong {
    color: var(--text-secondary);
    background: color-mix(in srgb, var(--danger) 10%, transparent);
    border-left: 2px solid var(--danger);
  }

  /* --- Measurement fields ------------------------------------------------- */
  .inv-fields {
    display: grid;
    gap: var(--space-3);
    margin-top: var(--space-2);
  }
  .inv-field {
    display: grid;
    gap: 4px;
  }
  .inv-field-label {
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-secondary);
  }
  .inv-field-unit {
    font-family: var(--font-mono);
    color: var(--text-muted);
  }
  .inv-field input {
    width: 100%;
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }

  /* --- Exploration checklist ---------------------------------------------- */
  .inv-checklist {
    display: grid;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
    padding: 0;
    list-style: none;
  }
  .inv-checklist label {
    display: flex;
    align-items: flex-start;
    gap: var(--space-2);
    font-size: var(--text-sm);
    line-height: 1.45;
    color: var(--text-secondary);
    cursor: pointer;
  }
  .inv-checklist input {
    flex: none;
    margin-top: 2px;
    width: 15px;
    height: 15px;
    accent-color: var(--accent);
    cursor: pointer;
  }

  /* --- Live readout ------------------------------------------------------- */
  .inv-probe-wrap {
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border-subtle);
    background: var(--surface-2);
  }
  .inv-probe-wrap[hidden] {
    display: none;
  }
  .inv-probe-title {
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-probe {
    display: grid;
    gap: 3px;
    /* Selectable on purpose: copying a measured number out of here into the
       answer field above is the core loop of a measure step. */
    user-select: text;
  }
  .inv-probe-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .inv-probe-row > :last-child {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
  }
  .inv-probe-row.is-emphasis > :last-child {
    color: var(--accent);
    font-weight: var(--weight-semibold);
  }

  /* --- Objects in this activity -------------------------------------------
     The keyboard's way to do what a click on the canvas does. Reuses the
     readout's own surface and title so it reads as part of the instrument
     rather than as a second panel, and wraps rather than scrolls: a step
     binds two or three objects, never twenty. */
  /* The wrapper is .inv-probe-wrap, shared with the readout above: same
     padding, same rule above it, same surface. Only the list itself is new. */
  .inv-objects {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }
  /* A selectable object, and the stage's own controls: same chip, and the
     class is different so that ".inv-object" means one thing. */
  .inv-object,
  .inv-stage-btn {
    display: inline-flex;
    align-items: baseline;
    gap: var(--space-2);
    padding: 4px var(--space-3);
    min-height: 32px;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    background: var(--surface-1);
    color: var(--text-secondary);
    font-size: var(--text-xs);
    cursor: pointer;
  }
  .inv-object:hover,
  .inv-stage-btn:hover {
    border-color: var(--accent);
    color: var(--text-primary);
  }
  .inv-object[aria-pressed='true'],
  .inv-stage-btn[aria-pressed='true'] {
    border-color: var(--accent);
    color: var(--text-primary);
    box-shadow: inset 0 0 0 1px var(--accent);
  }
  .inv-object-role {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .inv-object[aria-pressed='true'] .inv-object-role {
    color: var(--accent);
  }

  /* --- Panel footer ------------------------------------------------------- */
  .inv-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4);
    border-top: 1px solid var(--border-subtle);
  }
  .inv-footer-nav {
    display: flex;
    gap: var(--space-2);
  }
  .inv-footer .ui-button.subtle {
    color: var(--text-muted);
    background: transparent;
    border-color: transparent;
  }
  .inv-footer .ui-button.subtle:hover {
    color: var(--text-secondary);
    background: var(--button-hover-bg);
  }

  /* --- Data export dialog ------------------------------------------------- */
  /* Shares the finish dialog's surface: same ground, same header, same close
     button, so the two read as one family rather than two apps. */
  #dataExport {
    position: fixed;
    inset: 0;
    /* Above the object inspector, which sits at 2000 and so covers every other
       modal in the app. This dialog is most useful with an object selected,
       because that is when "export the selected object only" means something,
       and that is exactly when the inspector is open in front of it. Raising
       one dialog is a smaller change than renumbering the app's z-index scale;
       the inspector stays visible, dimmed, behind the backdrop. */
    z-index: 2100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--bg-color) 88%, transparent);
    backdrop-filter: blur(8px);
  }
  #dataExport.hidden {
    display: none;
  }
  #dataExportContent {
    width: min(540px, 100%);
    max-height: 90vh;
    overflow-y: auto;
    padding: var(--space-6);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }
  .export-intro {
    margin: 0 0 var(--space-4);
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text-secondary);
  }
  .export-scope {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2) var(--space-4);
    margin: 0 0 var(--space-4);
    padding: var(--space-3) var(--space-3) var(--space-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
  }
  .export-scope[hidden] {
    display: none;
  }
  .export-scope legend {
    padding: 0 var(--space-2);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .export-scope label {
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: var(--text-sm);
    color: var(--text-secondary);
    cursor: pointer;
  }
  .export-scope input:disabled + span {
    color: var(--text-muted);
    cursor: default;
  }
  .export-files {
    display: grid;
    gap: var(--space-2);
  }
  .export-file {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
  }
  .export-file-text {
    min-width: 0;
  }
  .export-file-name {
    display: block;
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }
  .export-file-detail {
    display: block;
    margin-top: 2px;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-muted);
  }
  .export-file .ui-button {
    flex: none;
  }
  .export-file.is-empty .export-file-name {
    color: var(--text-muted);
  }
  .export-fineprint {
    margin: var(--space-4) 0 0;
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-muted);
  }
  .export-fineprint a {
    color: var(--accent);
  }

  /* On a phone the file rows stop being a row: the button goes underneath its
     description rather than squeezing it to two words a line. */
  @media (max-width: 560px) {
    .export-file {
      flex-direction: column;
      align-items: stretch;
    }
    .export-file .ui-button {
      width: 100%;
    }
  }

  /* --- Finish dialog ------------------------------------------------------ */
  #investigationFinish {
    position: fixed;
    inset: 0;
    z-index: var(--z-modal);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--space-4);
    background: color-mix(in srgb, var(--bg-color) 88%, transparent);
    backdrop-filter: blur(8px);
  }
  #investigationFinish.hidden {
    display: none;
  }
  #investigationFinishContent {
    width: min(520px, 100%);
    max-height: 90vh;
    overflow-y: auto;
    padding: var(--space-6);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }
  .inv-finish-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: var(--space-3);
  }
  .inv-finish-header h3 {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
  }
  .inv-finish-header .ui-button {
    min-width: 0;
    padding: 2px 10px;
    font-size: var(--text-lg);
  }
  .inv-finish-summary p {
    margin: 0 0 var(--space-3);
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text-secondary);
  }
  .inv-finish-note {
    color: var(--text-muted) !important;
    font-size: var(--text-xs) !important;
  }
  .inv-name-field {
    display: grid;
    gap: 6px;
    margin: var(--space-4) 0 var(--space-2);
  }
  .inv-name-field span {
    font-size: var(--text-sm);
    color: var(--text-secondary);
  }
  #investigationName {
    padding: var(--space-2) var(--space-3);
    font-family: inherit;
    font-size: var(--text-md);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }
  .inv-name-error {
    margin: 0 0 var(--space-2);
    font-size: var(--text-xs);
    color: var(--danger);
  }
  .inv-name-error[hidden] {
    display: none;
  }
  .inv-finish-actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
    margin-top: var(--space-4);
  }
  .inv-finish-fineprint {
    margin: var(--space-4) 0 0;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-muted);
  }

  /* The scenario introduction card is 720px wide and docks exactly where the
     lesson's instrument panel goes. A lesson loads a scenario on every step
     that asks for one, so during an investigation the card is both redundant
     (the step text introduces the scenario) and directly in the way. */
  body.investigation-open #scenarioInfoBox {
    display: none !important;
  }

  /* --- Narrow screens ----------------------------------------------------- */
  @media (max-width: 900px) {
    /* Only once the panel is a bottom sheet does the floating help button land
       on top of the Next button. On a wide screen the two do not meet, so the
       tour stays reachable there. */
    body.investigation-open #tutorialBtn {
      display: none;
    }

    .investigation-panel {
      /* A sheet across the bottom rather than a side panel: on a phone the
         canvas is already the smaller dimension, and a full-height panel down
         one side would leave the simulation a strip. Still above the transport
         bar, which occupies the very bottom - measured rather than assumed,
         for the reason given on the base rule above. */
      top: auto;
      left: var(--space-2);
      right: var(--space-2);
      bottom: var(--transport-reserve, 76px);
      width: auto;
      max-height: min(52vh, 420px);
    }
    .inv-footer,
    .inv-progress,
    .inv-header {
      padding-left: var(--space-3);
      padding-right: var(--space-3);
    }
    .inv-body {
      padding: var(--space-3);
    }
  }
  @media (max-width: 900px) {
    #investigationBrowserContent {
      width: calc(100vw - var(--space-4));
      max-height: 92vh;
      padding: var(--space-5) var(--space-4) 0;
    }
  }
  /* On a phone the pinned header - title, four lines of intro, two links and
     the count - took nearly half the panel and stayed there. The gallery pins
     its header because the filters have to stay reachable; there are no filters
     here, so the whole panel scrolls and the intro gets out of the way. */
  @media (max-width: 560px) {
    #investigationBrowserContent {
      display: block;
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .inv-browser-scroll {
      overflow: visible;
    }
  }
  @media (max-width: 480px) {
    /* The bottom stays zero: the scroller below the header carries the bottom
       padding, so the last card can scroll clear of the panel edge. */
    #investigationBrowserContent {
      padding: var(--space-4) var(--space-4) 0;
    }
    .inv-browser-header h3 {
      font-size: var(--text-lg);
    }
    .inv-browser-header .ui-button {
      display: none;
    }
    .inv-finish-actions {
      flex-direction: column-reverse;
    }
    .inv-finish-actions .ui-button {
      width: 100%;
    }
  }
}

/* --- Investigations: derived fields, checks and plots --------------------- */
@layer components {
  .inv-reset-scenario {
    flex: none;
    width: 26px;
    height: 26px;
    min-height: 0;
    padding: 0;
    display: grid;
    place-items: center;
    font-size: var(--text-md);
    line-height: 1;
    color: var(--text-secondary);
    background: transparent;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .inv-reset-scenario:hover {
    color: var(--text-primary);
    background: var(--button-hover-bg);
    border-color: var(--border-strong);
  }

  .inv-field.is-derived input {
    color: var(--accent);
    background: var(--accent-soft);
    border-style: dashed;
    cursor: default;
  }
  .inv-field-auto {
    font-family: var(--font-mono);
    font-size: 0.6rem;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--accent);
  }

  /* Sanity check on entered measurements. Warnings read as guidance rather
     than failure: the numbers are a student's own observations, and the tool
     is pointing out a disagreement, not marking them wrong. */
  .inv-check {
    margin-top: var(--space-3);
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-xs);
    line-height: 1.5;
    border-radius: var(--radius-sm);
    border-left: 2px solid var(--border-strong);
    background: var(--surface-2);
    color: var(--text-secondary);
  }
  .inv-check[hidden] {
    display: none;
  }
  .inv-check.is-ok {
    border-left-color: var(--accent);
    background: var(--accent-soft);
    color: var(--text-primary);
  }
  .inv-check.is-warn {
    border-left-color: var(--warning);
    background: color-mix(in srgb, var(--warning) 12%, transparent);
  }
  .inv-check.is-error {
    border-left-color: var(--danger);
    background: color-mix(in srgb, var(--danger) 12%, transparent);
  }
  .inv-plot-title {
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-secondary);
  }
  .inv-plot-toggle {
    flex: none;
    padding: 3px 9px;
    font-family: inherit;
    font-size: var(--text-xs);
    line-height: 1.3;
    cursor: pointer;
    color: var(--text-secondary);
    background: var(--button-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
  }
  .inv-plot-toggle:hover {
    color: var(--text-primary);
    border-color: var(--border-strong);
  }
  .inv-plot-toggle.is-on {
    color: var(--accent);
    background: var(--accent-soft);
    border-color: var(--accent);
  }
  .inv-plot-note {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-muted);
  }
}

/* --- Investigations: the measurement plot, in its own panel ---------------- */
@layer components {
  /* Docked beside the lesson rather than inside its scroll: the whole point is
     watching points land as they are typed, and at the bottom of a scrolling
     column it went unnoticed until the last value went in. */
  .inv-plot-panel {
    position: fixed;
    top: var(--space-3);
    left: calc(min(396px, 40vw) + var(--space-3) * 2);
    z-index: var(--z-panel);
    width: min(460px, 42vw);
    /* Clears the transport bar, same as the lesson panel. Anything taller than
       the window scrolls inside the panel rather than off the bottom of it,
       which is what the widgets with three sliders and five presets need. */
    max-height: calc(100vh - var(--space-3) - 88px);
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-3) var(--space-4) var(--space-4);
    background: var(--panel-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
    color: var(--text-primary);
  }
  .inv-plot-panel[hidden] {
    display: none;
  }
  .inv-plot-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3);
    margin-bottom: var(--space-2);
  }
  .inv-plot-panel canvas {
    display: block;
    width: 100%;
  }
  .inv-plot-panel .inv-plot-note {
    margin-top: var(--space-2);
  }

  .inv-import-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin: var(--space-3) 0 var(--space-1);
  }
  .inv-import-btn {
    flex: none;
    padding: 5px 12px;
    font-family: inherit;
    font-size: var(--text-xs);
    cursor: pointer;
    color: var(--accent);
    background: var(--accent-soft);
    border: 1px solid var(--accent);
    border-radius: var(--radius-full);
  }
  .inv-import-btn:disabled {
    color: var(--text-muted);
    background: var(--button-bg);
    border-color: var(--border);
    cursor: default;
  }
  .inv-import-hint {
    font-size: var(--text-xs);
    line-height: 1.4;
    color: var(--text-muted);
  }

  /* Not enough width for a fixed 460px column beside the lesson, so the panel
     takes whatever is left of the window instead.

     It used to go `position: static` here, on the reasoning that it would fall
     back into the flow under the fields. It is a sibling of the lesson panel
     rather than a child of it, so what actually happened was that it landed at
     the bottom of a body with `overflow: hidden` and could not be reached at
     all. */
  @media (max-width: 1180px) {
    .inv-plot-panel {
      left: calc(min(396px, 40vw) + var(--space-3) * 2);
      right: var(--space-3);
      width: auto;
    }
  }

  /* Below this the lesson is a sheet across the bottom, so the instrument takes
     the space above it rather than a column beside it. The sheet gives some
     height back on the steps that carry an instrument, which is enough to put
     the picture and its readout above the fold; the controls under them are a
     flick away, which is ordinary on a phone and better than shrinking a
     diagram until it cannot be read. */
  @media (max-width: 900px) {
    .inv-plot-panel {
      top: var(--space-2);
      left: var(--space-2);
      right: var(--space-2);
      max-height: calc(
        100vh - var(--transport-reserve, 76px) - min(38vh, 300px) -
          var(--space-2) * 3
      );
    }
    body.investigation-aux .investigation-panel {
      max-height: min(38vh, 300px);
    }
  }
}

/* --- Investigations: the ellipse explorer --------------------------------- */
@layer components {
  .inv-ecc-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    margin-top: var(--space-2);
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .inv-ecc-row input[type='range'] {
    flex: 1;
    min-width: 0;
    accent-color: var(--accent);
  }
  .inv-ecc-row output {
    flex: none;
    min-width: 4.2em;
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--accent);
  }
  .inv-presets {
    display: flex;
    flex-wrap: wrap;
    gap: 5px;
    margin-top: var(--space-3);
  }
  .inv-preset {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 1px;
    padding: 4px 9px;
    font-family: inherit;
    font-size: var(--text-xs);
    line-height: 1.2;
    cursor: pointer;
    color: var(--text-secondary);
    background: var(--button-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }
  .inv-preset:hover {
    color: var(--text-primary);
    border-color: var(--accent);
  }
  .inv-preset span {
    font-family: var(--font-mono);
    font-size: 0.62rem;
    color: var(--text-muted);
  }
  .inv-preset-note {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
  }
  .inv-preset-note:empty {
    display: none;
  }

  /* Real subscripts, without the leading that browsers give them by default
     pushing lines apart in a column of dense prose. */
  .inv-body sub,
  .inv-body sup,
  .inv-tool-row sub,
  .inv-tool-row sup {
    position: relative;
    font-size: 0.72em;
    line-height: 0;
    vertical-align: baseline;
  }
  .inv-body sub,
  .inv-tool-row sub {
    bottom: -0.28em;
  }
  .inv-body sup,
  .inv-tool-row sup {
    top: -0.45em;
  }

  /* --- Investigations: the plot as a table --------------------------------- */
  /* The accessible equivalent of the scatter above it. Visible rather than
     hidden from sight: a reader who cannot separate two plotted colors, or who
     is reading a projected screen from the back of a room, needs the numbers as
     much as a screen-reader user does, and a second copy nobody can see is a
     copy nobody maintains. */
  .inv-plot-table-wrap {
    margin-top: var(--space-2);
    overflow-x: auto;
  }
  .inv-plot-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .inv-plot-table caption {
    text-align: left;
    padding-bottom: var(--space-1);
    color: var(--text-muted, var(--text-secondary));
  }
  .inv-plot-table th,
  .inv-plot-table td {
    padding: 2px var(--space-2) 2px 0;
    text-align: right;
    white-space: nowrap;
  }
  .inv-plot-table thead th {
    border-bottom: 1px solid var(--border-subtle);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }
  .inv-plot-table tbody th {
    text-align: left;
    font-weight: var(--weight-regular, 400);
  }
  .inv-plot-table td {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
  }

  /* --- Investigations: the interactive instrument panel -------------------- */
  .inv-tool-readout {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-subtle);
    /* A <dl> now, so the browser's own margins have to go. */
    margin-bottom: 0;
  }
  .inv-tool-row dt,
  .inv-tool-row dd {
    margin: 0;
  }
  .inv-tool-row {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }
  .inv-tool-row > :last-child {
    font-family: var(--font-mono);
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
  }
  .inv-tool-row.is-emphasis > :last-child {
    color: var(--accent);
    font-weight: var(--weight-semibold);
  }
  .inv-tool-controls {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: var(--space-3);
    padding-top: var(--space-3);
    border-top: 1px solid var(--border-subtle);
  }
  .inv-tool-controls .inv-ecc-row > span:first-child {
    flex: none;
    min-width: 10.5em;
  }
  .inv-tool-controls[hidden],
  .inv-tool-actions[hidden],
  .inv-tool-readout[hidden] {
    display: none;
  }
  .inv-tool-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-3);
  }
  .inv-tool-action {
    flex: 1;
    padding: var(--space-2) var(--space-3);
    font-family: inherit;
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--text-primary);
    cursor: pointer;
    background: var(--button-bg);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }
  .inv-tool-action:hover {
    border-color: var(--accent);
    color: var(--accent);
  }
  .inv-tool-action:active {
    background: var(--accent-soft);
  }
}

/* --- Investigations: the Light Curve tool, docked clear of the lesson ------ */
@layer overrides {
  /* Its own home is the bottom left, which is exactly where the lesson panel
     lives: opening it during a lesson used to bury the step you were reading.
     Here it takes the strip between the lesson panel and the control rail,
     which is the widest free space on the screen and the right shape for a
     light curve. The rail is content-sized and measures 208px; 240 clears it. */
  body.investigation-open .light-curve-container {
    left: calc(min(396px, 40vw) + var(--space-3) * 2);
    right: 240px;
    bottom: 88px;
    width: auto;
    height: 300px;
  }
  /* Steps that show an instrument panel as well. No lesson step asks for both
     at once, but a student can open the light curve from Tools at any point, so
     the two have to share the column rather than one of them being cut off.
     The light curve gives up most of its height, since on those steps the
     instrument is the thing being read. */
  body.investigation-lightcurve.investigation-aux .light-curve-container {
    /* min-height has to come down with it: the tool's own stylesheet floors it
       at 220px, which would silently ignore the height set here. */
    min-height: 0;
    height: 168px;
  }
  body.investigation-lightcurve .inv-plot-panel {
    max-height: calc(100vh - var(--space-3) - 88px - 184px);
  }

  /* --- A step that carries both an instrument and a plot ------------------- */
  /* Both dock to the same fixed position, so a measure step whose values feed a
     graph could previously show only one of them: the student had to page back
     to the previous screen to read the instrument they were recording from.
     When both are present they share the column, instrument above. */
  /* The instrument takes the larger share: it carries a picture, a readout of
     several rows and its own controls, where the plot below it is a picture and
     a caption. At 0.56 the slider a measurement step tells students to move
     ended up below the fold. */
  body.investigation-split #investigationTool {
    max-height: calc((100vh - var(--space-3) - 88px) * 0.62);
  }
  body.investigation-split #investigationPlot {
    top: calc((100vh - var(--space-3) - 88px) * 0.62 + var(--space-3) * 2);
    max-height: calc((100vh - var(--space-3) - 88px) * 0.38 - var(--space-3));
  }
  @media (max-width: 900px) {
    /* Two panels above a bottom sheet, sharing the band between them in the
       same 62/38 proportion as the desktop split. Each scrolls inside itself.

       This used to say `position: static` on the plot, on the reasoning that
       it would fall back into the flow under the lesson. It does not: it is a
       sibling of the lesson panel rather than a child of it, so it landed at
       the bottom of a body with `overflow: hidden` and could not be reached at
       all - on a 1024-tall tablet its top edge was twelve pixels below the
       window, and a student on a step with both an instrument and a plot could
       not see the graph of their own readings, or the table of them beside it.
       The comment on `.inv-plot-panel` above records the same mistake being
       made and fixed for the unsplit case; this rule kept it. */
    body.investigation-split #investigationTool {
      max-height: calc(
        (
            100vh - var(--transport-reserve, 76px) - min(38vh, 300px) -
              var(--space-2) * 3
          ) *
          0.62
      );
    }
    body.investigation-split #investigationPlot {
      top: calc(
        (
            100vh - var(--transport-reserve, 76px) - min(38vh, 300px) -
              var(--space-2) * 3
          ) *
          0.62 + var(--space-2) * 2
      );
      max-height: calc(
        (
            100vh - var(--transport-reserve, 76px) - min(38vh, 300px) -
              var(--space-2) * 3
          ) *
          0.38
      );
    }
  }
  @media (max-width: 1100px) {
    body.investigation-open .light-curve-container {
      left: calc(min(396px, 40vw) + var(--space-2) * 2);
      right: var(--space-2);
      bottom: var(--transport-reserve, 76px);
      height: 240px;
    }
  }
  @media (max-width: 900px) {
    /* The lesson is a bottom sheet at this width, so the light curve goes to
       the top of the viewport instead of stacking above the sheet.
       
       It used to sit at `transport-reserve + min(52vh, 420px)` from the
       bottom, which is an estimate of how tall the sheet is - and the sheet is
       not that tall. A step that carries a long body and an object list grows
       past it, and the light curve then covered the list: on a 390-wide phone
       the element under the first chip's center was the light curve's own
       observer controls, so a step telling the reader to select the planet
       could not be done at all. Anchoring to the top needs no estimate of
       anything, because nothing else is up there while a lesson is open. */
    body.investigation-open .light-curve-container {
      left: var(--space-2);
      right: var(--space-2);
      top: var(--space-2);
      bottom: auto;
      height: min(30vh, 210px);
    }
  }
}

/* --- Investigations: figures ---------------------------------------------- */
@layer components {
  .inv-figure {
    margin: var(--space-4) 0;
  }
  .inv-figure img {
    display: block;
    width: 100%;
    height: auto;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    /* The photograph is its own light source, so it keeps the same ground in
       every theme rather than sitting on a panel that changes under it. */
    background: #000;
  }
  .inv-figure figcaption {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin-top: var(--space-2);
  }
  .inv-figure-caption {
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
  }
  .inv-figure-credit {
    font-size: var(--text-2xs);
    line-height: 1.4;
    color: var(--text-muted);
  }
  .inv-figure-credit a {
    color: inherit;
    text-decoration: underline;
    text-underline-offset: 2px;
  }
  .inv-figure-credit a:hover {
    color: var(--text-secondary);
  }
}

/* --- Investigations: pull quotes ------------------------------------------ */
@layer components {
  .inv-quote {
    margin: var(--space-4) 0;
    padding: var(--space-3) var(--space-4);
    border-left: 3px solid var(--accent);
    background: var(--accent-soft);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  }
  .inv-quote blockquote {
    margin: 0;
    font-size: var(--text-sm);
    font-style: italic;
    line-height: 1.6;
    color: var(--text-primary);
  }
  .inv-quote blockquote::before {
    content: '\201C';
  }
  .inv-quote blockquote::after {
    content: '\201D';
  }
  .inv-quote figcaption {
    margin-top: var(--space-2);
    font-size: var(--text-xs);
    color: var(--accent);
  }
  .inv-quote figcaption::before {
    content: '\2014\00a0';
  }
}

@layer components {
  .inv-ellipse-key {
    display: grid;
    gap: 3px;
    margin-top: var(--space-2);
    font-size: var(--text-xs);
    line-height: 1.4;
  }
  .inv-ellipse-key span::before {
    content: '\25CF\00a0\00a0';
  }
  .inv-ellipse-key .k-a {
    color: #5ec8f5;
  }
  .inv-ellipse-key .k-b {
    color: #8de08a;
  }
  .inv-ellipse-key .k-c {
    color: #f2a65a;
  }
  .inv-ellipse-key .k-star {
    color: #ffd34d;
  }
  .inv-ellipse-key .k-empty {
    color: #9aa2b8;
  }
}

@layer components {
  .inv-wedge-control {
    margin-top: var(--space-3);
    padding: var(--space-3);
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
  }
  .inv-wedge-readout {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
  }
  .inv-wedge-readout strong {
    color: var(--accent);
  }
}

/* --- Investigations browser: the quiet line to the model and instructor pages */
@layer components {
  .inv-browser-links {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
  }
  .inv-browser-links a {
    color: var(--text-secondary);
    text-underline-offset: 2px;
  }
  .inv-browser-links a:hover,
  .inv-browser-links a:focus-visible {
    color: var(--accent);
  }
}

/* =============================================================================
   The front door
   -----------------------------------------------------------------------------
   A full-screen orientation layer shown once, on a first visit, between the
   splash and the sandbox. Markup shell in index.html, content and behavior in
   js/welcome.js.

   Three things shape everything below:

   1. It floats over a running simulation. The scrim is built from --surface-0
      so it is correct in all four themes, including Daylight, where the UI is
      light but the canvas deliberately stays dark. Gradients rather than
      backdrop-filter: a blur over a canvas that repaints every frame forces a
      full recomposite every frame, which is a real cost on a phone for an
      effect a gradient gets most of the way to.

   2. The wordmark continues the splash. Same face, weight, tracking and glow,
      so the splash appears to hand off to this rather than being replaced.

   3. Every grid is auto-fit with a min track, so cards reflow rather than
      overlap and the page never scrolls sideways.
   ========================================================================== */

@layer components {
  .welcome-screen {
    position: fixed;
    inset: 0;
    z-index: var(--z-welcome);

    overflow-y: auto;
    overflow-x: hidden;
    overscroll-behavior: contain;
    -webkit-overflow-scrolling: touch;

    /* Darkened, not blurred: the system underneath stays faintly visible at
       the top of the hero and is progressively covered as the reading starts. */
    background:
      radial-gradient(
        120% 80% at 50% 0%,
        var(--accent-soft) 0%,
        transparent 62%
      ),
      linear-gradient(
        to bottom,
        color-mix(in srgb, var(--surface-0) 86%, transparent) 0%,
        color-mix(in srgb, var(--surface-0) 95%, transparent) 34%,
        color-mix(in srgb, var(--surface-0) 98%, transparent) 100%
      );

    opacity: 0;
    transition: opacity var(--duration-slow) var(--ease-out);
  }

  .welcome-screen.is-shown {
    opacity: 1;
  }

  .welcome-dialog {
    position: relative;
    width: min(60rem, 100% - var(--space-8));
    margin: 0 auto;
    padding: clamp(var(--space-8), 7vh, var(--space-10)) 0
      calc(var(--space-10) + env(safe-area-inset-bottom, 0px));
  }

  .welcome-dialog:focus {
    outline: none;
  }

  .welcome-close {
    position: fixed;
    top: var(--space-4);
    right: var(--space-4);
    z-index: 1;

    display: grid;
    place-items: center;
    width: 2.25rem;
    height: 2.25rem;

    font-size: var(--text-base);
    color: var(--text-secondary);

    /* Fixed, so it floats over whatever is scrolling past underneath. It has
       to be opaque enough that card text never shows through the glyph. */
    background: var(--surface-1);
    box-shadow: var(--shadow);
    border: 1px solid var(--border);
    border-radius: var(--radius-full);
    cursor: pointer;
    transition:
      color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .welcome-close:hover,
  .welcome-close:focus-visible {
    color: var(--text-primary);
    border-color: var(--border-strong);
  }

  /* --- Hero ---------------------------------------------------------------- */

  .wel-hero {
    padding: clamp(var(--space-6), 6vh, var(--space-10)) 0
      clamp(var(--space-8), 7vh, var(--space-10));
    text-align: center;
  }

  .wel-eyebrow {
    margin: 0 0 var(--space-4);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--accent);
  }

  /* Deliberately identical to #splash h1, so the wordmark does not appear to
     move or change when one layer replaces the other. */
  .wel-wordmark {
    margin: 0;
    font-family: 'Poppins', var(--font-sans);
    font-weight: 600;
    font-size: clamp(2.75rem, 9vw, 5.5rem);
    line-height: 1;
    letter-spacing: 0.12em;
    color: var(--text-primary);
    text-shadow: 0 0 34px var(--accent-soft);
  }

  .wel-lede {
    max-width: 34em;
    margin: var(--space-5) auto 0;
    font-size: clamp(var(--text-base), 1.6vw, var(--text-lg));
    line-height: 1.65;
    color: var(--text-secondary);
    text-wrap: pretty;
  }

  .wel-hero-actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--space-3);
    margin-top: clamp(var(--space-6), 4vh, var(--space-8));
  }

  .wel-primary.ui-button {
    padding: var(--space-3) var(--space-6);
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    color: var(--accent-contrast);
    background: var(--accent);
    border-color: var(--accent-strong);
  }

  .wel-primary.ui-button:hover,
  .wel-primary.ui-button:focus-visible {
    background: var(--accent-strong);
  }

  .wel-secondary.ui-button {
    padding: var(--space-3) var(--space-5);
    font-size: var(--text-base);
  }

  /* Tertiary actions read as links: the hero must have one obvious next step,
     and three equally weighted buttons do not give it one. */
  .wel-quiet {
    padding: var(--space-2) var(--space-1);
    font: inherit;
    font-size: var(--text-sm);
    color: var(--text-secondary);
    background: none;
    border: 0;
    border-bottom: 1px solid transparent;
    cursor: pointer;
    transition:
      color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .wel-quiet:hover,
  .wel-quiet:focus-visible {
    color: var(--accent);
    border-bottom-color: currentColor;
  }

  /* --- Sections ------------------------------------------------------------ */

  .wel-section {
    padding: clamp(var(--space-8), 6vh, var(--space-10)) 0;
    border-top: 1px solid var(--border-subtle);
  }

  .wel-h2 {
    margin: 0 0 var(--space-5);
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    letter-spacing: -0.01em;
    color: var(--text-primary);
    text-wrap: balance;
  }

  .wel-section-head {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
  }

  .wel-section-head .wel-h2 {
    margin-bottom: var(--space-5);
  }

  .wel-say {
    max-width: 62ch;
    margin: 0 0 var(--space-6);
    font-size: var(--text-base);
    line-height: 1.7;
    color: var(--text-secondary);
    text-wrap: pretty;
  }

  /* --- Build / Explore / Learn --------------------------------------------- */

  .wel-triad {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-5);
  }

  .wel-point h3 {
    margin: 0 0 var(--space-2);
    padding-bottom: var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--accent);
    border-bottom: 1px solid var(--accent-line);
  }

  .wel-point p {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.65;
    color: var(--text-secondary);
  }

  /* --- Three ways in ------------------------------------------------------- */

  .wel-doors {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
    gap: var(--space-4);
  }

  .wel-door {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-5);
    background: color-mix(in srgb, var(--surface-1) 72%, transparent);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
  }

  .wel-door-eyebrow {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .wel-door-title {
    margin: 0;
    font-size: var(--text-lg);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .wel-door-text {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.6;
    color: var(--text-secondary);
  }

  /* Pushed to the bottom so the three buttons line up however unevenly the
     descriptions wrap. */
  .wel-door-cta.ui-button {
    margin-top: auto;
    align-self: flex-start;
  }

  /* --- Featured scenarios --------------------------------------------------- */

  .wel-scenarios {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: var(--space-3);
  }

  .wel-scenario {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-4);
    text-align: left;

    background: color-mix(in srgb, var(--surface-1) 68%, transparent);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition:
      transform var(--duration) var(--ease-out),
      border-color var(--duration) var(--ease),
      background-color var(--duration) var(--ease);
  }

  .wel-scenario:hover,
  .wel-scenario:focus-visible {
    transform: translateY(-2px);
    background: color-mix(in srgb, var(--surface-2) 85%, transparent);
    border-color: var(--accent-line);
  }

  /* The featured cards show the same capture the gallery does, through the
     shared .sc-card-shot markup, so a scenario looks the same wherever it is
     offered and there is only one thumbnail fallback to maintain. This used to
     be six hand-drawn SVG diagrams, which was a second visual vocabulary for
     the same six systems. */
  .wel-scenario .sc-card-shot {
    border: 0;
    border-radius: var(--radius-sm);
    overflow: hidden;
  }

  .wel-scenario-text {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
  }

  .wel-scenario-title {
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .wel-scenario-summary {
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-secondary);
    /* A catalog summary can run long. Clamping keeps six cards the same height
       without a second, shorter copy of every description. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
  }

  /* --- Investigations ------------------------------------------------------- */

  .wel-lessons {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-3);
    margin: 0 0 var(--space-6);
    padding: 0;
    list-style: none;
  }

  .wel-lesson {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    background: color-mix(in srgb, var(--surface-1) 60%, transparent);
    border-left: 2px solid var(--accent-line);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  }

  .wel-lesson-title {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .wel-lesson-sub {
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
  }

  .wel-lesson-meta {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  /* --- Audiences ------------------------------------------------------------ */

  .wel-audiences {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
    gap: var(--space-5);
  }

  .wel-audience h3 {
    margin: 0 0 var(--space-2);
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    color: var(--text-primary);
  }

  .wel-audience p {
    margin: 0;
    font-size: var(--text-sm);
    line-height: 1.65;
    color: var(--text-secondary);
  }

  /* --- Teaching links ------------------------------------------------------- */

  .wel-links {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
    gap: var(--space-3);
  }

  .wel-link {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-4);
    text-decoration: none;

    background: color-mix(in srgb, var(--surface-1) 68%, transparent);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    transition:
      border-color var(--duration) var(--ease),
      background-color var(--duration) var(--ease);
  }

  .wel-link:hover,
  .wel-link:focus-visible {
    background: color-mix(in srgb, var(--surface-2) 85%, transparent);
    border-color: var(--accent-line);
  }

  .wel-link-label {
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    color: var(--accent);
  }

  .wel-link-note {
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--text-secondary);
  }

  /* --- Foot ----------------------------------------------------------------- */

  .wel-foot {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding-top: clamp(var(--space-8), 6vh, var(--space-10));
    text-align: center;
    border-top: 1px solid var(--border-subtle);
  }

  .wel-foot-note {
    max-width: 40ch;
    margin: 0;
    font-size: var(--text-xs);
    line-height: 1.6;
    color: var(--text-muted);
  }

  /* --- Narrow screens -------------------------------------------------------- */

  @media (max-width: 700px) {
    .welcome-dialog {
      width: calc(100% - var(--space-4) * 2);
      padding-top: var(--space-8);
    }

    /* The whole page has to stay a reasonable scroll on a phone, so every
       section is tightened rather than any section being dropped: a visitor on
       a phone should meet the same Gravitas as a visitor on a laptop. */
    .wel-section {
      padding: var(--space-6) 0;
    }

    .wel-hero {
      padding: var(--space-4) 0 var(--space-6);
    }

    .wel-lede {
      margin-top: var(--space-4);
      font-size: var(--text-base);
    }

    .wel-say {
      margin-bottom: var(--space-5);
    }

    .wel-triad,
    .wel-audiences {
      gap: var(--space-4);
    }

    .wel-door {
      padding: var(--space-4);
    }

    /* The eyebrow is a desktop nicety: on a phone the three doors are stacked
       and the headings alone already read as a list. */
    .wel-door-eyebrow {
      display: none;
    }

    /* Title and length are enough to show what a lesson looks like. The
       subtitle is three more lines each, for three lessons, and the button
       underneath goes to the real browser where all of it is shown. */
    .wel-lesson-sub {
      display: none;
    }

    /* The heading and its "browse all" link stop sitting on one line here, and
       the heading's own bottom margin then opens a gap that makes the link look
       like a stray subtitle. */
    .wel-section-head .wel-h2 {
      margin-bottom: var(--space-2);
    }

    .wel-section-head .wel-quiet {
      padding-left: 0;
    }

    /* One card per row rather than six desktop-sized cards stacked: the same
       content, a third of the scrolling. */
    .wel-scenario {
      flex-direction: row;
      align-items: center;
      gap: var(--space-3);
      padding: var(--space-3);
    }

    .wel-scenario-summary {
      -webkit-line-clamp: 2;
      line-clamp: 2;
    }

    .wel-hero-actions .ui-button {
      flex: 1 1 100%;
      justify-content: center;
    }

    .welcome-close {
      top: var(--space-2);
      right: var(--space-2);
    }
  }

  /* The layer scrolls itself; the page behind it must not move underneath. */
  body.welcome-open {
    overflow: hidden;
  }

  @media (prefers-reduced-motion: reduce) {
    .welcome-screen {
      transition: none;
    }

    .wel-scenario:hover,
    .wel-scenario:focus-visible {
      transform: none;
    }
  }
}

/* =============================================================================
   Scenario gallery
   -----------------------------------------------------------------------------
   The scenario browser as a visual catalog: a thumbnail grid, concept chips and
   a search box. Markup shell in index.html, behavior in js/scenarioBrowser.js.

   The old browser is still styled in styles.css, in the `legacy` layer, with
   hardcoded #1a1a2e / #4facfe values. Because `components` is declared after
   `legacy`, the rules below win on layer order without !important, and the
   panel picks up the theme like everything else. The legacy block is left
   alone rather than deleted: it is not this change's job to unpick it, and
   nothing in it now paints.
   ========================================================================== */

@layer components {
  #scenarioListContent {
    /* A gallery needs width the old text column did not. */
    width: min(1180px, calc(100vw - var(--space-8)));
    min-width: 0;
    max-width: none;
    max-height: min(88vh, 960px);
    padding: var(--space-6) var(--space-6) 0;

    display: flex;
    flex-direction: column;
    /* The header, search, chips and count stay put; only the grid scrolls, so
       the filters are always reachable however far down the catalog you are. */
    overflow: hidden;

    color: var(--text-primary);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-lg);
  }

  /* The legacy tint sheet, which sat above the content on its own stacking
     context. It has no job now that the panel uses surface tokens. */
  #scenarioListContent::before {
    content: none;
  }

  /* --- Header --------------------------------------------------------------- */

  .sc-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--space-4);
  }

  .sc-head h3 {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-semibold);
    letter-spacing: -0.01em;
    color: var(--text-primary);
  }

  .sc-subtitle {
    max-width: 62ch;
    margin: var(--space-2) 0 0;
    font-size: var(--text-sm);
    line-height: 1.55;
    color: var(--text-secondary);
  }

  #scenarioListContent .scenario-search-wrap {
    margin: var(--space-5) 0 0;
  }

  /* --- Concept chips -------------------------------------------------------- */

  .sc-concepts {
    margin-top: var(--space-5);
  }

  .sc-concepts-label {
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  .sc-chips {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2);
  }

  .sc-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);

    font-family: inherit;
    font-size: var(--text-xs);
    color: var(--text-secondary);

    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-full);
    cursor: pointer;
    white-space: nowrap;
    transition:
      color var(--duration) var(--ease),
      background-color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .sc-chip:hover,
  .sc-chip:focus-visible {
    color: var(--text-primary);
    border-color: var(--border-strong);
  }

  /* One accent, on the chip that is actually active. Twelve differently colored
     concepts would turn a scientific catalog into a bag of sweets, and color
     alone would be the only thing carrying the state. aria-pressed carries it
     for anyone who cannot see the fill. */
  .sc-chip.is-active {
    color: var(--accent-contrast);
    background: var(--accent);
    border-color: var(--accent-strong);
  }

  .sc-chip-count {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    opacity: 0.65;
  }

  .sc-concept-note {
    max-width: 78ch;
    margin: var(--space-3) 0 0;
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-secondary);
  }

  .sc-count {
    margin: var(--space-4) 0 var(--space-3);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }

  /* --- The grid ------------------------------------------------------------- */

  .sc-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding-bottom: var(--space-6);
    scrollbar-width: thin;
    scrollbar-color: var(--border-strong) transparent;
  }

  .sc-grid {
    display: grid;
    /* auto-fill rather than auto-fit: a single result keeps a card-sized card
       instead of stretching across the whole panel. */
    grid-template-columns: repeat(auto-fill, minmax(15.5rem, 1fr));
    gap: var(--space-4);
  }

  .sc-card {
    display: flex;
    flex-direction: column;
    padding: 0;
    overflow: hidden;
    text-align: left;

    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition:
      transform var(--duration) var(--ease-out),
      border-color var(--duration) var(--ease),
      box-shadow var(--duration) var(--ease);
  }

  .sc-card:hover,
  .sc-card:focus-visible {
    transform: translateY(-2px);
    border-color: var(--accent-line);
    box-shadow: var(--shadow);
  }

  /* --- Thumbnail ------------------------------------------------------------ */

  .sc-card-shot {
    position: relative;
    display: block;
    /* Never shrink. The card is a column flex container, and a flex item with
       only an aspect ratio to size it gets collapsed to a sliver when the text
       below it is tall: on a phone every thumbnail came out four pixels high. */
    flex: 0 0 auto;
    /* Reserved before the image arrives, so lazy loading cannot make the grid
       jump as cards scroll into view. */
    aspect-ratio: 16 / 9;
    background: var(--space-far);
    border-bottom: 1px solid var(--border-subtle);
  }

  .sc-card-shot img {
    display: block;
    width: 100%;
    height: 100%;
    object-fit: cover;
  }

  /* The fallback sits under the image and is only revealed when the image is
     missing or fails: a scenario added before its thumbnail is generated gets a
     titled placeholder rather than the browser's broken-image glyph. */
  .sc-card-fallback {
    position: absolute;
    inset: 0;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: var(--space-1);
    padding: var(--space-3);
    text-align: center;
    background:
      radial-gradient(
        90% 120% at 50% 0%,
        color-mix(in srgb, var(--accent) 12%, transparent) 0%,
        transparent 70%
      ),
      var(--space-far);
  }

  .sc-card-shot.is-missing img {
    display: none;
  }

  .sc-card-shot.is-missing .sc-card-fallback {
    display: flex;
  }

  .sc-card-fallback-mark {
    font-size: var(--text-lg);
    color: var(--accent);
    opacity: 0.7;
  }

  .sc-card-fallback-name {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    line-height: 1.4;
    color: var(--text-muted);
  }

  /* --- Card text ------------------------------------------------------------ */

  .sc-card-body {
    display: flex;
    flex: 1 1 auto;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-4) var(--space-4);
  }

  .sc-card-title {
    font-size: var(--text-base);
    font-weight: var(--weight-semibold);
    line-height: 1.35;
    color: var(--text-primary);
    /* Some catalog titles carry a subtitle after a colon. Two lines is enough
       for those and stops one long title setting the height of a whole row. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    overflow: hidden;
  }

  .sc-card-summary {
    font-size: var(--text-xs);
    line-height: 1.55;
    color: var(--text-secondary);
    /* Clamped, not rewritten: the full summary stays in the catalog, reaches
       the card's title attribute, and is shown in full by the scenario card
       once the scenario is loaded. */
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
  }

  .sc-card-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-1);
    margin-top: auto;
    padding-top: var(--space-1);
  }

  .sc-pill {
    padding: 2px var(--space-2);
    font-size: var(--text-2xs);
    line-height: 1.5;
    color: var(--text-muted);
    background: var(--surface-3);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-full);
    white-space: nowrap;
  }

  .sc-pill.is-more {
    color: var(--text-secondary);
    background: none;
  }

  #scenarioListContent .scenario-search-empty {
    margin: var(--space-8) 0;
    text-align: center;
    color: var(--text-secondary);
  }

  /* --- Narrow screens -------------------------------------------------------- */

  @media (max-width: 900px) {
    #scenarioListContent {
      width: calc(100vw - var(--space-4));
      max-height: 92vh;
      padding: var(--space-5) var(--space-4) 0;
    }

    /* One row that scrolls sideways, rather than four rows of chips before the
       first scenario is visible. The row is a flex line with no wrapping, so
       the active concept can be swept to without losing vertical space. */
    .sc-chips {
      flex-wrap: nowrap;
      overflow-x: auto;
      scrollbar-width: none;
      padding-bottom: var(--space-1);
      /* A soft edge is the only affordance needed to say "this scrolls". */
      mask-image: linear-gradient(
        to right,
        #000 0,
        #000 calc(100% - 24px),
        transparent 100%
      );
    }

    .sc-chips::-webkit-scrollbar {
      display: none;
    }

    .sc-chip {
      /* Comfortable as a touch target without becoming a button wall. */
      min-height: 36px;
    }
  }

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

    .sc-card {
      min-height: 44px;
    }

    .sc-head h3 {
      font-size: var(--text-lg);
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .sc-card:hover,
    .sc-card:focus-visible {
      transform: none;
    }
  }
}

@layer components {
  /* ==========================================================================
     Object inspector
     --------------------------------------------------------------------------
     A compact instrument readout, not a modal. It replaced a 600px panel with a
     2px cyan border, a 20px blur and four separate glows, which covered 35% of
     a 1440x900 window and made the thing it was describing hard to watch.

     The rule that shapes the rest: the simulation is the spectacle, and this
     panel is the label beside it. One-pixel border, one shadow, accent used
     only where it carries meaning.
     ====================================================================== */
  #objectInspector {
    position: fixed;
    top: 96px;
    right: 300px;
    width: 420px;
    max-width: calc(100vw - 2 * var(--space-4));
    max-height: min(72vh, 640px);

    display: none;
    flex-direction: column;

    color: var(--text-primary);
    font-family: var(--font-sans);
    background: color-mix(in srgb, var(--surface-1) 94%, transparent);
    border: 1px solid var(--border);
    border-radius: var(--radius-lg);
    /* 10px rather than 20: the panel still reads as glass, and half the
       compositing work goes away. */
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    box-shadow: var(--shadow-lg);
    z-index: 2000;
    overflow: hidden;
    /* Never animate: the drag implementation writes left/top directly, and a
       transition on that produces the jump this panel used to be famous for. */
    transition: none;
  }

  #objectInspector.visible {
    display: flex;
  }

  #objectInspector.dragging {
    cursor: grabbing;
    user-select: none;
  }

  /* --- Header: identity on the left, actions on the right ------------------ */
  .inspector-header {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--border-subtle);
    cursor: grab;
    flex: 0 0 auto;
  }
  .inspector-header:active {
    cursor: grabbing;
  }

  .inspector-icon {
    font-size: 19px;
    line-height: 1;
    flex: 0 0 auto;
  }

  .inspector-identity {
    display: flex;
    flex-direction: column;
    min-width: 0;
    flex: 1 1 auto;
    gap: 1px;
  }

  .inspector-title {
    margin: 0;
    font-size: var(--text-md);
    font-weight: 600;
    line-height: 1.2;
    color: var(--text-primary);
    /* A long name truncates rather than pushing the delete button off. */
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The object type as a quiet eyebrow, so the name can be the loud thing. */
  .inspector-kind {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    line-height: 1.2;
  }
  .inspector-kind:empty {
    display: none;
  }

  /* Buttons must not start a drag: the header is the drag handle. */
  .inspector-controls > * {
    cursor: pointer;
  }

  /* --- Tabs: a restrained segmented control, not two large buttons --------- */
  .inspector-tabs {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
    margin: var(--space-2) var(--space-3) 0;
    padding: 2px;
    background: var(--surface-2);
    border-radius: var(--radius-sm);
    flex: 0 0 auto;
  }

  .inspector-tab {
    min-height: 28px;
    padding: 0 var(--space-2);
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    font-weight: 600;
    color: var(--text-muted);
    background: transparent;
    border: none;
    border-radius: calc(var(--radius-sm) - 1px);
    cursor: pointer;
  }
  .inspector-tab:hover {
    color: var(--text-secondary);
  }
  .inspector-tab.active {
    color: var(--text-primary);
    background: var(--surface-3, var(--surface-1));
    box-shadow: var(--shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.3));
  }
  .inspector-tab:focus-visible {
    outline: var(--focus-ring);
    outline-offset: -2px;
  }

  .inspector-content {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--space-3);
  }
  .inspector-tab-content {
    display: none;
  }
  .inspector-tab-content.active {
    display: block;
  }

  /* --- Mass: label, slider and value on one row --------------------------- */
  .insp-mass {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-2);
    margin-bottom: var(--space-3);
  }
  .insp-mass-label {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: var(--text-xs);
    font-weight: 600;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .insp-mass input[type='range'] {
    width: 100%;
    min-width: 0;
    margin: 0;
  }
  /* A readout, not a badge: no glow, no fill, just a bordered number. */
  .insp-mass-value {
    min-width: 84px;
    max-width: 124px;
    padding: 2px 6px;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    text-align: right;
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
  }

  /* --- Properties: a dense key/value table -------------------------------- */
  .insp-props {
    margin: 0;
  }
  .insp-prop {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
    padding: 5px 0;
  }
  /* A hairline between rows rather than a border under every one: enough to
     carry the eye across, not enough to draw a grid. Two grid children per row
     left a gap in the line where the columns met, which read as a stack of
     boxes rather than a table. */
  .insp-prop + .insp-prop {
    border-top: 1px solid
      color-mix(in srgb, var(--border-subtle) 55%, transparent);
  }
  .insp-prop-label {
    flex: 0 0 auto;
    font-size: var(--text-xs);
    color: var(--text-muted);
    white-space: nowrap;
  }
  .insp-prop-value {
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--text-primary);
    text-align: right;
    overflow-wrap: anywhere;
  }

  /* A group heading, used only where a section genuinely clarifies. */
  .insp-group {
    grid-column: 1 / -1;
    margin-top: var(--space-2);
    padding-top: var(--space-2);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    border-top: 1px solid var(--border-subtle);
  }
  .insp-props > .insp-group:first-child {
    margin-top: 0;
    padding-top: 0;
    border-top: none;
  }

  /* --- Appearance and environment, for a black hole ----------------------- */
  /* A disclosure, because everything in it is a display choice and it is not
     what most people open the inspector for. Reuses .insp-toggle-row so the
     rows line up with the overlay toggles above them. */
  .insp-appearance {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
  }
  .insp-appearance > summary {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    cursor: pointer;
  }
  .insp-appearance-note {
    font-size: var(--text-xs);
    line-height: 1.45;
    color: var(--text-muted);
    margin: var(--space-1) 0 var(--space-2);
  }
  .insp-appearance .insp-toggle-row > label {
    flex: 1 1 auto;
    font-size: var(--text-xs);
  }
  .insp-appearance select,
  .insp-appearance input[type='range'] {
    flex: 0 1 auto;
    max-width: 55%;
  }
  .insp-appearance output {
    font-variant-numeric: tabular-nums;
    font-size: var(--text-xs);
    color: var(--text-muted);
    min-width: 3.2em;
    text-align: right;
  }

  /* --- Overlay toggles, as one compact section ---------------------------- */
  .insp-overlays {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
  }
  .insp-overlays-title {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
    margin-bottom: var(--space-1);
  }
  .insp-toggle-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: 28px;
  }
  .insp-toggle-row > span:first-child {
    flex: 1 1 auto;
    font-size: var(--text-xs);
    color: var(--text-secondary);
  }

  /* --- Help and disclosure ------------------------------------------------ */
  .insp-help {
    width: 16px;
    height: 16px;
    min-height: 16px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: 10px;
    line-height: 1;
    color: var(--text-muted);
    background: transparent;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-full);
    cursor: help;
  }
  .insp-help:hover {
    color: var(--text-primary);
    border-color: var(--accent);
  }

  .insp-about {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
  }
  .insp-about > summary {
    font-size: var(--text-xs);
    color: var(--text-secondary);
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: center;
    gap: 6px;
    min-height: 24px;
  }
  .insp-about > summary::-webkit-details-marker {
    display: none;
  }
  .insp-about > summary::before {
    content: '▸';
    font-size: 9px;
    color: var(--text-muted);
    transition: transform 120ms ease;
  }
  .insp-about[open] > summary::before {
    transform: rotate(90deg);
  }
  .insp-about-body {
    margin-top: var(--space-2);
    font-size: var(--text-xs);
    line-height: var(--leading-normal, 1.5);
    color: var(--text-secondary);
  }
  .insp-about-body p {
    margin: 0 0 var(--space-2);
  }
  .insp-about-body p:last-child {
    margin-bottom: 0;
  }

  /* --- Energy tab --------------------------------------------------------- */
  .insp-energy-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    margin-bottom: var(--space-2);
  }
  .insp-energy-title {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.08em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .insp-energy-actions {
    display: flex;
    gap: var(--space-1);
  }
  .insp-icon-btn {
    width: 26px;
    height: 26px;
    min-height: 26px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    font-size: var(--text-xs);
    color: var(--text-muted);
    background: transparent;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    cursor: pointer;
  }
  .insp-icon-btn:hover {
    color: var(--text-primary);
    border-color: var(--accent);
    background: var(--surface-2);
  }

  .insp-energy-chart {
    position: relative;
    height: 190px;
    margin-top: var(--space-2);
  }
  /* Pinned to the container so the chart can never push the panel taller than
     the box it was given. Chart.js still owns the bitmap size and the device
     pixel ratio; this only fixes the box it draws into. */
  .insp-energy-chart canvas {
    position: absolute;
    inset: 0;
    display: block;
  }

  /* Between a wide desktop and a phone - a small laptop, or a big window at
     200% browser zoom - the control rail takes a bigger share of the width, so
     the panel docks nearer the edge rather than sitting on top of it. */
  @media (max-width: 1180px) {
    #objectInspector {
      right: var(--space-3);
      width: min(400px, calc(100vw - 2 * var(--space-3)));
    }
  }

  /* --- Narrow screens: a sheet, but only as tall as it needs to be -------- */
  @media (max-width: 620px) {
    #objectInspector {
      top: auto;
      right: var(--space-2);
      left: var(--space-2);
      bottom: calc(var(--space-2) + env(safe-area-inset-bottom, 0px));
      width: auto;
      max-width: none;
      /* Content-driven. A planet with six properties has no business filling
         the screen just because the device is narrow. */
      max-height: min(78vh, calc(100vh - 120px));
    }

    /* The header is the only way out once the body scrolls, so it stays put. */
    .inspector-header {
      position: sticky;
      top: 0;
      z-index: 2;
      background: color-mix(in srgb, var(--surface-1) 96%, transparent);
      /* Dragging a sheet by its header fights the scroll underneath it. */
      cursor: default;
    }
    .inspector-header:active {
      cursor: default;
    }

    .inspector-tab {
      min-height: 36px;
    }
    .insp-toggle-row {
      min-height: 44px;
    }
    .insp-about > summary {
      min-height: 40px;
    }
    /* Visually small, but a comfortable target underneath. */
    .inspector-close,
    .inspector-delete {
      position: relative;
    }
    .inspector-close::after,
    .inspector-delete::after {
      content: '';
      position: absolute;
      inset: -7px;
    }
    .insp-mass {
      grid-template-columns: auto 1fr;
      row-gap: var(--space-1);
    }
    .insp-mass-value {
      grid-column: 2;
      justify-self: end;
    }
    .insp-mass input[type='range'] {
      grid-column: 1 / -1;
      min-height: 32px;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .insp-about > summary::before {
      transition: none;
    }
  }
}

/* ---------------------------------------------------------------------------
   Pinned comparison cards
   ---------------------------------------------------------------------------
   A pinned card is the inspector's property table with an identity strip on
   top, and nothing else. It reads as a smaller, quieter sibling of the panel it
   came from rather than a second copy of it: narrower, no tabs, no controls.
   --------------------------------------------------------------------------- */

.inspector-pin {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: none;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-secondary);
  cursor: pointer;
  transition:
    background var(--dur-fast) var(--ease-out),
    color var(--dur-fast) var(--ease-out);
}

.inspector-pin:hover {
  background: var(--surface-3);
  color: var(--text-primary);
}

.inspector-pin:focus-visible {
  outline: var(--focus-ring);
  outline-offset: 2px;
}

#pinnedInspectors {
  /* A positioning context only; the cards themselves are fixed. */
  position: absolute;
  inset: 0;
  pointer-events: none;
  z-index: 950;
}

.insp-pin {
  position: fixed;
  width: 300px;
  /* Two cards plus the gaps have to fit one column of an 800px window, or the
     comparison the pin exists for does not fit on screen. */
  max-height: 38vh;
  overflow-y: auto;
  padding: var(--space-2) var(--space-3) var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  /* Matches the inspector's own ground so a card reads as the same instrument
     in a smaller case. --surface-raised does not exist; it rendered clear. */
  background: color-mix(in srgb, var(--surface-1) 94%, transparent);
  backdrop-filter: blur(10px);
  box-shadow: var(--shadow-md);
  pointer-events: auto;
}

.insp-pin-header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
  padding-bottom: var(--space-2);
  border-bottom: 1px solid var(--border-subtle);
  cursor: grab;
  user-select: none;
}

.insp-pin-header:active {
  cursor: grabbing;
}

.insp-pin-header .inspector-identity {
  flex: 1 1 auto;
  min-width: 0;
}

.insp-pin .insp-props {
  font-size: var(--text-xs);
}

@media (max-width: 620px) {
  /* One narrow column of cards would leave no room for the panel they are
     being compared against, so on a phone they span the width and stack. */
  .insp-pin {
    width: calc(100vw - 2 * var(--space-3));
    max-height: 34vh;
  }

  /* 30px is under the 44px a finger needs, and this is the control that gets
     the card off the screen again. */
  .insp-pin .inspector-close,
  .inspector-pin {
    min-width: 44px;
    min-height: 44px;
  }
}

/* ---------------------------------------------------------------------------
   Reference-frame switcher
   ---------------------------------------------------------------------------
   A .rail-field row, plus a line of state that appears only while a frame is
   active. The rail is already taller than a laptop screen with every group
   open, so a permanent three-line explainer here would push the primary action
   over the last tool; the note earns its space only when there is something to
   say.
   --------------------------------------------------------------------------- */

.rail-field-note {
  margin: 0;
  padding: 0 var(--space-3) var(--space-2);
  font-size: var(--text-xs);
  line-height: 1.35;
  color: var(--accent);
}

.rail-field-note[hidden] {
  display: none;
}

/* Shown only when the scene contains a cluster of galaxies. A rotation curve is
   the wrong instrument for a swarm on randomly oriented orbits; these three
   numbers are the right one, and they are the same three Zwicky worked from. */
.rc-cluster {
  padding: 0 var(--space-3);
}

.rc-cluster-title {
  margin: 0 0 var(--space-1);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* --- Rotation curve panel ----------------------------------------------------
   Reuses the shared observation-panel shell. Only two things are new: a legend
   that has to name three marks the canvas draws, and the halo switch, which is
   the one control in the app that changes the force law rather than the view.
   --------------------------------------------------------------------------- */

.rc-legend {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px var(--space-2);
  margin: 0;
  padding: 0 var(--space-3);
  font-size: 10px;
  line-height: 1.4;
  color: var(--text-muted);
}

.rc-key {
  display: inline-block;
  width: 16px;
  height: 2px;
  flex: none;
  vertical-align: middle;
}

.rc-key-points {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--accent);
}

/* These two must match what chartColors() hands the canvas, fallbacks included.
   --info is not defined in tokens.css, so chartColors falls back to #5b9bd5 and
   a legend key written as plain var(--info) drew nothing at all. Dashed here
   because the canvas draws that series dashed: two different marks for one
   series would be worse than no legend. */
.rc-key-visible {
  height: 0;
  border-top: 2px dashed var(--danger, #e2725b);
}

.rc-key-total {
  background: var(--info, #5b9bd5);
}

.rc-toggle-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--text-xs);
  color: var(--text-secondary);
}

.rc-controls .obs-control-row + .obs-control-row {
  margin-top: var(--space-1);
}

.rc-cluster-note {
  margin: var(--space-1) 0 0;
}

/* ==========================================================================
   Rotation curve: choosing which law governs the outskirts
   --------------------------------------------------------------------------
   Three mutually exclusive choices, so they are drawn as one segmented control
   rather than as three switches. A reader must not be able to form the idea
   that a halo and MOND could both be running: they are competing explanations
   for the same measurement and applying both would be physics nobody holds.
   ====================================================================== */
@layer components {
  .rc-mode-row {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-bottom: var(--space-2);
  }

  .rc-mode-buttons {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 2px;
    padding: 2px;
    background: var(--surface-2);
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
  }

  .rc-mode {
    min-width: 0;
    padding: var(--space-1) var(--space-2);
    background: transparent;
    border: 1px solid transparent;
    border-radius: calc(var(--radius-sm) - 1px);
    color: var(--text-secondary);
    font: inherit;
    font-size: var(--text-2xs);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    cursor: pointer;
  }

  .rc-mode:hover:not(:disabled),
  .rc-mode:focus-visible {
    background: var(--surface-3);
    color: var(--text-primary);
  }

  /* The selected law. Marked by weight and a filled ground as well as color,
     because which model is on the plot is the single most important piece of
     state in this panel. */
  .rc-mode[aria-pressed='true'] {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-contrast);
    font-weight: var(--weight-semibold);
  }

  /* MOND is offered only where a galactic scale has been declared. Disabled
     rather than hidden, so that a reader in the Solar System can see the option
     exists and read why it does not apply here. */
  .rc-mode:disabled {
    opacity: 0.45;
    cursor: not-allowed;
  }

  /* Fitted versus fixed. The distinction the panel exists to make legible: two
     curves of similar quality mean very different things when one of them spent
     two free parameters getting there. */
  .rc-params {
    margin: 0 0 var(--space-2);
    font-size: var(--text-2xs);
    color: var(--text-muted);
    line-height: 1.4;
  }

  .rc-param {
    display: inline-block;
    margin-right: var(--space-1);
    padding: 0 var(--space-1);
    border-radius: var(--radius-xs, 3px);
    font-weight: var(--weight-semibold);
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }

  .rc-param.is-fitted {
    background: color-mix(in srgb, var(--danger) 22%, transparent);
    color: var(--danger);
  }

  .rc-param.is-fixed {
    background: color-mix(in srgb, var(--success) 22%, transparent);
    color: var(--success);
  }

  .rc-a0 {
    margin: 0 0 var(--space-2);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  /* The MOND curve's legend key, matching the long dash it is drawn with. */
  .rc-key-mond {
    background: repeating-linear-gradient(
      90deg,
      var(--success) 0 9px,
      transparent 9px 12px
    );
  }
}

@layer components {
  /* The limitations disclosure in the rotation-curve panel. MOND fits rotation
     curves well, which is the one thing this panel can demonstrate; the rest of
     the picture belongs beside it rather than only in the documentation. */
  .rc-limits {
    margin: 0 0 var(--space-2);
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .rc-limits > summary {
    cursor: pointer;
    color: var(--text-secondary);
  }

  .rc-limits ul {
    margin: var(--space-2) 0 0;
    padding-left: var(--space-4);
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    line-height: 1.4;
  }
}

/* ---------------------------------------------------------------------------
 * The authoring preview bar
 * ---------------------------------------------------------------------------
 * Only ever visible when ?author= put the panel in preview mode, and styled to
 * be unmistakable: an author who has forgotten which mode they are in must not
 * confuse a preview with what a class sees.
 * ------------------------------------------------------------------------- */
@layer components {
  .inv-author-bar {
    border-bottom: 1px solid var(--border, #2a2f3a);
    background: color-mix(in srgb, var(--accent, #4dd0e1) 8%, transparent);
    padding: var(--space-2, 8px) var(--space-3, 12px);
    font-size: 11px;
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    max-height: 40vh;
    overflow-y: auto;
  }

  .inv-author-bar[hidden] {
    display: none !important;
  }

  .inv-author-jump {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
  }

  .inv-author-badge {
    font-weight: 700;
    letter-spacing: 0.08em;
    font-size: 9px;
    padding: 2px 6px;
    border-radius: 3px;
    background: var(--accent, #4dd0e1);
    color: var(--bg, #10131a);
  }

  .inv-author-jump input {
    width: 4.5em;
    font: inherit;
    padding: 2px 4px;
  }

  .inv-author-facts {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px var(--space-2, 8px);
    align-items: baseline;
  }

  .inv-author-fact {
    display: contents;
  }

  .inv-author-fact > span {
    opacity: 0.65;
    text-transform: uppercase;
    font-size: 9px;
    letter-spacing: 0.06em;
  }

  .inv-author-fact > code {
    font-size: 11px;
    word-break: break-word;
  }

  .inv-author-findings {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }

  .inv-author-findings.is-clean {
    opacity: 0.55;
    font-style: italic;
  }

  .inv-author-finding {
    padding: 4px 6px;
    border-radius: 3px;
    border-left: 3px solid transparent;
    background: color-mix(in srgb, currentcolor 6%, transparent);
  }

  .inv-author-finding.is-error {
    border-left-color: var(--danger, #ef5350);
  }

  .inv-author-finding.is-warn {
    border-left-color: var(--warning, #ffb74d);
  }

  .inv-author-level {
    font-weight: 700;
    text-transform: uppercase;
    font-size: 9px;
    margin-right: 6px;
  }

  .inv-author-rule {
    opacity: 0.55;
    margin-left: 6px;
    font-size: 9px;
  }
}

/* ---------------------------------------------------------------------------
 * Target size (WCAG 2.2 AA, 2.5.8)
 * ---------------------------------------------------------------------------
 * Every pointer target has to be at least 24 by 24 CSS pixels unless it is
 * inline in a sentence. Three kinds of control here were not.
 *
 * The sliders were the worst: the input's box is seven pixels tall, so the
 * whole width of a settings slider was a seven-pixel-high target. The fix
 * keeps the thin track - it is a deliberate part of the design and a fat one
 * would crowd the panel - by growing the element and painting its background
 * only inside the content box. The track looks identical; the element that
 * receives the pointer is twenty-four pixels tall.
 * ------------------------------------------------------------------------- */
@layer components {
  input[type='range'] {
    /* box-sizing is content-box for the padding to add to the height rather
       than eat into it, and background-clip keeps the painted track thin. */
    box-sizing: content-box;
    padding-block: 8px;
    background-clip: content-box;
    min-height: 8px;
  }

  /* The thumb is the target a pointer usually goes for, so it gets there too.
     18px was under the minimum on its own. */
  input[type='range']::-webkit-slider-thumb {
    width: 24px;
    height: 24px;
  }

  input[type='range']::-moz-range-thumb {
    width: 24px;
    height: 24px;
  }

  /* The inspector's help affordances were 16px circles. */
  .insp-help {
    width: 24px;
    height: 24px;
    min-height: 24px;
    font-size: 11px;
  }

  /* A bare checkbox is 13-15px in every engine. The label beside it is part of
     the same target, but the box itself is what a finger aims at. */
  .share-check input[type='checkbox'],
  .settings-panel input[type='checkbox'],
  input[type='checkbox'].ui-checkbox {
    width: 24px;
    height: 24px;
    accent-color: var(--accent);
  }
}

/* ---------------------------------------------------------------------------
 * Progress safety: the save status and the backup controls
 * ---------------------------------------------------------------------------
 * The status is a standing statement, not a toast. A storage failure has to
 * keep saying so until it is resolved, and the two states are visually
 * different because they mean different things: one is reassurance a reader can
 * ignore, the other is work about to be lost.
 * ------------------------------------------------------------------------- */
@layer components {
  .inv-progress-safety {
    padding: var(--space-2, 8px) var(--space-3, 12px);
    border-top: 1px solid var(--border-subtle, #232a38);
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
  }

  .inv-save-status {
    margin: 0;
    font-size: var(--text-2xs, 11px);
    line-height: 1.45;
    color: var(--text-muted);
  }

  .inv-save-status[hidden] {
    display: none !important;
  }

  /* A problem, not a reassurance. Bordered rather than colored text alone:
     color is not the only channel this may be read through. */
  .inv-save-status[data-state='full'],
  .inv-save-status[data-state='unavailable'] {
    color: var(--text-primary);
    background: color-mix(in srgb, var(--warning) 10%, transparent);
    border-left: 3px solid var(--warning);
    padding: var(--space-2, 8px);
    border-radius: var(--radius-sm, 4px);
  }

  .inv-backup-actions {
    display: flex;
    flex-wrap: wrap;
    gap: var(--space-2, 8px);
  }

  .inv-backup-actions[hidden] {
    display: none !important;
  }

  .inv-backup-actions .ui-button {
    font-size: var(--text-2xs, 11px);
    flex: 1 1 auto;
  }
}

/* =============================================================================
   The synthetic observing run
   -----------------------------------------------------------------------------
   Folded into the radial-velocity panel and collapsed until it is switched on.
   It adds four number fields to a panel that had none, so the fields are laid
   out on a grid that reflows to one column on a phone rather than overflowing
   the panel, and the status line reserves no space when there is nothing to
   say.
   ========================================================================== */
@layer components {
  .rv-survey {
    border-top: 1px solid var(--border, rgb(255 255 255 / 12%));
    margin-top: 0.5rem;
    padding-top: 0.5rem;
  }

  .rv-survey-toggle {
    align-items: center;
    cursor: pointer;
    display: flex;
    font-size: 0.78rem;
    gap: 0.4rem;
  }

  .rv-survey-fields {
    display: grid;
    gap: 0.5rem;
    margin-top: 0.5rem;
  }

  .rv-survey-hint {
    color: var(--text-dim, #9aa4b2);
    font-size: 0.72rem;
    line-height: 1.4;
    margin: 0;
  }

  .rv-survey-grid {
    display: grid;
    gap: 0.4rem 0.6rem;
    grid-template-columns: repeat(auto-fit, minmax(9.5rem, 1fr));
  }

  .rv-survey-field {
    align-items: center;
    display: flex;
    font-size: 0.72rem;
    gap: 0.3rem;
  }

  .rv-survey-field > span:first-child {
    color: var(--text-dim, #9aa4b2);
    flex: 1 1 auto;
  }

  .rv-survey-field input,
  .rv-survey-list textarea,
  .rv-survey-list input {
    background: var(--input-bg, rgb(255 255 255 / 6%));
    border: 1px solid var(--border, rgb(255 255 255 / 15%));
    border-radius: 4px;
    color: inherit;
    font: inherit;
    font-size: 0.72rem;
    min-width: 0;
    padding: 0.15rem 0.3rem;
    width: 4.5rem;
  }

  .rv-survey-unit {
    color: var(--text-dim, #9aa4b2);
    flex: 0 0 auto;
  }

  .rv-survey-actions {
    align-items: center;
    display: flex;
    flex-wrap: wrap;
    gap: 0.5rem;
    justify-content: space-between;
  }

  .rv-survey-status {
    font-size: 0.72rem;
    line-height: 1.4;
    margin: 0;
  }

  /* A border rather than a color on its own: the warning has to survive a
     color-blind reader and a monochrome printout. */
  .rv-survey-status[data-state='warn'],
  /* --- The binary lesson's sweep ------------------------------------------ */
  /* Almost nothing, deliberately. The section is built out of the panel
     system's own furniture - obs-panel-hint for the prose, pause-event-actions
     for the controls, experiment-table for the table - so the only rules here
     are the two things it has that nothing else does: a plot, and rows dimmed
     because nothing can be concluded from them. */
  .experiment-table tr[data-outcome='unreliable'],
  .experiment-table tr[data-outcome='incomplete'],
  .experiment-table tr[data-outcome='notRun'] {
    opacity: 0.75;
  }

  /* --- When the telescope looked ------------------------------------------ */
  /* Separated from the cadence fields above it by a rule rather than by space
     alone: the two groups answer different questions - how precisely, and at
     what times - and a reader who reads them as one grid of eight numbers has
     to work out which is which. */
  .rv-survey-schedule,
  .rv-survey-compare,
  .binary-sweep,
  /* The assist lesson's two experiments: same furniture, same rule. */
  .assist-experiment {
    border-top: 1px solid var(--border-subtle, rgb(255 255 255 / 10%));
    padding-top: 0.5rem;
  }

  .rv-survey-field select {
    background: var(--input-bg, rgb(255 255 255 / 6%));
    border: 1px solid var(--border, rgb(255 255 255 / 15%));
    border-radius: 4px;
    color: inherit;
    font: inherit;
    font-size: 0.72rem;
    min-width: 0;
    padding: 0.15rem 0.3rem;
  }

  /* A list of times is prose-shaped, so it gets the width of the panel rather
     than the 4.5rem a number field gets. */
  .rv-survey-list {
    display: grid;
    font-size: 0.72rem;
    gap: 0.2rem;
  }

  .rv-survey-list > span {
    color: var(--text-dim, #9aa4b2);
  }

  /* A list of times is prose-shaped, so it overrides the width and the face
     the field rule above gives every other input in this panel. */
  .rv-survey-list textarea,
  .rv-survey-list input {
    font-family: var(--font-mono, monospace);
    padding: 0.25rem 0.3rem;
    resize: vertical;
    width: 100%;
  }

  .rv-survey-note,
  .rv-survey-compare-report {
    font-size: 0.72rem;
    line-height: 1.4;
    margin: 0;
  }

  .rv-survey-note:empty,
  .rv-survey-compare-report:empty {
    display: none;
  }

  .rv-survey-note[data-state='warn'],
  .rv-survey-compare-report[data-state='warn'] {
    border-left: 3px solid var(--warning, #e0a458);
    padding-left: 0.5rem;
  }

  .rv-survey-compare {
    display: grid;
    gap: 0.4rem;
  }
}

/* =============================================================================
   Pause at event
   -----------------------------------------------------------------------------
   A form rather than a chart, so it borrows the observing panel's chrome and
   supplies its own field layout. The fields reflow to one column on a phone,
   and the refusal message is distinguished by a border rather than by color
   alone so it survives a color-blind reader and a printout.
   ========================================================================== */
@layer components {
  .pause-event-form {
    display: grid;
    gap: 0.45rem;
    padding: 0.5rem 0;
  }

  .pause-event-field {
    align-items: center;
    display: flex;
    font-size: 0.75rem;
    gap: 0.4rem;
  }

  .pause-event-field > span:first-child {
    color: var(--text-dim, #9aa4b2);
    flex: 0 0 5.5rem;
  }

  .pause-event-field select,
  .pause-event-field input {
    background: var(--input-bg, rgb(255 255 255 / 6%));
    border: 1px solid var(--border, rgb(255 255 255 / 15%));
    border-radius: 4px;
    color: inherit;
    flex: 1 1 auto;
    font: inherit;
    font-size: 0.75rem;
    min-width: 0;
    padding: 0.2rem 0.3rem;
  }

  .pause-event-unit {
    color: var(--text-dim, #9aa4b2);
    flex: 0 0 auto;
  }

  .pause-event-actions {
    display: flex;
    gap: 0.4rem;
  }

  .pause-event-message {
    border-left: 3px solid transparent;
    font-size: 0.73rem;
    line-height: 1.4;
    margin: 0 0 0.4rem;
    padding-left: 0.5rem;
  }

  .pause-event-message[data-kind='refused'] {
    border-left-color: var(--warning, #e0a458);
  }

  .pause-event-message[data-kind='fired'] {
    border-left-color: var(--accent, #6fb1ff);
  }

  .pause-event-readout {
    display: grid;
    gap: 0.4rem;
    grid-template-columns: repeat(auto-fit, minmax(7rem, 1fr));
  }

  .pause-event-note {
    display: flex;
    flex-direction: column;
    font-size: 0.73rem;
    gap: 0.2rem;
    margin-top: 0.5rem;
  }

  .pause-event-note span {
    color: var(--text-dim, #9aa4b2);
  }

  .pause-event-note input {
    background: var(--input-bg, rgb(255 255 255 / 6%));
    border: 1px solid var(--border, rgb(255 255 255 / 15%));
    border-radius: 4px;
    color: inherit;
    font: inherit;
    font-size: 0.75rem;
    padding: 0.25rem 0.35rem;
  }
}

@layer components {
  /* The pause-at-event marker on the timeline track. Positioned by clock, so it
     stays on the moment rather than on a frame index that shifts as the ring
     buffer fills. */
  /* The scrubber's own box, so a marker at a percentage lands on the track. */
  .timeline-track {
    align-items: center;
    display: flex;
    flex: 1 1 auto;
    min-width: 0;
    position: relative;
  }

  .timeline-track .timeline-scrubber {
    flex: 1 1 auto;
    min-width: 0;
  }

  .timeline-event-marker {
    background: var(--accent, #6fb1ff);
    border-radius: 1px;
    height: 1rem;
    pointer-events: none;
    position: absolute;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 2px;
  }

  .timeline-event-marker::after {
    background: inherit;
    border-radius: 50%;
    content: '';
    height: 5px;
    left: 50%;
    position: absolute;
    top: -3px;
    transform: translateX(-50%);
    width: 5px;
  }
}

/* =============================================================================
   Answer feedback and staged hints
   -----------------------------------------------------------------------------
   An unreadable answer is not a wrong one, so it is not styled like one: it
   gets the neutral border a note gets rather than the red a wrong answer gets.
   Distinguished by border and wording as well as color, so the difference
   survives a color-blind reader and a printout.
   ========================================================================== */
@layer components {
  .inv-feedback.is-unreadable {
    border-left: 3px solid var(--info, #5b9bd5);
    padding-left: 0.5rem;
  }

  .inv-misconception {
    display: inline-block;
    margin-top: 0.3rem;
  }

  .inv-hints {
    display: grid;
    gap: 0.4rem;
    margin-top: 0.6rem;
  }

  .inv-hint {
    border-left: 3px solid var(--border, rgb(255 255 255 / 20%));
    font-size: 0.85rem;
    line-height: 1.5;
    margin: 0;
    padding-left: 0.6rem;
  }

  .inv-hint.is-worked {
    border-left-color: var(--accent, #6fb1ff);
  }

  .inv-hint-row {
    align-items: center;
    display: flex;
    gap: 0.6rem;
  }

  .inv-hint-tally {
    color: var(--text-dim, #9aa4b2);
    font-size: 0.75rem;
  }
}

/* --- The assignment builder --------------------------------------------------
   Reached from ?assign=<lesson>, so it is an instructor's whole screen rather
   than one panel among several: it overlays the simulation instead of docking
   beside it, because nobody building an assignment is watching the world.
   ---------------------------------------------------------------------------- */
@layer components {
  .assignment-builder {
    position: fixed;
    inset: 0;
    z-index: 300;
    background: var(--surface-0, rgba(3, 6, 16, 0.97));
    overflow-y: auto;
    padding: var(--space-4, 20px);
  }
  .assignment-inner {
    max-width: 760px;
    margin: 0 auto;
    display: flex;
    flex-direction: column;
    gap: var(--space-3, 12px);
  }
  .assignment-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-3, 12px);
  }
  .assignment-field {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .assignment-field input,
  .assignment-field textarea {
    width: 100%;
    font: inherit;
    padding: 6px 8px;
    background: var(--surface-2, rgba(255, 255, 255, 0.06));
    border: 1px solid var(--border-subtle, rgba(0, 180, 255, 0.3));
    border-radius: var(--radius-sm, 6px);
    color: inherit;
  }
  .assignment-row {
    display: flex;
    align-items: center;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
  }
  .assignment-steps {
    list-style: none;
    margin: 0;
    padding: 0;
    border: 1px solid var(--border-subtle, rgba(0, 180, 255, 0.3));
    border-radius: var(--radius-sm, 6px);
    max-height: 46vh;
    overflow-y: auto;
  }
  .assignment-step {
    display: flex;
    align-items: baseline;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
    padding: 5px 10px;
    border-bottom: 1px solid var(--border-faint, rgba(255, 255, 255, 0.07));
  }
  .assignment-step label {
    display: flex;
    align-items: baseline;
    gap: 8px;
    flex: 1 1 auto;
    min-width: 0;
    cursor: pointer;
  }
  /* A step that is in the activity because something else needs it looks
     included without looking chosen: the instructor did not tick it, and the
     interface should not pretend they did. */
  .assignment-step[data-included='true'] {
    background: var(--surface-2, rgba(255, 255, 255, 0.05));
  }
  .assignment-kind,
  .assignment-why {
    font-size: 0.82em;
    color: var(--text-muted, rgba(255, 255, 255, 0.6));
  }
  .assignment-why {
    flex-basis: 100%;
    padding-left: 26px;
    color: var(--accent-warm, #e8b04b);
  }
  .assignment-problem:not(:empty) {
    color: var(--accent-warm, #e8b04b);
  }
  .assignment-hint {
    font-size: 0.88em;
    color: var(--text-muted, rgba(255, 255, 255, 0.65));
    margin: 0;
  }
  #assignLinkNote[data-state='long'] {
    color: var(--accent-warm, #e8b04b);
  }
}

/* --- The maneuver planner ---------------------------------------------------
   A member of the bottom-left instrument stack, so it inherits .obs-panel's
   position, its height cap and its pinned toolbar.
   ---------------------------------------------------------------------------- */
@layer components {
  .maneuver-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    padding: 0 var(--space-3, 12px);
  }
  .maneuver-field {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2, 8px);
  }
  .maneuver-field input,
  .maneuver-field select {
    flex: 0 0 11ch;
  }
  .maneuver-preview table {
    width: 100%;
  }
  /* The prediction's assumptions, not a footnote to them. Kept visually
     distinct from the numbers so it does not read as one of them. */
  .maneuver-caveat {
    font-size: 0.85em;
    color: var(--text-muted, rgba(255, 255, 255, 0.66));
    border-left: 2px solid var(--border-subtle, rgba(0, 180, 255, 0.35));
    padding-left: var(--space-2, 8px);
    margin: 0;
  }
  .maneuver-unbound {
    color: var(--accent-warm, #e8b04b);
    font-weight: 600;
    margin: 4px 0 0;
  }
  .maneuver-actions {
    display: flex;
    gap: var(--space-2, 8px);
    flex-wrap: wrap;
  }
  .maneuver-log {
    max-height: 18vh;
    overflow-y: auto;
  }
}

/* --- The restricted three-body teaching mode ---------------------------------
   Another member of the bottom-left instrument stack.
   ---------------------------------------------------------------------------- */
@layer components {
  .cr3bp-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2, 8px);
    padding: 0 var(--space-3, 12px);
  }
  /* Whether the overlay's claims apply at all. Loud when they do not, because
     a diagram of a system that is not on screen is worse than no diagram. */
  .cr3bp-validity {
    margin: 0;
    font-size: 0.88em;
  }
  .cr3bp-validity[data-state='ok'] {
    color: var(--text-muted, rgba(255, 255, 255, 0.65));
  }
  .cr3bp-validity[data-state='invalid'] {
    color: var(--accent-warm, #e8b04b);
    font-weight: 600;
  }
  .cr3bp-jacobi {
    font-variant-numeric: tabular-nums;
    font-size: 1.05em;
  }
  .cr3bp-convention {
    font-size: 0.82em;
    color: var(--text-muted, rgba(255, 255, 255, 0.6));
    border-left: 2px solid var(--border-subtle, rgba(0, 180, 255, 0.35));
    padding-left: var(--space-2, 8px);
    margin: 0;
  }
  .cr3bp-claims summary {
    cursor: pointer;
    font-size: 0.88em;
  }
}

@layer components {
  /* --- Conservation check ------------------------------------------------
     Grouped and labeled. It used to be three unlabeled rows in the corner of
     every first visit; it is now off by default and, when asked for, arrives
     as a named block with its explanation reachable from the keyboard. */
  .readout-conservation {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border-subtle);
  }
  .readout-conservation-title {
    margin: 0 0 var(--space-1);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .readout-caveats {
    margin: var(--space-1) 0 0;
    padding-left: 1.1em;
    font-size: var(--text-2xs);
    color: var(--text-secondary);
  }
  .readout-caveats li {
    margin-bottom: 2px;
  }
  /* A <details>, not a title attribute: an explanation that only appears on
     hover is unreachable by keyboard and unreachable on a touch screen. */
  .readout-conservation-help {
    margin-top: var(--space-2);
    font-size: var(--text-2xs);
    color: var(--text-secondary);
  }
  .readout-conservation-help > summary {
    cursor: pointer;
    color: var(--accent);
  }
  .readout-conservation-help > summary:focus-visible {
    outline: var(--focus-ring);
    outline-offset: var(--focus-ring-offset);
  }
  .readout-conservation-help p {
    margin: var(--space-2) 0 0;
  }
}

@layer components {
  /* A number the model guessed rather than was told.
     -------------------------------------------------------------------------
     Every star the sandbox generates is a mass and nothing else, so its
     temperature, luminosity and radius are all main-sequence estimates. That
     is fine, and it is not fine to print them looking like measurements. The
     word is the signal; the smaller, dimmer type is a reinforcement, not the
     only difference. */
  .inspector-estimated {
    margin-left: 0.4em;
    font-size: var(--text-2xs);
    color: var(--text-muted);
    white-space: nowrap;
  }
}
