/* =============================================================================
   Chrome: the readout, the control rail, and the bottom dock
   -----------------------------------------------------------------------------
   Three pieces of the interface that had each grown past what their original
   layout could carry:

     - the readout was a sound panel, nine object counts of which most read
       zero, and a five-line crib of the keyboard controls
     - the control rail had reached forty rows in one column, most of them in
       Tools
     - the transport bar and the footer both live along the bottom edge and had
       no arrangement between them, so on a wide window the scrubber ran over
       the copyright line

   Last of the component-layer sheets, so it can restate what styles.css set in
   the legacy layer for the same elements without reaching for !important.
   ========================================================================== */

@layer components {
  /* ==========================================================================
     1. The readout
     --------------------------------------------------------------------------
     A header that says what the simulation is doing, then measurements. The
     old panel was mostly not measurements: a paragraph about a sound feature
     that is off by default, three rows of counts written whether or not there
     was anything to count, and a list of what the arrow keys do. The list has
     gone to the Shortcuts dialog and the counts are written only for the kinds
     of body actually present, which is what makes room for the readings that
     used to be painted in a box in the opposite corner.
     ====================================================================== */

  #overlay {
    /* The legacy sheet fixes this at 384px with 10px of padding and a
       hard-coded near-black background. The panel is a column of label/value
       rows now, which needs less width and a surface that follows the theme. */
    width: min(280px, calc(100vw - 2 * var(--space-4)));
    max-width: none;
    padding: 0;
    top: var(--space-4);
    left: var(--space-4);
    font-family: var(--font-sans);
    font-size: var(--text-xs);
    line-height: 1.5;
    color: var(--hud-text);
    text-shadow: none;
    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;
  }

  .readout-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-2) var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--hud-border);
  }

  .readout-status {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
    /* The header is a flex row and this is the part that can be long: without
       a floor of zero it refuses to shrink, and without nowrap a two-word
       state ("En marcha") breaks one character to a line inside it. */
    min-width: 0;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    letter-spacing: 0.02em;
    color: var(--hud-text-strong, var(--text-primary));
  }

  /* Running or paused, as a color as well as a word. This is the line a
     lecturer points at from across a room, where a two-letter difference
     between "Running" and "Paused" does not carry and a green or amber dot
     does. The word is still there, so the color is not the only signal. */
  .readout-status-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--accent-positive, #4ade80);
    box-shadow: 0 0 0 3px
      color-mix(in srgb, var(--accent-positive, #4ade80) 22%, transparent);
  }

  .readout-status[data-state='paused'] .readout-status-dot {
    background: var(--accent-warning, #fbbf24);
    box-shadow: 0 0 0 3px
      color-mix(in srgb, var(--accent-warning, #fbbf24) 22%, transparent);
  }

  .readout-head-actions {
    flex: none;
    display: flex;
    align-items: center;
    gap: 2px;
    pointer-events: auto;
  }

  /* The two controls that belong to the panel itself. Icon-sized, because a
     sound switch and a minimize switch are not worth a labeled row each in
     the panel whose whole problem was that it had too many rows. */
  .readout-icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    padding: 0;
    font-size: 13px;
    line-height: 1;
    color: var(--hud-text);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    cursor: pointer;
    pointer-events: auto;
    transition:
      background var(--duration) var(--ease),
      color var(--duration) var(--ease);
  }

  .readout-icon-btn:hover,
  .readout-icon-btn:focus-visible {
    background: var(--surface-2);
    color: var(--text-primary);
  }

  .readout-icon-btn[data-state='active'] {
    color: var(--accent-primary, var(--text-primary));
  }

  .readout-icon-btn[disabled] {
    opacity: 0.45;
    cursor: default;
  }

  /* The legacy sheet positions the minimize button absolutely in the corner of
     a padded panel. It is a flex child of the header now.
     
     It carries a word as well as a chevron, so unlike the sound switch it is
     sized by its label rather than pinned square - and the word goes when the
     header runs out of room, which on a phone in Spanish it does. */
  #overlay .overlay-minimize-btn {
    position: static;
    display: inline-flex;
    align-items: center;
    gap: 4px;
    width: auto;
    min-width: 26px;
    height: 26px;
    padding: 0 var(--space-2);
    white-space: nowrap;
    font-size: 12px;
    font-weight: var(--weight-semibold);
    background: transparent;
    border: 1px solid transparent;
    box-shadow: none;
    text-shadow: none;
    color: var(--hud-text);
  }

  #overlay .overlay-minimize-btn:hover {
    transform: none;
    background: var(--surface-2);
    color: var(--text-primary);
    box-shadow: none;
  }

  @media (max-width: 620px) {
    /* Chevron only. The accessible name still says what it does. */
    #overlay .overlay-toggle-label {
      display: none;
    }

    #overlay .overlay-minimize-btn {
      padding: 0;
      width: 26px;
    }
  }

  .overlay-stats {
    display: flex;
    flex-direction: column;
  }

  .overlay-stats:empty {
    display: none;
  }

  /* Label left, value right, on one baseline. Tabular figures and a fixed
     column mean the decimal point does not move as the numbers change, which
     is the difference between a readout you can watch and one you have to
     re-read. */
  .readout-metrics {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 0;
    padding: var(--space-2) var(--space-3);
    border-bottom: 1px solid var(--hud-border);
  }

  .readout-metric {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-3);
  }

  .readout-metric dt {
    color: var(--text-muted);
    white-space: nowrap;
  }

  .readout-metric dd {
    margin: 0;
    font-variant-numeric: tabular-nums;
    font-weight: var(--weight-medium);
    color: var(--hud-text-strong, var(--text-primary));
    white-space: nowrap;
  }

  .readout-metrics.is-measure .readout-metric dd {
    color: var(--accent-primary, var(--text-primary));
  }

  .readout-metrics.is-diagnostics {
    font-size: var(--text-2xs);
  }

  .readout-caveat {
    margin: var(--space-1) 0 0;
    font-size: var(--text-2xs);
    line-height: 1.4;
    color: var(--text-muted);
  }

  /* Counts as chips, and only for the kinds that are there. A scenario with
     six planets and one black hole used to report six zeros across two further
     rows; wrapping chips make the list as long as the world is varied and no
     longer. */
  .readout-counts {
    display: flex;
    flex-wrap: wrap;
    gap: 4px var(--space-2);
    margin: 0;
    padding: var(--space-2) var(--space-3);
    list-style: none;
    border-bottom: 1px solid var(--hud-border);
  }

  .readout-count {
    display: inline-flex;
    align-items: baseline;
    gap: 4px;
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .readout-count b {
    font-variant-numeric: tabular-nums;
    font-size: var(--text-xs);
    font-weight: var(--weight-semibold);
    color: var(--hud-text-strong, var(--text-primary));
  }

  .readout-empty {
    margin: 0;
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-2xs);
    color: var(--text-muted);
    border-bottom: 1px solid var(--hud-border);
  }

  /* The vector key. It is also drawn on the canvas, because a screenshot needs
     it; here it is legible at small sizes and does not compete with the
     simulation for the corner. */
  .readout-legend {
    display: flex;
    flex-direction: column;
    gap: 3px;
    margin: 0;
    padding: var(--space-2) var(--space-3);
    list-style: none;
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .readout-legend li {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

  .readout-swatch {
    flex: none;
    width: 18px;
    height: 0;
    border-top: 3px solid var(--swatch, currentColor);
  }

  .readout-swatch.is-dashed {
    border-top-width: 2px;
    border-top-style: dashed;
  }

  /* The last section in the panel has nothing under it to be separated from. */
  .overlay-stats > *:last-child {
    border-bottom: 0;
  }

  /* Minimized: the header alone, which still says whether the thing is
     running. */
  #overlay.minimized {
    width: auto;
    max-width: min(260px, calc(100vw - 2 * var(--space-4)));
    padding: 0;
    background: color-mix(in srgb, var(--hud-surface) 84%, transparent);
  }

  #overlay.minimized .readout-head {
    border-bottom: 0;
  }

  #overlay.minimized .overlay-stats {
    display: none;
  }

  /* A short window is the case where the readout and the canvas compete. */
  @media (max-height: 700px) {
    .readout-legend,
    .readout-metrics.is-diagnostics {
      display: none;
    }
  }

  /* ==========================================================================
     2. The bottom dock: transport bar and footer
     --------------------------------------------------------------------------
     Both are fixed along the bottom edge, and until now neither knew about the
     other: the transport bar centered itself in the band left of the rail and
     the footer sat in the right-hand corner, so whether they collided depended
     on how wide the footer happened to be. It grew - a theme picker, then a
     language picker - and on a wide window the scrubber ran across the
     copyright line.

     The fix is to give the transport bar a right-hand boundary it cannot cross:
     the footer's own measured width, published as --attribution-width by
     js/ui.js. Both ends are then pinned and `margin-inline: auto` centers the
     bar in whatever room is left, so the two can no longer overlap at any
     window size or in any language - and Spanish is the case that matters,
     being reliably longer than English.
     ====================================================================== */

  @media (min-width: 1025px) {
    /* Centered on the window, which is where the eye goes looking for it, and
       narrowed rather than shifted when the footer would be in the way.
       --transport-max is the widest it can be and still stop short of the
       footer; js/bottomDock.js works it out from the footer's measured width. */
    .timeline-bar {
      left: 50%;
      right: auto;
      transform: translateX(-50%);
      margin-inline: 0;
      width: min(680px, var(--transport-max, 680px));
      max-width: none;
    }

    /* Unless narrowing has gone too far. Below a usable slider length the bar
       stops trying to be centered on the window and centers in the room that is
       actually free, to the left of the footer. A scrubber slightly off-center
       is a smaller cost than one too short to scrub with. */
    body.dock-banded .timeline-bar {
      left: var(--space-4);
      right: var(--footer-reserve, 0px);
      transform: none;
      margin-inline: auto;
      width: auto;
      max-width: 680px;
    }
  }

  /* Below the rail's breakpoint the rail is a menu rather than a column, so the
     bar has the full width - and the footer, still anchored bottom-right, now
     lies under it rather than beside it. The bar moves up by the footer's own
     height to clear it.

     The lower bound matters. Below 621px the phone layout in components.css
     owns the bottom edge: the bar is pinned just above the home indicator, the
     footer wraps into its own row, and a lesson panel covers the screen. Adding
     the footer's height to the bar's offset there lifts it over the lesson's
     own Next button, which is a control the student has to be able to press. */
  @media (min-width: 621px) and (max-width: 1024px) {
    .timeline-bar {
      left: var(--space-3);
      right: var(--space-3);
      width: auto;
      max-width: 680px;
      margin-inline: auto;
      transform: none;
      bottom: calc(var(--space-3) + var(--attribution-height, 0px));
    }
  }

  @media (min-width: 1025px) {
    /* Into the corner. The inset that used to be here reserved the control
       rail's full width on every screen, which put the footer a quarter of the
       way across a wide window to avoid a rail that stops nowhere near it.
       js/bottomDock.js measures whether the rail actually reaches this corner
       and reserves the width only when it does. */
    .attribution {
      right: calc(var(--space-3) + var(--rail-footer-inset, 0px));
    }
  }

  @media (max-width: 620px) {
    .attribution {
      left: var(--space-3);
      right: var(--space-3);
      justify-content: center;
      flex-wrap: wrap;
      white-space: normal;
    }
  }

  /* ==========================================================================
     3. The control rail
     --------------------------------------------------------------------------
     Forty full-width rows in one column, twenty of them under Tools. The rail
     scrolled, which meant it worked, but a list you have to scroll to reach
     the end of is a list nobody reads the end of.

     Three changes, in order of how much height each returns:

       - short controls pair up into two columns
       - Tools is broken into named sub-groups, so the section is scanned
         rather than read
       - the analysis panels become toggle chips, which is what they are: five
         things that are either open or shut
     ====================================================================== */

  /* Two columns for the controls whose labels fit in half a rail. Marked in
     the markup rather than inferred, because which labels fit is a fact about
     the words and Spanish does not have the same ones. */
  .rail-grid {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
  }

  /* Half a rail is 88px, which two short words fit on one line at this size
     and wrap on at the default. A wrapped label in a two-column grid is worse
     than the single column it replaced, so the size comes down with it. */
  .ui-container .rail-grid .ui-button {
    justify-content: center;
    padding: 6px var(--space-1);
    font-size: var(--text-2xs);
    white-space: nowrap;
    text-align: center;
  }

  /* A sub-heading inside a section. Smaller and quieter than the section
     header, because the section header is the thing that opens and shuts and
     these are only signposts within it. */
  .rail-subhead {
    margin: var(--space-2) 0 2px;
    padding: 0 var(--space-3);
    font-size: 9px;
    font-weight: var(--weight-semibold);
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--text-muted);
    opacity: 0.75;
  }

  .rail-section > .rail-subhead:first-child {
    margin-top: 0;
  }

  /* The analysis panels. Each is open or shut, so each is a chip that shows
     which - not a button that gives no sign of having been pressed. */
  .rail-chips {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 2px;
  }

  /* Padding is 4px rather than 5.
     The rail must fit the window without scrolling - one section open at a
     time is the whole design, and e2e/presentation.spec.js asserts it. The
     Tools group has grown by a chip at a time and was eleven pixels over
     before this pass and sixteen after the evidence notebook joined it. A
     pixel off the top and bottom of each chip recovers about twenty across the
     group, which is the difference between fitting and not; the chips remain
     comfortably above the size a pointer needs. */
  .ui-container .rail-chip {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 4px;
    width: 100%;
    padding: 4px var(--space-2);
    font-size: var(--text-2xs);
    line-height: 1.25;
    text-align: center;
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    cursor: pointer;
    transition:
      background var(--duration) var(--ease),
      color var(--duration) var(--ease),
      border-color var(--duration) var(--ease);
  }

  .ui-container .rail-chip:hover,
  .ui-container .rail-chip:focus-visible {
    background: var(--surface-3);
    color: var(--text-primary);
  }

  .ui-container .rail-chip[aria-pressed='true'] {
    color: var(--text-primary);
    background: color-mix(
      in srgb,
      var(--accent-primary, #38bdf8) 20%,
      transparent
    );
    border-color: color-mix(
      in srgb,
      var(--accent-primary, #38bdf8) 55%,
      transparent
    );
  }

  /* Three of a kind, one per row. Three columns fits in 57px each, and
     "Protractor" does not fit in 57px in either language - the labels came out
     clipped, which is a worse answer than three tidy rows. */
  .ui-container .measure-tools {
    display: grid;
    grid-template-columns: 1fr;
    gap: 2px;
  }

  /* The stopwatch's own four controls do pair up: they are a glyph and one
     short word each, and they only appear while the stopwatch is out. */
  .ui-container #stopwatchControls {
    grid-template-columns: 1fr 1fr;
  }

  .ui-container .measure-tools .measure-btn {
    justify-content: flex-start;
    padding: 5px var(--space-2);
    font-size: var(--text-2xs);
    text-align: left;
  }

  .ui-container #stopwatchControls .measure-btn {
    justify-content: center;
    text-align: center;
  }

  /* The speed control is a single object - slow, reading, fast - and reads as
     one when the three parts share a surface. */
  .ui-container .speed-controls {
    display: grid;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
    gap: 2px;
    margin: var(--space-1) 0;
    padding: 2px;
    background: var(--surface-2);
    border-radius: var(--radius-sm);
  }

  .ui-container .speed-controls .ui-button {
    justify-content: center;
    padding: 4px var(--space-1);
    font-size: var(--text-2xs);
  }

  /* Sections are tighter now that there are more of them. */
  /* 1px between chips rather than 2. Ten gaps in the Tools group, so this is
     the last ten pixels the rail needed to fit without scrolling. The chips
     still read as separate: each has its own background against the rail. */
  .rail-group > [role='group'] {
    gap: 1px;
  }

  /* ==========================================================================
     The recording indicator
     --------------------------------------------------------------------------
     Top center of the viewport, over the simulation and above every panel.
     The one piece of state in the interface that costs something to be wrong
     about - a clip that is still running, or one the user believes is running
     and is not - so it is placed where nothing can cover it and it is the only
     thing on screen that moves on its own.
     ======================================================================== */
  .recording-badge {
    position: fixed;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    /* Above the scenario info card, which is also fixed to the top center and
       sits at 1000. The card is transient and the badge is the more urgent of
       the two; the badge clears the card's title and covers only the blank
       strip of padding above it. */
    z-index: 1100;
    display: flex;
    align-items: center;
    gap: var(--space-1, 6px);
    padding: 4px 10px;
    border-radius: 999px;
    background: rgba(20, 6, 10, 0.82);
    border: 1px solid rgba(255, 92, 92, 0.55);
    color: #ffe6e6;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: var(--text-2xs, 11px);
    line-height: 1.5;
    letter-spacing: 0.04em;
    pointer-events: none;
    backdrop-filter: blur(4px);
  }

  .recording-badge[hidden] {
    display: none;
  }

  /* ==========================================================================
     The update badge
     --------------------------------------------------------------------------
     Bottom left, out of the way of the top-center recording indicator and of
     every panel that opens from the right. A new version waiting is not urgent
     - the version already running is complete and will stay that way - so this
     is the quietest thing on screen that is still a control: no motion, no
     color that reads as an alert, and it never takes focus.

     It is reachable by keyboard because it is real buttons in the document, and
     it wraps rather than overflowing so a narrow phone still shows both
     actions.
     ======================================================================== */
  .update-badge {
    position: fixed;
    bottom: 12px;
    left: 12px;
    z-index: 1100;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-1, 6px);
    max-width: min(24rem, calc(100vw - 24px));
    padding: 6px 8px 6px 12px;
    border-radius: 999px;
    background: var(--surface-raised, rgba(16, 18, 26, 0.92));
    border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.18));
    color: var(--text-primary, #e8eaf2);
    font-size: var(--text-2xs, 11px);
    line-height: 1.5;
    box-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
    backdrop-filter: blur(4px);
  }

  .update-badge[hidden] {
    display: none;
  }

  .update-badge-apply,
  .update-badge-dismiss {
    font: inherit;
    color: inherit;
    border-radius: 999px;
    border: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.24));
    background: transparent;
    cursor: pointer;
  }

  .update-badge-apply {
    padding: 3px 10px;
    background: var(--accent-soft, rgba(120, 170, 255, 0.16));
  }

  .update-badge-dismiss {
    padding: 3px 8px;
    line-height: 1;
  }

  .update-badge-apply:hover,
  .update-badge-dismiss:hover {
    border-color: var(--border-strong, rgba(255, 255, 255, 0.42));
  }

  .update-badge-apply:focus-visible,
  .update-badge-dismiss:focus-visible {
    outline: 2px solid var(--focus-ring, #7aa2ff);
    outline-offset: 2px;
  }

  .recording-dot {
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: #ff4d4d;
    animation: recordingPulse 1.4s ease-in-out infinite;
  }

  /* A recording is not over when the budget is nearly spent, but the user
     should know it is about to be. */
  .recording-badge[data-nearly='true'] {
    border-color: rgba(255, 196, 92, 0.8);
    color: #ffeccd;
  }

  @keyframes recordingPulse {
    0%,
    100% {
      opacity: 1;
    }
    50% {
      opacity: 0.25;
    }
  }

  /* The blink is the indicator, so it does not go away under reduced motion -
     it slows to something that reads as a state rather than as animation. */
  @media (prefers-reduced-motion: reduce) {
    .recording-dot {
      animation-duration: 3s;
    }
  }

  /* ==========================================================================
     The A/B experiment bench
     --------------------------------------------------------------------------
     A member of the bottom-left instrument stack, so it borrows .obs-panel for
     its frame and only describes what is different: a form rather than a
     chart, and therefore rows of controls that have to stay legible when the
     stack squeezes the panel's height.
     ======================================================================== */
  .experiment-panel {
    width: 420px;
    max-width: calc(100vw - 40px);
    /* The other instruments are a chart of a fixed height; this one is a form
       whose length depends on how many bodies are on screen. Left unbounded it
       grows out of the top of the window and its own controls become
       unreachable, so it takes at most two thirds of the viewport and scrolls
       its body instead. The stack's own max-height, applied when several
       panels are open, tightens this further and never loosens it. */
    max-height: min(68vh, 640px);
  }

  .experiment-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3) var(--space-3);
    overflow-y: auto;
    min-height: 0;
  }

  .experiment-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

  .experiment-actions {
    flex-wrap: wrap;
  }

  .experiment-label {
    font-size: var(--text-2xs);
    color: var(--text-secondary);
    white-space: nowrap;
  }

  .experiment-input {
    flex: 1;
    min-width: 0;
    padding: 4px 8px;
    font: inherit;
    font-size: var(--text-2xs);
    color: var(--text-primary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
  }

  .experiment-note,
  .experiment-hint {
    margin: 0;
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .experiment-note {
    font-family: var(--font-mono, ui-monospace, monospace);
  }

  .experiment-section > summary {
    cursor: pointer;
    font-size: var(--text-2xs);
    color: var(--text-secondary);
    padding: 2px 0;
  }

  .experiment-chips {
    display: flex;
    flex-wrap: wrap;
    gap: 4px;
    margin: var(--space-1) 0;
  }

  .experiment-chip {
    padding: 3px 8px;
    font-size: var(--text-2xs);
    color: var(--text-secondary);
    background: var(--surface-2);
    border: 1px solid var(--border);
    border-radius: 999px;
    cursor: pointer;
  }

  /* Selected is a fill *and* a border weight, never color alone: the chips
     are the one place a student's selection has to be unambiguous. */
  .experiment-chip[aria-pressed='true'] {
    color: var(--text-primary);
    background: var(--accent-soft);
    border-color: var(--accent);
    font-weight: 600;
  }

  .experiment-metrics {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
    gap: 2px 10px;
  }

  .experiment-metric {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: var(--text-2xs);
    color: var(--text-secondary);
  }

  .experiment-metric.is-unavailable {
    opacity: 0.5;
  }

  .experiment-runs {
    display: grid;
    grid-template-columns: 1fr;
    gap: 4px;
  }

  .experiment-run {
    display: grid;
    grid-template-columns: auto 1fr auto;
    align-items: center;
    gap: var(--space-2);
    padding: 4px 6px;
    background: var(--surface-2);
    border-radius: var(--radius-sm);
  }

  .experiment-run-label {
    font-weight: 600;
    font-size: var(--text-2xs);
  }

  .experiment-run-state {
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }

  .experiment-warning {
    margin: 0;
    padding: 6px 8px;
    font-size: var(--text-2xs);
    border-radius: var(--radius-sm);
    border-left: 3px solid var(--border);
  }

  .experiment-warning.is-warn {
    color: var(--text-primary);
    background: rgba(226, 114, 91, 0.12);
    border-left-color: var(--danger);
  }

  .experiment-warning.is-info {
    color: var(--text-secondary);
    background: var(--surface-2);
    border-left-color: var(--info);
  }

  .experiment-diff {
    font-size: var(--text-2xs);
    padding: 6px 8px;
    background: var(--surface-2);
    border-radius: var(--radius-sm);
  }

  .experiment-diff-list {
    font-family: var(--font-mono, ui-monospace, monospace);
    color: var(--text-primary);
    word-break: break-word;
  }

  .experiment-diff-incidental {
    color: var(--text-muted);
    margin-top: 2px;
  }

  .experiment-chart {
    position: relative;
    min-height: 170px;
  }

  .experiment-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-2xs);
    font-variant-numeric: tabular-nums;
  }

  .experiment-table th,
  .experiment-table td {
    padding: 3px 6px;
    text-align: right;
    border-bottom: 1px solid var(--border-subtle);
  }

  .experiment-table th[scope='row'],
  .experiment-table thead th:first-child {
    text-align: left;
    font-weight: 500;
  }

  .experiment-saved-row {
    display: flex;
    align-items: center;
    gap: 6px;
  }

  .experiment-saved-open {
    flex: 1;
    text-align: left;
    padding: 3px 6px;
    font-size: var(--text-2xs);
    color: var(--text-secondary);
    background: none;
    border: none;
    cursor: pointer;
  }

  .experiment-saved-open:hover {
    color: var(--text-primary);
    text-decoration: underline;
  }

  .experiment-saved-delete {
    padding: 2px 6px;
    background: none;
    border: none;
    color: var(--text-muted);
    cursor: pointer;
  }

  .experiment-file {
    position: relative;
    cursor: pointer;
  }

  /* A phone gets the full width and a shorter chart: the panel is a form, and
     a form squeezed into 320px with a 170px chart above it is all chart. */
  @media (max-width: 768px) {
    .experiment-panel {
      width: auto;
      /* A phone has the transport bar and the keyboard to clear as well. */
      max-height: 60vh;
    }

    .experiment-chart {
      min-height: 130px;
    }

    .experiment-metrics {
      grid-template-columns: 1fr;
    }
  }

  /* ==========================================================================
     The evidence notebook
     --------------------------------------------------------------------------
     Another member of the bottom-left instrument stack, so it borrows
     .obs-panel for its frame like the bench does and describes only what is
     different: a scrolling list of entries, each one a table of numbers the
     reader cannot edit above three fields they can.

     That contrast is carried visually as well as structurally. The recorded
     numbers are set in the mono face on a sunken ground with no border, the
     way a readout is; the prose fields are ordinary bordered textareas. A
     student should be able to see at a glance which half of an entry is
     theirs.
     ======================================================================== */
  .notebook-panel {
    width: 460px;
    max-width: calc(100vw - 40px);
    /* Same reasoning as the bench above: the content's length depends on how
       many readings have been kept, and left unbounded the panel grows out of
       the top of the window taking its own controls with it. */
    max-height: min(72vh, 680px);
  }

  .nb-intro {
    margin: 0;
    padding: var(--space-2) var(--space-3) 0;
    font-size: var(--text-xs);
    color: var(--text-muted);
  }

  .nb-body {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-3) var(--space-3);
    overflow-y: auto;
    min-height: 0;
  }

  .nb-status {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }
  /* A refused write is the one message on this panel that must not be missed:
     the student's evidence is on screen and nowhere else. */
  .nb-status.is-bad {
    color: var(--danger, #ff8f8f);
    font-family: inherit;
    font-size: var(--text-xs);
  }

  .nb-empty {
    margin: 0;
    padding: var(--space-4) 0;
    color: var(--text-muted);
    font-size: var(--text-sm);
    text-align: center;
  }

  .nb-list {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .nb-entry {
    padding: var(--space-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    background: rgb(255 255 255 / 2%);
  }

  .nb-entry-head {
    display: grid;
    grid-template-columns: 1.6rem 1fr auto;
    gap: var(--space-2);
    align-items: start;
  }

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

  .nb-entry-titles {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
  }

  .nb-title {
    width: 100%;
    box-sizing: border-box;
    padding: 2px var(--space-1);
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--text);
    font: inherit;
    font-size: var(--text-sm);
    font-weight: 600;
  }
  /* Borderless until touched: the heading reads as a heading, and reveals
     itself as editable on hover or focus rather than looking like a form. */
  .nb-title:hover,
  .nb-title:focus {
    border-color: var(--border);
    background: rgb(0 0 0 / 18%);
  }
  .nb-title:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }

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

  .nb-entry-actions {
    display: flex;
    gap: var(--space-1);
  }

  .nb-tampered {
    margin: var(--space-2) 0 0;
    font-size: var(--text-xs);
    color: var(--danger, #ff8f8f);
  }

  /* --- The numbers, which are not editable ------------------------------- */

  .nb-table {
    width: 100%;
    margin: var(--space-2) 0 0;
    border-collapse: collapse;
    font-size: var(--text-xs);
  }
  .nb-table caption {
    padding-bottom: 2px;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    text-align: left;
  }
  .nb-table th,
  .nb-table td {
    padding: 3px var(--space-1);
    border-bottom: 1px solid var(--border);
    text-align: left;
    vertical-align: top;
  }
  .nb-table thead th {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text-muted);
    font-weight: 400;
  }
  .nb-table tbody th {
    font-weight: 400;
    color: var(--text);
  }
  .nb-value {
    font-family: var(--font-mono);
    white-space: nowrap;
  }
  .nb-qnote {
    color: var(--text-muted);
  }

  /* Measured, predicted, revealed. Distinguished by weight and letterforms as
     well as color: this panel is printed, photocopied and read by screen
     readers, and "revealed" is the one word on it that changes what a claim is
     worth. The word itself is always present in the cell. */
  .nb-kind {
    display: inline-block;
    padding: 0 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    white-space: nowrap;
  }
  .nb-kind.is-analytic {
    border-style: dashed;
    color: var(--text-muted);
  }
  .nb-kind.is-truth {
    border-color: var(--warning-line, rgb(255 176 84 / 55%));
    color: var(--warning, #ffb054);
  }

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

  /* --- The words, which are ---------------------------------------------- */

  .nb-field {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: var(--space-2) 0 0;
  }
  .nb-field label {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .nb-textarea {
    width: 100%;
    box-sizing: border-box;
    padding: 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-xs);
    line-height: 1.45;
    resize: vertical;
  }
  .nb-textarea:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 1px;
    border-color: var(--accent-line);
  }
  .nb-textarea::placeholder {
    color: var(--text-muted);
  }

  /* --- Conditions, folded away -------------------------------------------- */

  .nb-details {
    margin-top: var(--space-2);
    font-size: var(--text-xs);
  }
  .nb-details summary {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    cursor: pointer;
  }
  .nb-prov {
    display: grid;
    grid-template-columns: auto 1fr;
    gap: 2px var(--space-3);
    margin: var(--space-2) 0 0;
  }
  .nb-prov dt {
    color: var(--text-muted);
  }
  .nb-prov dd {
    margin: 0;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    word-break: break-word;
  }

  /* --- The capture form --------------------------------------------------- */

  .nb-draft {
    padding: var(--space-3);
    border: 1px solid var(--accent-line);
    border-radius: var(--radius-md);
    background: rgb(255 255 255 / 4%);
  }
  .nb-draft h4 {
    margin: 0;
    font-size: var(--text-sm);
  }
  .nb-draft-what {
    margin: 2px 0 0;
    font-size: var(--text-xs);
    color: var(--text);
  }
  .nb-draft-hint {
    margin: var(--space-1) 0 0;
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }
  .nb-draft-actions {
    display: flex;
    gap: var(--space-2);
    margin-top: var(--space-3);
  }

  @media (max-width: 768px) {
    .notebook-panel {
      width: auto;
      max-height: 60vh;
    }
    .nb-entry-head {
      grid-template-columns: 1.6rem 1fr;
    }
    /* On its own row rather than squeezed beside a title that has nowhere
       left to shrink to. */
    .nb-entry-actions {
      grid-column: 1 / -1;
      justify-content: flex-end;
    }
    .nb-prov {
      grid-template-columns: 1fr;
    }
    .nb-prov dd {
      margin-bottom: var(--space-1);
    }
  }

  /* ==========================================================================
     The RV workspace's uncertainty analysis
     --------------------------------------------------------------------------
     Scoped entirely under .rvfit-mc. The rest of the workspace panel carries
     no stylesheet of its own - it borrows .obs-panel and the generic controls -
     and giving its existing canvases a width here would silently resize
     instruments this change has nothing to do with.
     ======================================================================== */
  .rvfit-mc {
    margin-top: var(--space-3);
    padding-top: var(--space-2);
    border-top: 1px solid var(--border);
    font-size: var(--text-xs);
  }
  .rvfit-mc > summary {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
    cursor: pointer;
  }
  .rvfit-mc canvas {
    display: block;
    width: 100%;
    margin-top: var(--space-3);
  }

  .rvfit-mc-controls {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-end;
    gap: var(--space-2);
    margin-top: var(--space-2);
  }
  .rvfit-mc-field {
    display: flex;
    flex-direction: column;
    gap: 2px;
    min-width: 0;
    flex: 1 1 6rem;
  }
  .rvfit-mc-field > span {
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .rvfit-mc-field input {
    width: 100%;
    min-width: 0;
    box-sizing: border-box;
    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-xs);
  }
  .rvfit-mc-field input:focus-visible {
    outline: var(--focus-ring);
    outline-offset: 1px;
  }
  .rvfit-mc-seedhint,
  .rvfit-mc-provenance,
  .rvfit-mc-rest {
    margin: var(--space-1) 0 0;
    font-size: var(--text-2xs);
    color: var(--text-muted);
  }
  .rvfit-mc-provenance {
    font-family: var(--font-mono);
  }
  .rvfit-mc-status {
    margin: var(--space-2) 0 0;
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    color: var(--text-muted);
    min-height: 1em;
  }

  /* The single interval, when there is one to give. Set larger than the prose
     around it because it is the answer a reader came for. */
  .rvfit-mc-interval {
    margin: var(--space-2) 0 0;
    font-size: var(--text-sm);
    color: var(--text);
  }
  /* And the three ways there is not one. All warnings rather than errors: none
     of them is a malfunction, each is something the data is telling you. */
  .rvfit-mc-multimodal,
  .rvfit-mc-warn,
  .rvfit-mc-refused {
    margin: var(--space-2) 0 0;
    padding: var(--space-2);
    border-left: 2px solid var(--warning, #f2a65a);
    background: rgb(255 255 255 / 3%);
    color: var(--text);
  }
  .rvfit-mc-refused {
    border-left-color: var(--text-muted);
  }

  .rvfit-mc-fam-head {
    margin: var(--space-3) 0 var(--space-1);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .rvfit-mc-table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-2xs);
  }
  .rvfit-mc-table th,
  .rvfit-mc-table td {
    padding: 2px var(--space-1);
    border-bottom: 1px solid var(--border);
    text-align: left;
    font-weight: 400;
    white-space: nowrap;
  }
  .rvfit-mc-table thead th {
    color: var(--text-muted);
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }
  .rvfit-mc-table tbody td {
    font-family: var(--font-mono);
  }
  /* Ties a table row to its clump in the histogram above. Never the only cue:
     the row also carries the share and the interval in words and numbers, so a
     reader who cannot see the color loses nothing. */
  .rvfit-mc-swatch {
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 4px;
    border-radius: 2px;
    vertical-align: baseline;
  }

  .rvfit-mc-assume {
    margin-top: var(--space-3);
  }
  .rvfit-mc-assume-head {
    margin: 0 0 var(--space-1);
    font-family: var(--font-mono);
    font-size: var(--text-2xs);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--text-muted);
  }
  .rvfit-mc-assume ul {
    margin: 0;
    padding-left: var(--space-4);
    color: var(--text-muted);
  }
  .rvfit-mc-assume li {
    margin-bottom: 2px;
  }

  .rvfit-mc-guidance {
    margin-top: var(--space-3);
  }
  .rvfit-mc-guidance h4 {
    margin: 0 0 var(--space-1);
    font-size: var(--text-sm);
    color: var(--text);
  }
  .rvfit-mc-guidance p {
    margin: 0 0 var(--space-2);
    max-width: 62ch;
    color: var(--text-muted);
  }

  @media (max-width: 768px) {
    .rvfit-mc-field {
      flex-basis: calc(50% - var(--space-2));
    }
    .rvfit-mc-table {
      display: block;
      overflow-x: auto;
    }
  }

  /* The button says what it will do next, and looks like what it is doing. */
  .ui-container #recordBtn[aria-pressed='true'] {
    background: rgba(120, 26, 34, 0.75);
    border-color: rgba(255, 92, 92, 0.6);
    color: #ffe6e6;
  }
}

/* ==========================================================================
   The A/B bench during an investigation
   --------------------------------------------------------------------------
   The bench's home is the bottom left, which is where the lesson panel lives:
   the butterfly-effect investigation asks students to open the bench on step 3,
   and it landed on top of the step they were reading. It takes the strip beside
   the lesson instead - the same one the light curve moves to. The stack in
   js/observationLayout.js only ever writes `bottom`, so which column a panel
   stacks in is whatever `left` says here.

   That strip is also where a lesson step's own tool or plot panel docks, so
   when both are up they share it: the tool keeps the top, the bench the bottom,
   and `--bench-reserve` is the height the tool has to give back. js/experiments/
   panel.js puts `bench-open` on the body so this can be asked at all.
   ======================================================================== */
@layer overrides {
  body.investigation-open .experiment-panel {
    left: calc(min(396px, 40vw) + var(--space-3) * 2);
    /* Clear of the control rail on the right, which is content-sized and
       measures 208px; 240 leaves it a margin. The bench keeps its own 420px
       where the strip is wide enough and narrows rather than sliding under the
       rail where it is not. */
    max-width: calc(100vw - min(396px, 40vw) - var(--space-3) * 2 - 240px);
  }

  /* The instrument stack moves with the bench, and for the same reason.
     A scenario-specific panel appears on its own when its scenario loads -
     that is the point of it - and the two binary labs and the two assist
     scenarios all have a lesson written around them. The lesson panel is 396px
     at the left edge with a z-index of 20; the stack is 420px at the left edge
     with a z-index of 160. So the instrument landed squarely on top of the
     lesson and swallowed its clicks: walking binary-star-planets failed on a
     multiple-choice step because the Binary Planet Run readout was over the
     options, and the reader saw the lesson text disappear behind a panel they
     had not asked for. Same strip, same offset, same reasoning as the bench. */
  body.investigation-open .obs-panel {
    left: calc(min(396px, 40vw) + var(--space-3) * 2);
    max-width: calc(100vw - min(396px, 40vw) - var(--space-3) * 2 - 240px);
  }

  /* --- Sharing the strip with a step's tool or plot ----------------------- */
  body.investigation-open.bench-open {
    --bench-reserve: calc(min(46vh, 420px) + var(--space-3));
  }

  body.investigation-open.bench-open .experiment-panel {
    max-height: min(46vh, 420px);
  }

  body.investigation-open.bench-open .inv-plot-panel {
    /* 120px is the stack's own base offset, which is where the bench starts. */
    max-height: calc(100vh - var(--space-3) - 120px - var(--bench-reserve));
  }

  /* A step carrying an instrument and a plot at once already splits the column
     0.62/0.38; with the bench open it is the same split of a shorter column. */
  body.investigation-split.bench-open #investigationTool {
    max-height: calc(
      (100vh - var(--space-3) - 120px - var(--bench-reserve)) * 0.62
    );
  }
  body.investigation-split.bench-open #investigationPlot {
    top: calc(
      (100vh - var(--space-3) - 120px - var(--bench-reserve)) * 0.62 +
        var(--space-3) * 2
    );
    max-height: calc(
      (100vh - var(--space-3) - 120px - var(--bench-reserve)) * 0.38 -
        var(--space-3)
    );
  }

  @media (max-width: 1100px) {
    body.investigation-open .experiment-panel,
    body.investigation-open .obs-panel {
      left: calc(min(396px, 40vw) + var(--space-2) * 2);
      max-width: calc(100vw - min(396px, 40vw) - var(--space-2) * 2 - 240px);
    }
  }

  /* --- Below 900px the lesson is a sheet across the bottom ---------------- */
  /* No strip beside it, so the bench returns to the left edge and sits above
     the sheet. `bottom` needs !important because the stack writes it inline and
     cannot know the sheet is there.
     The transport bar's height is measured rather than assumed, for the reason
     given on .investigation-panel: it is 48px on one row and 62px on two, and
     this block used to carry its own copy of the 76px guess. When the lesson
     sheet started clearing the bar by its measured height and the bench did
     not, the sheet's top rose ten pixels into the bench and the two overlapped
     at 390px wide. One source of truth, or they drift apart again. */
  @media (max-width: 900px) {
    body.investigation-open {
      --lesson-sheet: min(52vh, 420px);
    }
    /* A step with a tool panel shortens the sheet; the bench follows it up. */
    body.investigation-open.investigation-aux,
    body.investigation-open.investigation-split {
      --lesson-sheet: min(38vh, 300px);
    }

    body.investigation-open .experiment-panel {
      left: var(--space-2);
      right: var(--space-2);
      width: auto;
      max-width: none;
      bottom: calc(
        var(--transport-reserve, 76px) + var(--lesson-sheet) + var(--space-2)
      ) !important;
      max-height: calc(
        100vh - var(--transport-reserve, 76px) - var(--lesson-sheet) -
          var(--space-2) * 3
      ) !important;
    }

    /* No strip to move into on a phone, so the instrument goes back to the
       full width and sits above the lesson sheet rather than beside it. */
    body.investigation-open .obs-panel {
      left: var(--space-2);
      right: var(--space-2);
      width: auto;
      max-width: none;
      bottom: calc(
        var(--transport-reserve, 76px) + var(--lesson-sheet) + var(--space-2)
      );
      max-height: calc(
        100vh - var(--transport-reserve, 76px) - var(--lesson-sheet) -
          var(--space-2) * 3
      );
    }

    /* Three panels do not fit one above another on a phone, so on a tool step
       the bench takes a third of the space above the sheet and the tool keeps
       the rest rather than the two overlapping. */
    body.investigation-open.bench-open .experiment-panel {
      max-height: min(30vh, 260px) !important;
    }
    body.investigation-open.bench-open .inv-plot-panel {
      max-height: calc(
        100vh - var(--transport-reserve, 76px) - var(--lesson-sheet) -
          min(30vh, 260px) - var(--space-2) * 4
      );
    }
    body.investigation-split.bench-open #investigationTool,
    body.investigation-split.bench-open #investigationPlot {
      max-height: none;
    }
  }
}

/* ==========================================================================
   Adding an object: the picker, and the armed state
   --------------------------------------------------------------------------
   The button used to cycle through eight types one click at a time and
   placement was always live, so a near miss on a small body added a star
   instead of opening an inspector. It now opens this picker, and only a type
   chosen here arms the canvas.
   ====================================================================== */
@layer components {
  .object-add {
    position: relative;
  }

  /* Armed. The rail is dark and the button is already the primary one, so the
     signal is a ring and a brighter face rather than another color. */
  #objectTypeBtn.is-armed {
    background: var(--accent);
    color: var(--accent-contrast);
    border-color: var(--accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent);
  }

  /* And the canvas says so too, for the whole time it is armed: a cursor is
     the one piece of feedback that is where the reader is looking. */
  body.is-adding #simulationCanvas {
    cursor: crosshair;
  }

  /* The armed button keeps its glyph legible against the accent fill. The
     glyphs carry their own class hues, which are chosen for a dark panel. */
  #objectTypeBtn {
    display: inline-flex;
    align-items: center;
    gap: var(--space-2);
  }

  #objectTypeBtn.is-armed .object-glyph {
    filter: brightness(0.35) saturate(1.4);
  }

  /* -----------------------------------------------------------------------
     Placement status.

     A cursor is not feedback on a touch screen, and on a narrow window the
     rail - the only other place that said anything - dismisses itself as soon
     as a type is chosen. So arming was invisible from the canvas, and the next
     tap created a black hole with no warning.

     Above the transport bar rather than over it, and pointer-events: none, so
     it can neither cover a control nor swallow the click it is telling you to
     make. Centered on the canvas, which is where the reader is looking.
     -------------------------------------------------------------------- */
  .placement-status {
    position: fixed;
    left: 50%;
    /* A starting value only. js/ui.js measures the transport bar and sets
       `bottom` from it, because the bar's own distance from the bottom edge is
       not one number - it changes at the tablet breakpoint and again in an
       embed, and a guessed constant overlapped it by twenty pixels at
       768x1024. e2e/uiCoherence.spec.js checks the two never overlap. */
    bottom: calc(var(--space-4) + 56px + var(--space-2));
    transform: translateX(-50%);
    z-index: var(--z-timeline);
    pointer-events: none;

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

    background: color-mix(in srgb, var(--surface-1) 92%, transparent);
    border: 1px solid var(--accent-line);
    border-radius: var(--radius-full);
    box-shadow: var(--shadow);
    color: var(--text-primary);
    font-size: var(--text-2xs);
    line-height: var(--leading-tight);
  }

  .placement-status-glyph {
    display: flex;
    flex: none;
  }

  .placement-status-text {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
  }

  .placement-status-type {
    font-weight: var(--weight-semibold);
  }

  .placement-status-hint {
    color: var(--text-secondary);
  }

  .placement-status-sep {
    padding: 0 0.15em;
    color: var(--text-muted);
  }

  /* The picker is a top-layer popover, and it is a child of <body> rather than
     of the rail. It has to be.
     -----------------------------------------------------------------------
     It used to be an absolutely positioned descendant of `.object-add`, which
     is inside `.ui-container`. That element scrolls its own overflow - it has
     to, or the rail runs off the bottom of a short window - and an overflow
     container clips its absolutely positioned descendants. So the picker was
     cut off whenever the rail was scrolled, whenever the window was short, and
     whenever the button sat near the bottom of the rail. The fix is not to stop
     the rail scrolling; it is for the popover not to live inside it.

     js/ui.js moves this element to <body> at start-up and positions it from the
     trigger's getBoundingClientRect() on every open, on resize, on orientation
     change and on any scroll that could have moved the button. Everything below
     is the part CSS can own: the box, and the internal scrolling that a
     viewport-bounded max-height needs. `left`, `top` and `max-height` are set
     by that code and deliberately not here. */
  .object-picker {
    position: fixed;
    left: 0;
    top: 0;
    right: auto;
    bottom: auto;
    width: max-content;
    /* A plain minimum now that it no longer has a parent to measure against:
       `min-width: 100%` used to resolve to the rail's width, which on a tablet
       is the whole window. */
    min-width: 200px;
    max-width: min(260px, calc(100vw - var(--space-6)));
    /* Taller than the room beside the trigger and it scrolls inside itself
       rather than off the screen. */
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Above the rail it belongs to, below a modal. */
    z-index: var(--z-inspector);
    padding: var(--space-2);
    background: var(--surface-1);
    border: 1px solid var(--border);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-lg);
  }

  /* One column. Two columns of half a rail left every name ellipsised to
     "Add …", which is no more use than the cycling button it replaced. */
  .object-picker-grid {
    display: grid;
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .object-picker-item {
    display: flex;
    align-items: center;
    /* Explicit, because a <button> centers its content and these are rows in a
       list: eight centered rows gave the eight names eight different left edges
       and nothing to scan down. */
    justify-content: flex-start;
    gap: var(--space-2);
    width: 100%;
    min-width: 0;
    padding: var(--space-2);
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    color: var(--text-primary);
    font: inherit;
    font-size: var(--text-2xs);
    text-align: left;
    cursor: pointer;
  }

  .object-picker-item:hover,
  .object-picker-item:focus-visible {
    background: var(--surface-3);
    border-color: var(--border);
  }

  .object-picker-item[aria-checked='true'] {
    border-color: var(--accent);
  }

  /* One size, one column. The emoji this replaced were drawn by the operating
     system at eight different advance widths, so the eight names beside them
     started at eight different x positions. */
  .object-glyph {
    flex: none;
    width: 18px;
    height: 18px;
    display: block;
  }

  /* The name is the part that can overrun: Spanish "Añadir enanas blancas" is
     half again the English. It ellipsises rather than widening the popover. */
  .object-picker-name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .object-picker-stop {
    width: 100%;
    margin-top: var(--space-2);
    padding: var(--space-2);
    background: transparent;
    border: 1px solid var(--border-subtle);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font: inherit;
    font-size: var(--text-2xs);
    cursor: pointer;
  }

  .object-picker-stop:hover,
  .object-picker-stop:focus-visible {
    background: var(--surface-3);
    color: var(--text-primary);
  }
}

/* The sound panel.
   -------------------------------------------------------------------------
   A fixed-position popover moved to <body> by js/ui.js, for the same reason
   the object picker is: the readout scrolls its own overflow and would clip
   an absolutely positioned descendant. `left`, `top` and `max-height` are set
   from the trigger's rectangle by that code and deliberately not here. */
.sound-panel {
  position: fixed;
  left: 0;
  top: 0;
  width: max-content;
  min-width: 250px;
  max-width: min(320px, calc(100vw - var(--space-6)));
  overflow-y: auto;
  overscroll-behavior: contain;
  z-index: var(--z-inspector);
  padding: var(--space-3);
  background: var(--surface-1);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-lg);
  font-size: var(--text-sm);
  color: var(--text-primary);
}

.sound-panel-title {
  margin: 0 0 var(--space-1);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}

.sound-panel-state {
  margin: 0 0 var(--space-2);
  color: var(--text-secondary);
}

.sound-panel-switch {
  width: 100%;
  padding: var(--space-2);
  margin-bottom: var(--space-2);
  font: inherit;
  font-weight: var(--weight-semibold);
  color: var(--text-primary);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.sound-panel-switch[aria-pressed='true'] {
  border-color: var(--accent);
}

.sound-panel-note {
  margin: 0 0 var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-xs);
  line-height: 1.45;
}

.sound-panel-rows {
  margin: 0 0 var(--space-2);
}

.sound-panel-row {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  padding: 2px 0;
}

.sound-panel-row dt {
  color: var(--text-muted);
  flex: 0 0 auto;
}

.sound-panel-row dd {
  margin: 0;
  text-align: right;
  min-width: 0;
}

.sound-panel-volume {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.sound-panel-volume input {
  flex: 1 1 auto;
  min-width: 0;
}

.sound-panel-volume output {
  flex: 0 0 auto;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
}

.sound-panel-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}

.sound-panel-btn {
  flex: 1 1 0;
  /* A finger target, not a mouse one: this panel is reachable on a phone. */
  min-height: 36px;
  padding: var(--space-1) var(--space-2);
  font: inherit;
  color: var(--text-primary);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.sound-panel-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.sound-panel-link {
  margin: 0;
  font-size: var(--text-xs);
}

/* The speaker's own glyph carries the state, so the button needs a way to show
   "permitted but silent" as distinct from "playing". Color is never the only
   difference: the glyph itself changes, and so does the accessible name. */
.sonification-toggle[data-state='playing'] {
  color: var(--accent);
}

.sonification-toggle[data-state='blocked'] {
  color: var(--warn, #f2a65a);
}
