/* This file is part of Chaos Accelerator, licensed under the Common Public
   Attribution License, Version 1.0 (CPAL-1.0) - see LICENSE in the project
   root, or https://chaosaccelerator.com/license for a hosted copy. */

:root {
  --bg: #14161c;
  --bg-panel: #1b1e27;
  --bg-input: #0f1116;
  --border: #2c3040;
  --text: #e6e8ef;
  --text-dim: #9399ad;
  --accent: #5b8cff;
  --accent-hover: #75a0ff;
  --danger: #ff5f56;
  --panel-width: 320px;
  /* The floating panel's gap from the viewport edges, and its own
     translucent fill - a touch lighter than the docked --bg-panel so it
     reads as sitting ON the fractal rather than being cut out of it. */
  --panel-inset: 14px;
  --bg-panel-float: rgba(27, 30, 39, 0.86);
  /* What the menu column keeps clear for the back link and the four menu
     buttons, so an open card can never push the other menus off the bottom
     of the screen. Named because two rules have to agree on it - the card's
     own max-height, and the portrait override of it. The multiplier is one
     per collapsed menu button, so it grows with the number of menus. */
  --menu-reserved: calc((var(--settings-btn-size) + 8px) * 4 + 20px);
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}

#app {
  height: 100vh;
  display: flex;
  flex-direction: column;
}




.status-ok {
  font-size: 12px;
  padding: 4px 10px;
  border-radius: 999px;
  white-space: nowrap;
}
.status-ok { background: #21273a; color: #9db0e8; }
.status-error { background: #3a1418; color: #ff9a9a; }

/* Errors only. "Editing", "Playing", "Ready" were the whole reason the old
   status chip needed permanent space, and none of them told anyone
   anything they couldn't see; a compile failure or an unreadable scene
   very much does, so the element stays and only shows for those. */
#grid-status:not(.status-error) {
  display: none;
}
#grid-status.status-error {
  width: 100%;
}

#grid-main {
  position: relative;
  flex: 1;
  display: flex;
  min-height: 0;
}

/* ---- The menu column ----
   Everything on this page that is not the fractal, in one upper-left
   stack: the way back, and four menus that are a square icon button
   until opened, at which point that button becomes the card and pushes
   whatever is below it down. Absolutely positioned rather than flexed, so
   #grid-canvas-area takes the full area and needs no width accounting.

   pointer-events: none, with it turned back on per item. The column is
   --panel-width wide whether or not anything is open, and that width sits
   over the fractal: without this, a drag or a wheel-zoom aimed at the
   picture beside a collapsed button would land on empty column instead.
   Two elements rather than one because the stack scrolls and the resize
   handle beside it must not. */
#grid-menu-column {
  position: absolute;
  z-index: 3;
  top: var(--panel-inset);
  left: var(--panel-inset);
  width: var(--panel-width);
  max-width: calc(100% - var(--panel-inset) * 2);
  pointer-events: none;
}
#grid-menu-stack {
  display: flex;
  flex-direction: column;
  /* Every card is --panel-width; the collapsed buttons and the back link
     are their own size and must not stretch to match. */
  align-items: flex-start;
  gap: 8px;
  /* Viewport units, not a percentage: the column's own height is auto (it
     shrinks to this stack), and a percentage max-height against an
     auto-height parent resolves to none - so the stack would never
     scroll, it would just run off the bottom of the screen. */
  max-height: calc(100vh - var(--panel-inset) * 2);
  overflow-y: auto;
  /* overflow-y: auto would otherwise also compute overflow-x to auto (per
     the CSS Overflow spec, a non-visible value on one axis pulls the
     visible other axis to auto) - a long unbroken hover readout (see
     #grid-view #hover-readout below) was enough to grow a horizontal
     scrollbar this never needs. */
  overflow-x: hidden;
}
#grid-menu-stack > * {
  pointer-events: auto;
  flex-shrink: 0;
}

/* Both the column itself and an open card's own body (see .menu-card-body
   below) can grow a vertical scrollbar - most visibly on Analysis, whose
   body is the tallest once more than a couple of sections are switched on.
   Unstyled, that scrollbar is whatever light/system default the browser
   ships, sitting on top of this page's own dark floating-card look. Firefox
   reads the two scrollbar-* properties; everything else reads the
   ::-webkit-scrollbar rules below them - painting the same thumb/track
   twice, not painting it differently, so it never needs a browser check. */
#grid-menu-stack,
.menu-card-body {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
#grid-menu-stack::-webkit-scrollbar,
.menu-card-body::-webkit-scrollbar {
  width: 10px;
}
#grid-menu-stack::-webkit-scrollbar-track,
.menu-card-body::-webkit-scrollbar-track {
  background: transparent;
}
#grid-menu-stack::-webkit-scrollbar-thumb,
.menu-card-body::-webkit-scrollbar-thumb {
  background-color: var(--border);
  border-radius: 6px;
  border: 2px solid transparent;
  background-clip: padding-box;
}
#grid-menu-stack::-webkit-scrollbar-thumb:hover,
.menu-card-body::-webkit-scrollbar-thumb:hover {
  background-color: var(--accent);
  background-clip: padding-box;
}

/* The shared look of every control in the column: a translucent card
   sitting ON the fractal rather than cut out of it. Two shadows, not one:
   the wide 10/30 one alone is almost all bottom - at that offset, blur
   alone leaves the top and left edges too faint to read against the
   fractal, so the shadow looks like it's been cut off there rather than
   just fading out. The tight 2/6 shadow is close enough to the edge to
   stay visible on every side and reads as a contact shadow hugging the
   card, with the wide one still doing the actual "floating" depth below. */
.menu-back,
.menu-toggle,
.menu-card {
  background: var(--bg-panel-float);
  backdrop-filter: blur(12px);
  -webkit-backdrop-filter: blur(12px);
  border: 1px solid var(--border);
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.5), 0 10px 30px rgba(0, 0, 0, 0.45);
}

/* Wider than the three below it because it carries its own label - the
   only one of the four whose destination isn't obvious from an icon. */
.menu-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  height: var(--settings-btn-size);
  padding: 0 14px;
  border-radius: 10px;
  font-size: 13px;
  line-height: 1;
  white-space: nowrap;
  text-decoration: none;
  color: var(--text);
}
.menu-back:hover {
  background: var(--bg-input);
  border-color: var(--accent);
}

.menu-item {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* The collapsed state of a menu. Square, and the same size as the others,
   so the four form one column whatever is open. */
.menu-toggle {
  width: var(--settings-btn-size);
  height: var(--settings-btn-size);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  font-size: 18px;
  line-height: 1;
  color: var(--text);
  border-radius: 10px;
  cursor: pointer;
}
.menu-toggle:hover {
  background: var(--bg-input);
  border-color: var(--accent);
}
/* Exactly one of the button and the card is in the layout at a time -
   never both, and never a hidden-but-present one taking up space in a
   column whose whole job is to push what's below it down. */
.menu-item.is-open > .menu-toggle {
  display: none;
}

.menu-card {
  width: var(--panel-width);
  max-width: 100%;
  /* Leave room for the back link and the four menu buttons, so opening a
     tall card never pushes the other menus off the bottom of the screen -
     the card scrolls inside itself instead. The column scrolls too (see
     #grid-menu-stack), which is what catches the case of several open at
     once; this is what stops ONE of them swallowing the column. */
  max-height: calc(100vh - var(--panel-inset) * 2 - var(--menu-reserved));
  border-radius: 12px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  /* The card is what the button turned into, so it grows out of the
     button's own corner. Not a transition: the card is display:none when
     shut (it has to leave the layout), and there is nothing to transition
     from. */
  transform-origin: top left;
  animation: menu-card-open 0.14s ease-out;
}
.menu-card[hidden] {
  display: none;
}
@keyframes menu-card-open {
  from { opacity: 0; transform: scale(0.96); }
  to { opacity: 1; transform: scale(1); }
}

/* The whole header line is the collapse control (see makeMenu in
   fractal-grid.js), not just the caret at its end - cursor and a hover fill
   are what tell the pointer that clicking anywhere on this row, not just
   the button, shuts the card. */
.menu-card-header {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 10px 10px 14px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
  cursor: pointer;
}
.menu-card-header:hover {
  background: var(--bg-input);
}
.menu-card-header h2 {
  font-size: 14px;
  margin: 0;
  flex: 1;
  min-width: 0;
}
/* The same glyph as the collapsed button, so an open card still reads as
   that button rather than as an unrelated panel. */
.menu-card-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--text-dim);
  font-size: 15px;
  line-height: 1;
}
/* Sits after the name, which is where the eye goes to put a menu away. */
.menu-collapse {
  flex-shrink: 0;
  font-size: 16px;
}

.menu-card-body {
  padding: 14px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  overflow-y: auto;
  /* Same reason as #grid-menu-stack's: overflow-y alone would pull
     overflow-x to auto and let a long unbroken readout grow a horizontal
     scrollbar this never needs. */
  overflow-x: hidden;
}
/* Inspect's three groups want more air between them than a settings card's
   fields do - the extra 8px on top of the gap above is what the old
   floating panel's own 20px worked out to. */
.menu-card-body > .panel-section + .panel-section {
  margin-top: 8px;
}

/* Pinned to the column's own right edge, outside the scrolling stack, so
   it stays put while the stack scrolls under it. The drag handler reads
   the stack's rect and writes --panel-width, which every open card is
   sized from. Nothing to resize with every menu shut, so it isn't there. */
#panel-resizer {
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 6px;
  cursor: col-resize;
  background: transparent;
  border-radius: 0 12px 12px 0;
  pointer-events: auto;
  display: none;
}
#grid-menu-column.has-open-menu > #panel-resizer {
  display: block;
}
#panel-resizer:hover,
#panel-resizer.dragging {
  background: var(--accent);
}
/* Widen the hit area past the visible 6px line without widening the line
   itself - a bare 6px target is uncomfortably thin to grab. */
#panel-resizer::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -3px;
  right: -3px;
}

button {
  font-family: inherit;
  font-size: 13px;
  padding: 8px 14px;
  border-radius: 6px;
  border: 1px solid transparent;
  background: var(--accent);
  color: white;
  cursor: pointer;
}
button:hover { background: var(--accent-hover); }
#grid-view button:disabled {
  cursor: default;
  opacity: 0.5;
  background: var(--border);
  color: var(--text-dim);
}
#grid-view button.secondary {
  background: transparent;
  border-color: var(--border);
  color: var(--text);
  padding: 6px 10px;
}
button.secondary:hover { background: var(--bg-input); }
/* btnInspectLine/btnInspectGrid only: gray/outlined at rest, same as every
   other idle button on the first page - matches .tool-btn's own default
   over there. Its own rule rather than .secondary, which also shrinks
   padding and would resize the button the moment it arms; this keeps the
   idle and armed (below) states the exact same size. */
#btn-inspect-line,
#btn-inspect-grid {
  background: transparent;
  border-color: var(--border);
  color: var(--text);
}
#btn-inspect-line:hover,
#btn-inspect-grid:hover {
  background: var(--bg-input);
}
/* Armed (waiting for a grid drag): blue, same as .tool-btn.active over on
   the first page - so blue reads as "engaged" consistently everywhere in
   the app, not just here. See armInspect's own comment in fractal-grid.js
   for the (now corrected) history of why this used to run the other way.
   Selectors combine the ID with the class (not just button.inspect-armed)
   so this outranks the idle rule above on specificity alone - two ID
   selectors otherwise beat a single class every time, regardless of which
   rule comes later in the file. */
#btn-inspect-line.inspect-armed,
#btn-inspect-grid.inspect-armed {
  background: var(--accent);
  border-color: var(--accent);
  color: white;
}
#btn-inspect-line.inspect-armed:hover,
#btn-inspect-grid.inspect-armed:hover {
  background: var(--accent-hover);
}

#grid-view .field-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.tool-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
}

.section-label {
  font-size: 12px;
  color: var(--text-dim);
  margin: 12px 0 8px 0;
}

#grid-view .field {
  display: block;
  font-size: 13px;
  margin-bottom: 14px;
}
#grid-view .field span {
  display: block;
  margin-bottom: 6px;
  color: var(--text-dim);
}
/* ---- Scoped to #grid-view ----
   These eight rules style the same class/element differently on the two
   views, and both stylesheets load into one document now (see chaos.html).
   The editor's copies stay global; the grid's are scoped here so loading
   order stops deciding which view wins. Every other rule in this file is
   already unique to the grid (its ids are namespaced) or byte-identical to
   physics.css, so nothing else needs it. */
#grid-view .field select {
  width: 100%;
  font: inherit;
  padding: 5px 7px;
  color: var(--text);
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: 5px;
}
/* Sits under a control to explain what it's currently doing, in the same
   dimmer voice as the label above it but a size smaller. Two classes
   deep specifically to outrank `.field span` above, which would otherwise
   win the margin (a type selector still beats nothing on the tiebreak). */
.field .field-hint {
  display: block;
  margin: 6px 0 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-dim);
}

input[type="range"] {
  width: 100%;
  accent-color: var(--accent);
}

/* Two native range inputs stacked on the same track (the standard
   dependency-free way to build a dual-handle slider) - see
   resolutionMinSlider/resolutionMaxSlider in fractal-grid.js. Both inputs
   span the full width and are pointer-events:none themselves so a click
   between the handles doesn't get grabbed by whichever happens to be on
   top; only their thumbs re-enable pointer-events, so each is independently
   draggable. .dual-range-track/-fill are purely visual, drawn underneath. */
.dual-range {
  position: relative;
  height: 24px;
  display: flex;
  align-items: center;
}
.dual-range-track {
  position: absolute;
  left: 0;
  right: 0;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.dual-range-fill {
  position: absolute;
  height: 4px;
  border-radius: 2px;
  background: var(--accent);
}
.dual-range input[type="range"] {
  position: absolute;
  left: 0;
  right: 0;
  width: 100%;
  margin: 0;
  background: none;
  pointer-events: none;
  -webkit-appearance: none;
  appearance: none;
}
.dual-range input[type="range"]::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  background: transparent;
  height: 4px;
}
.dual-range input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  pointer-events: auto;
  width: 14px;
  height: 14px;
  margin-top: -5px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg-panel);
  cursor: pointer;
}
.dual-range input[type="range"]::-moz-range-track {
  background: transparent;
  height: 4px;
}
.dual-range input[type="range"]::-moz-range-thumb {
  pointer-events: auto;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  border: 2px solid var(--bg-panel);
  cursor: pointer;
}

/* Apple-style toggle switch - under the hood this is still the exact same
   <input type="checkbox" id="color-zoom-checkbox">, just visually hidden
   (see .toggle-switch input below) with .toggle-slider drawn on top of it
   and styled off its :checked state, so all of fractal-grid.js's existing
   .checked/change-event logic needs no changes at all. Mirrors physics.
   html's Sticky Edges toggle exactly (minus :disabled - this checkbox is
   never disabled). */
#grid-view .toggle-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  font-size: 13px;
  margin-bottom: 14px;
  cursor: pointer;
}
.toggle-switch {
  position: relative;
  display: inline-block;
  width: 44px;
  height: 24px;
  flex-shrink: 0;
}
.toggle-switch input {
  position: absolute;
  opacity: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.toggle-slider {
  position: absolute;
  inset: 0;
  background: var(--border);
  border-radius: 999px;
  transition: background 0.15s ease;
  pointer-events: none;
}
.toggle-slider::before {
  content: "";
  position: absolute;
  width: 18px;
  height: 18px;
  left: 3px;
  top: 3px;
  background: white;
  border-radius: 50%;
  transition: transform 0.15s ease;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.toggle-switch input:checked + .toggle-slider {
  background: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
  transform: translateX(20px);
}

#grid-view .readout {
  font-size: 12px;
  color: var(--text-dim);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* #hover-readout is the one .readout in this panel that's actually long
   (coordinates plus a step count) rather than a short fixed label like the
   zoom readout - nowrap on it was what needed a horizontal scrollbar
   before .menu-card-body gained overflow-x: hidden above. Higher specificity
   than #grid-view .readout (two ID selectors beat one ID + one class) so
   this wins regardless of source order. */
#grid-view #hover-readout {
  white-space: normal;
  overflow-wrap: break-word;
}

#hover-preview {
  position: relative;
  width: 100%;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
  /* Same reasoning as #grid-canvas-area: a plain color here, not transparent -
     the mini canvas is drawn once per replay frame and the background
     color between shapes is what actually carries the Output value. */
  background: #0c0d11;
}

#hover-canvas {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 280 / 200;
}

#hover-empty-state {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  padding: 12px;
  font-size: 12px;
  color: var(--text-dim);
  background: #0c0d11;
  pointer-events: none;
}
#hover-empty-state[hidden] {
  display: none;
}

#grid-canvas-area {
  /* How far the toast has to sit above the bottom edge - clear of the
     render-progress ring (lower left) and the watermark (lower right),
     which are the whole of what lives down there now that the timeline
     transport has moved into the Map Evolution card. See .inspect-toast. */
  --toast-bottom: calc(var(--panel-inset) + var(--settings-btn-size) * 0.75 + 16px);
  flex: 1;
  position: relative;
  min-width: 0;
  background: #0c0d11;
  /* Without this, an .inspect-marker for a point currently panned outside
     the visible grid (negative left/top relative to this box) would still
     paint on top of everything. (The old left panel used to be
     unpositioned, which put this element above it in stacking order
     whatever the DOM order; the menu column has its own z-index now, so
     that half no longer applies - the clipping still does.) */
  overflow: hidden;
  /* Hand every touch gesture here to fractal-grid.js's own pan/pinch-zoom
     handlers instead of the browser's default scroll/pinch-zoom - without
     this, the two fight over the same gesture (the page zooms/scrolls
     underneath while the grid also tries to pan/zoom itself). */
  touch-action: none;
}

/* One of these per Inspect point ("point" and "line" entries only - a
   "grid" entry draws its own connected mesh instead, see
   #inspect-preview's own comment), on the grid itself - background color
   is set from JS (as --marker-color, not background directly - see
   updateInspectMarkers) to that point's own golden-angle hue (matching its
   bodies in the hover preview), with a black outline so it stays visible
   over any hue the fractal coloring underneath happens to be. Position
   (left/top) is set from JS on every render, in CSS pixels relative to
   #grid-canvas-area's own box. Clickable (see updateInspectMarkers) to
   remove that point, so it takes real pointer events rather than passing
   clicks through to the grid underneath; [hidden] (set when the Inspect
   card is collapsed) still wins over that, since a hidden element gets no
   events regardless of pointer-events. */
.inspect-marker {
  position: absolute;
  width: 14px;
  height: 14px;
  margin-left: -7px;
  margin-top: -7px;
  border-radius: 50%;
  border: 2px solid #000;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.5);
  cursor: pointer;
  background: var(--marker-color);
}
/* Hovering previews the click: the dot's own color gives way to a plain ×
   on a dark disc, the same shape/meaning as the list row's own remove
   button (.inspect-row-remove), so there's no doubt clicking removes this
   point rather than adding another one on top of it. Plain background here
   (not !important) is enough to beat the base rule above despite its color
   coming from an inline custom property - a :hover selector already
   outranks the bare class it's attached to, custom property or not. */
.inspect-marker:hover {
  background: var(--bg-panel);
  border-color: var(--danger);
  display: flex;
  align-items: center;
  justify-content: center;
}
.inspect-marker:hover::after {
  content: "\00d7";
  font-size: 13px;
  font-weight: 700;
  line-height: 1;
  color: var(--danger);
}

/* Three things share this one SVG, none of them ever visible at once:
   1. The dashed "if you let go now" line + dots while dragging Inspect
      (Line) - see updateInspectLinePreview.
   2. The axis-aligned rectangle outline while dragging Inspect (Grid) -
      see updateInspectGridPreview.
   3. Once a grid is actually added, its own permanent connect-the-dots
      mesh - one <line> per pair of adjacent points, colored by
      inspectGridPointColor and kept in world-position sync with pan/zoom
      by updateInspectMarkers (same as .inspect-marker dots above). Drawn
      here rather than as extra WebGL geometry since, like the marker dots,
      it's a cheap, occasional, screen-space-only overlay - not something
      that needs to survive being reprojected into the fractal shader.
   No viewBox is set, so 1 SVG user unit = 1 CSS pixel, matching
   worldToCanvasAreaPixel's output directly. */
#inspect-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* The line/dots/rect above are shown and hidden by toggling a plain
   `hidden` ATTRIBUTE on each one (see hideInspectLinePreview/
   hideInspectGridPreview) rather than the `.hidden` IDL property, since
   that property isn't reliably wired up for SVG elements - but the
   browser's own UA stylesheet rule for `[hidden]` turns out to be just as
   unreliable here: SVG shape elements were staying `display: inline`
   (visibly drawn) even with the attribute present. This rule is the actual
   fix - an explicit, author-authored `[hidden] { display: none }` that
   does apply, scoped to this SVG so it can't affect anything else. */
#inspect-preview [hidden] {
  display: none;
}

/* The rendering-progress gauge, lower left - see updateRenderProgressRing
   and makeMenu in fractal-grid.js. Positioned exactly like #inspect-toast
   above it, but as a fifth accordion menu rather than a plain overlay: the
   .menu-item/.menu-toggle/.menu-card machinery (see .menu-toggle and
   .menu-card further up this file) is shared with the four menus in the
   column, including the LEFT is-open accordion group (Inspect, Analysis,
   Display Mode) it joins since it sits in that same corner - even though
   this one lives down here instead of in that stack. Opening it never
   closes Settings or Map Evolution on the right, and vice versa. */
#menu-render-progress {
  position: absolute;
  left: var(--panel-inset);
  bottom: var(--panel-inset);
}

/* Settings and Map Evolution, top right - same .menu-item/.menu-toggle/
   .menu-card machinery as the four menus in the left column (and the
   render-progress gauge below), just anchored to the opposite corner
   instead of living in that stack, and their own accordion group (see
   makeMenu in fractal-grid.js): opening one of these two never closes
   anything on the left, so e.g. Settings and Inspect can both be open at
   once. z-index matches #grid-menu-column so an open card here layers the
   same as an open one over there.

   A real column, mirroring #grid-menu-stack: a collapsed menu's button
   leaves the layout when its card opens (see .menu-item.is-open >
   .menu-toggle), so the two have to stack rather than each own a fixed
   spot - otherwise an open Settings card would simply paint over the Map
   Evolution button below it. align-items: flex-end keeps the two collapsed
   buttons on the right edge, where a full-width card's own right edge also
   lands, so nothing shifts sideways as one opens. */
#grid-right-menu-column {
  position: absolute;
  z-index: 3;
  top: var(--panel-inset);
  right: var(--panel-inset);
  max-width: calc(100% - var(--panel-inset) * 2);
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 8px;
  /* Same reasoning as #grid-menu-stack's: viewport units, since the
     column's own height is auto. */
  max-height: calc(100vh - var(--panel-inset) * 2);
  /* And it scrolls, as that stack does and for the same reason - more than
     one card open at once. Only one of Settings and Map Evolution ever is,
     but the Movie card is meant to stay open beside Map Evolution (see
     menuGroups in fractal-grid.js), and the two together are taller than a
     laptop screen. overflow-x for #grid-menu-stack's reason too. */
  overflow-y: auto;
  overflow-x: hidden;
}
#grid-right-menu-column > .menu-item {
  max-width: 100%;
  flex-shrink: 0;
}
/* A card is what its button turned into, so - unlike the left column's
   menus, which grow from their own top-left corner - these grow from top
   right, matching the corner their toggle buttons sit in.

   Each also leaves room for the OTHER TWO's collapsed buttons plus the
   gaps between them, so opening any one never pushes the others off the
   bottom - this column is only three items deep, so it needs nothing like
   the left column's --menu-reserved. */
#grid-right-menu-column .menu-card {
  transform-origin: top right;
  max-height: calc(100vh - var(--panel-inset) * 2 - (var(--settings-btn-size) + 8px) * 2);
}
/* The collapsed state: no button chrome of its own (no background/border -
   deliberately unlike .menu-toggle elsewhere), just the ring, sized to
   roughly three quarters of the other collapsed buttons and dimmed to
   half-opacity so it reads as a passive gauge sitting over the fractal
   rather than another control competing with the four real buttons above.
   drop-shadow rather than a box-shadow, deliberately - box-shadow follows
   the element's rectangular box, which would draw a square shadow behind a
   ring; drop-shadow follows the painted stroke itself, matching the shape. */
/* Same rule as .menu-item.is-open > .menu-toggle above, restated for this
   toggle's own class - it deliberately isn't .menu-toggle itself (that
   class also carries a square button's background/border/shadow, which
   this bare ring doesn't want), so it needs its own copy of the rule that
   removes the collapsed control from the layout while its card is open. */
.menu-item.is-open > .render-progress-toggle {
  display: none;
}
.render-progress-toggle {
  width: calc(var(--settings-btn-size) * 0.75);
  height: calc(var(--settings-btn-size) * 0.75);
  padding: 0;
  border: none;
  background: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
}
.render-progress-toggle #render-progress-ring {
  width: 100%;
  height: 100%;
  opacity: 0.5;
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.5)) drop-shadow(0 10px 30px rgba(0, 0, 0, 0.45));
}
/* The open state: a single header row and nothing below it - there is no
   .menu-card-body, so the card is exactly one line tall, and (unlike the
   other four cards, where only the header collapses the menu so the body's
   own controls stay clickable) clicking anywhere on it closes it, because
   the header IS the entire card. Sized to fit that one line rather than
   the other cards' fixed --panel-width, and grown out of the ring's own
   corner (see transform-origin) so it reads as the same control opening,
   not a new panel appearing beside it. */
.render-progress-card {
  width: auto;
  max-width: none;
  max-height: none;
  transform-origin: bottom left;
}
.render-progress-header {
  border-bottom: none;
  padding: 6px 16px 6px 6px;
}
.render-progress-icon {
  width: calc(var(--settings-btn-size) * 0.75);
  height: calc(var(--settings-btn-size) * 0.75);
  flex-shrink: 0;
}
.render-progress-icon svg {
  width: 100%;
  height: 100%;
}
.render-progress-label {
  font-size: 13px;
  white-space: nowrap;
}
/* Both rings share one path (see each pair's identical cx/cy/r in
   chaos.html) so progress reads as one ring changing color, not two
   concentric ones. Rotated 90deg so the dash pattern's start point - the
   circle's own 3-o'clock point - lands at 6 o'clock (south) instead;
   stroke-dasharray/dashoffset are set from JS, where the sweep direction
   (dashoffset shrinking from the full circumference to 0) is what actually
   draws it clockwise from there - south to west at a quarter, south to
   north at half. */
.render-progress-ring-track {
  fill: none;
  stroke-width: 3;
  stroke-linecap: round;
  transform: rotate(90deg);
  transform-origin: 20px 20px;
}
#render-progress-ring-coarse,
#render-progress-ring-coarse-open {
  stroke: var(--text-dim);
}
/* Drawn second, directly on top of the coarse ring's own path - at rest
   (dashoffset left at its own circumference) it is fully invisible, so the
   coarse ring alone shows through until antialiasing actually starts. */
#render-progress-ring-aa,
#render-progress-ring-aa-open {
  stroke: #fff;
}

/* ---- The timeline transport ----
   The body of the Map Evolution card (see #menu-playback in chaos.html and
   the Playback section of fractal-grid.js): the shared .playback-controls
   row, the frame readout under it, then the Simulation Duration slider that
   sets where that timeline ends. The card itself needs no look of its own -
   it is an ordinary .menu-card like every other menu's. */
/* .playback-controls carries a margin-top for the flowing column it was
   designed for - meaningless as the first thing in this card's body. */
#grid-playback-panel .playback-controls {
  margin-top: 0;
}
#grid-view #grid-timeline-readout {
  margin: 6px 0 0 0;
  text-align: center;
}

/* Sits right above #inspect-list, but its own margin - not the list's -
   is what has to carry the gap above it: nothing else here puts any space
   between the .tool-grid superlative row above it and a plain button,
   which otherwise left the two touching. */
#btn-inspect-clear-all {
  margin-top: 10px;
}

#inspect-list {
  list-style: none;
  margin: 12px 0 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.inspect-row {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12px;
  color: var(--text-dim);
}

.inspect-dot {
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border-radius: 50%;
  border: 2px solid #000;
}

/* A "line"/"grid" row's stand-in for .inspect-dot above - see
   updateInspectList's own comment on why it's a big glyph instead of a
   small one. align-self: stretch (overriding .inspect-row's own
   align-items: center just for this child) is what lets it actually reach
   the full height of the two-line .inspect-coords-group beside it, rather
   than just being a big glyph vertically centered in a short row. */
.inspect-row-icon {
  flex-shrink: 0;
  width: 18px;
  align-self: stretch;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  line-height: 1;
  color: var(--text-dim);
}

.inspect-coords {
  flex: 1;
  font-variant-numeric: tabular-nums;
}

/* A "line"/"grid" row's two-line body (start coords, end coords) in place
   of a single .inspect-coords span - same flex:1 role in the row, just
   stacked instead of single-line, so the row still reads as ONE entry with
   ONE remove button rather than two. */
.inspect-coords-group {
  flex: 1;
  display: flex;
  flex-direction: column;
  gap: 2px;
  font-variant-numeric: tabular-nums;
}

.inspect-row-remove {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-dim);
  border-radius: 6px;
  padding: 0 6px;
  line-height: 1.6;
  cursor: pointer;
}
.inspect-row-remove:hover {
  background: var(--bg-input);
  color: var(--text);
}

/* "Drag to inspect a line/grid" - see #inspect-toast's own HTML comment
   for when this shows. Opacity-animated rather than the usual [hidden]
   snap, so it actually reads as a toast (fades in, sits, fades out)
   instead of just blinking on and off - which is also why, unlike this
   file's other transient UI, it is never [hidden]: toggling display:none
   and an opacity transition in the same moment leaves the transition
   nothing to animate from (the same reasoning as .settings-panel's own
   "Always in the DOM" comment in app-shell.css). pointer-events:none is
   what actually keeps it out of the way at rest instead. */
.inspect-toast {
  position: absolute;
  left: 50%;
  /* Clear of the bottom row - see --toast-bottom on #grid-canvas-area. */
  bottom: var(--toast-bottom);
  transform: translateX(-50%);
  z-index: 20;
  max-width: 320px;
  text-align: center;
  background: rgba(12, 13, 17, 0.92);
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 10px 16px;
  color: var(--text);
  font-size: 13px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s ease;
}
.inspect-toast.visible {
  opacity: 1;
}

#grid-canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: default;
  /* Belt-and-suspenders with #grid-canvas-area's own touch-action: none (which
     already covers this element as its ancestor) - set directly on the
     actual touch target too. */
  touch-action: none;
  /* The backing store matches the display's actual device pixels (see
     resizeCanvas in fractal-grid.js), so this is only a safety net against
     the rare sub-pixel rounding mismatch between CSS size and device
     pixels - nearest-neighbor is never worse than smoothed for that. */
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
#grid-canvas.dragging { cursor: grabbing; }

#empty-state {
  position: absolute;
  inset: 0;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  color: var(--text-dim);
  font-size: 14px;
  text-align: center;
  padding: 24px;
}
/* An unconditional `display: flex` above would win over the browser's own
   [hidden] { display: none }, since author styles beat user-agent defaults
   regardless of specificity - only apply it once `hidden` is gone. */
#empty-state:not([hidden]) {
  display: flex;
}
#empty-state a {
  color: var(--accent);
}

/* Reusable tip popover (see showTip in fractal-grid.js) - one shared
   instance, positioned in JS against whatever element it's currently
   pointing at. Fixed positioning so it isn't clipped by the menu column's
   own overflow-y: auto - see #grid-menu-stack and .menu-card-body, both of
   which scroll and both of which hold tip anchors. */
.tip-popover {
  position: fixed;
  z-index: 1000;
  max-width: 240px;
  background: var(--bg-panel);
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
}
.tip-popover[hidden] {
  display: none;
}
/* The arrow's horizontal offset is set per-tip by positionTip (JS), which
   aims it at the middle of whatever control the tip is pointing at. It has
   to be free to move because the popover body itself gets clamped inside the
   viewport: for an anchor near the right edge (the Settings gear) the body
   ends up well to the LEFT of its anchor, and a fixed-position arrow would
   then point at empty space. The 16px fallback is the old fixed value, used
   only if the popover is ever shown without positionTip running. */
.tip-popover::before {
  content: "";
  position: absolute;
  top: -6px;
  left: var(--tip-arrow-left, 16px);
  width: 10px;
  height: 10px;
  background: var(--bg-panel);
  border-left: 1px solid var(--accent);
  border-top: 1px solid var(--accent);
  transform: rotate(45deg);
}
.tip-popover p {
  margin: 0 0 10px 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text);
}
.tip-popover-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
}
.tip-popover-actions button {
  font-size: 12px;
  padding: 5px 10px;
}

/* Gear button in the panel head, and its × twin inside the settings panel -
   square, icon-only, otherwise matching button.secondary's look. */
.icon-button {
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  font-size: 16px;
  line-height: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text);
}
.icon-button:hover {
  background: var(--bg-input);
}

/* .settings-panel and its header/body are styled in app-shell.css now - the
   settings button and the panel it opens both moved to a shared, cross-view
   floating design (see .floating-settings-btn there) instead of each page
   keeping its own copy of a slide-in drawer. */

/* ---- Display Mode rows ----
 *
 * One row per mode: a thumbnail of what that mode looks like, and a
 * sentence saying what it does with the sample cost on its last line.
 * A radiogroup rather than a <select>, because the thumbnail IS the
 * explanation here - the difference between these five is entirely visual,
 * and a dropdown can only ever name them.
 *
 * The rows sit flush against each other with dividers between, so the four
 * read as one list of choices rather than five separate controls - that's
 * #grid-display-mode-list's own gap: 0 below, kept separate from the card
 * body's gap so it doesn't also flatten the space above the list, between
 * it and the Reset View/zoom row. */
#grid-display-panel-body {
  padding: 14px 6px 6px 6px;
}
#grid-display-mode-list {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.display-mode-row {
  display: flex;
  align-items: center;
  gap: 11px;
  width: 100%;
  padding: 8px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: 10px;
  text-align: left;
  color: var(--text);
  cursor: pointer;
  /* The rows are buttons, so they inherit button's own centring and
     font-size from physics.css unless told otherwise. */
  font: inherit;
}
.display-mode-row + .display-mode-row {
  margin-top: 2px;
}
.display-mode-row:hover {
  background: var(--bg-input);
}
.display-mode-row[aria-checked="true"] {
  background: var(--bg-input);
  border-color: var(--accent);
}

/* A share of the row rather than a fixed width, so dragging the panel
   resizer scales the thumbnail and its text column together instead of
   shrinking only the text. */
.display-mode-row img {
  flex: 0 0 auto;
  /* Both of these, and min-width: 0 as well. An <img> is a replaced element,
     so its intrinsic 400px is what min-width: auto resolves to inside a flex
     row - a flex-basis alone loses to that and the thumbnail renders full
     size, pushing the text out of the card entirely. */
  width: 44%;
  min-width: 0;
  height: auto;
  display: block;
  border-radius: 6px;
}

/* min-width: 0 so a long word can wrap instead of forcing the row wider
   than the card - the default min-width:auto on a flex child would let it
   push the thumbnail out instead.
 *
 * The font size and line height here are tuned against the thumbnail's
 * width above so that a one-sentence description plus the sample line comes
 * out a little shorter than the image is tall - about 0.83 of it - at the
 * card's DEFAULT width. Widening the card only rewraps the text shorter, so
 * the relationship holds from there up.
 *
 * It does not hold below the default, and cannot: in a side-by-side row the
 * text's height is set by how narrow its column is, and giving it more room
 * means taking width from the thumbnail, which shrinks the thumbnail's
 * height by as much - there is no share of the row that wins at every
 * width. Dragging the panel narrower than its default therefore makes these
 * rows progressively more text than picture. Nothing clips or overflows
 * when it does; the row just stops being the shape it was tuned to be,
 * which is the price of not carrying a second layout for it. */
.display-mode-text {
  min-width: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--text-dim);
}
.display-mode-row[aria-checked="true"] .display-mode-text {
  color: var(--text);
}
.display-mode-name {
  display: block;
  font-weight: 600;
  color: var(--text);
}
/* Standard's own row only (see the DISPLAY_MODES loop in fractal-grid.js) -
   .toggle-field's own margin-bottom already separates it from the sample
   cost line below, but nothing put space between it and the name above. */
.display-mode-text .toggle-field {
  margin-top: 6px;
}
/* The sample cost, on its own last line - the one number that says what
   choosing this row will cost, so it is set apart from the sentence rather
   than buried at the end of it. */
.display-mode-cost {
  display: block;
  font-size: 12px;
  color: var(--text-dim);
}

/* ---- The longest-ridge / longest-valley overlay ----
   Created and removed from JS (see setFeatureOverlay in fractal-grid.js)
   when Feature Census's own switch asks for it, so it costs nothing at all
   the rest of the time. Sits over the grid inside #grid-canvas-area's own
   box, in the same CSS pixels the canvas fills, and takes no pointer
   events: panning and zooming still belong to the grid underneath - which
   is what removes this in the first place. */
#grid-feature-overlay {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* ---- The Movie card (see "Movies" in fractal-grid.js) ---- */

#movie-add-keyframe,
#movie-done {
  width: 100%;
}

#movie-keyframe-list {
  list-style: none;
  margin: 10px 0 12px 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
#movie-keyframe-list:empty {
  display: none;
}

/* Two lines a keyframe: what it is, then what can be done to it. One line
   would be the card's whole width in buttons, with no room left to say which
   keyframe they belong to. */
.movie-keyframe {
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--bg-input);
  font-size: 12px;
  color: var(--text-dim);
}
.movie-keyframe-what {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 8px;
  white-space: pre;
}
.movie-keyframe-what b {
  flex-shrink: 0;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 9px;
  background: var(--accent);
  color: #fff;
  font-size: 11px;
}
/* A number in the row (see movieField in fractal-grid.js): caption, the
   number, and a stacked pair of arrows that say it can be changed. */
.movie-field {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  flex-shrink: 0;
}
.movie-field-caption {
  color: var(--text-dim);
  font-size: 11px;
}
.movie-field input {
  width: 62px;
  padding: 2px 4px;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
  text-align: right;
  color: var(--text);
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: 5px;
}
.movie-field input:focus {
  outline: none;
  border-color: var(--accent);
}
.movie-field-arrows {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
}
.movie-field-arrows button {
  width: 16px;
  height: 10px;
  padding: 0;
  font-size: 7px;
  line-height: 1;
  color: var(--text-dim);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 3px;
  cursor: pointer;
}
.movie-field-arrows button:hover {
  color: var(--text);
  background: var(--bg-input);
  border-color: var(--accent);
}
.movie-keyframe-actions {
  display: flex;
  justify-content: flex-end;
  gap: 4px;
  margin-top: 6px;
}
.movie-keyframe-actions .icon-button {
  width: 26px;
  height: 24px;
  font-size: 13px;
}
#movie-summary {
  margin: 10px 0;
}

/* ---- Settings > Performance Settings (see "Performance settings" in
   fractal-grid.js) ---- */

/* A titled group inside a card's body - the first the Settings card has
   needed. Set off from the controls above it by a rule rather than a box:
   the card is already a box. */
.settings-section {
  display: flex;
  flex-direction: column;
  margin-top: 4px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.settings-section h3 {
  margin: 0 0 12px 0;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-dim);
}
/* The readout's switch belongs to the section but not to the presets, and
   sits apart from the controls they set. */
#grid-view #perf-readout-field {
  margin: 4px 0 0 0;
  padding-top: 14px;
  border-top: 1px dashed var(--border);
}

/* The three-stop preset slider, with each stop's name under it. The names
   are a three-column grid rather than flexed text, so "Custom" sits under
   the middle stop whatever the two longer names on either side wrap to. */
.perf-preset {
  margin-bottom: 10px;
}
.perf-preset-labels {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  gap: 6px;
  margin-top: 4px;
  font-size: 11px;
  line-height: 1.25;
  color: var(--text-dim);
}
.perf-preset-labels span {
  padding: 2px 0;
}
.perf-preset-labels [data-preset="low"] { text-align: left; }
.perf-preset-labels [data-preset="custom"] { text-align: center; }
.perf-preset-labels [data-preset="high"] { text-align: right; }
/* Low and High can be pressed; Custom only ever happens. */
.perf-preset-labels [data-preset="low"],
.perf-preset-labels [data-preset="high"] {
  cursor: pointer;
}
.perf-preset-labels .is-current {
  color: var(--text);
  font-weight: 600;
}
/* "Custom" names a place the slider can only end up, never be put - so its
   name is only there while the slider is. (Hidden, not removed: it is the
   middle column of the grid above.) */
.perf-preset-labels [data-preset="custom"]:not(.is-current) {
  visibility: hidden;
}
/* The controls the slider sets, folded away under it (see #perf-details in
   chaos.html) - the editor's own "Advanced" disclosure, whose legend has to
   sit on this card's fill rather than the editor panel's. */
#perf-details {
  margin-top: 0;
}
#perf-details[open] > summary {
  background: #1e212b;
}
#perf-details[open] {
  padding-top: 16px;
}
#grid-view .field select:disabled {
  opacity: 0.55;
}

/* The performance readout: bottom centre of the map - the one stretch of its
   edge no menu opens over (the columns hang from the two upper corners, and
   an open card reaches the bottom of its side) - and above the watermark's
   line. pointer-events: none - the map under it still pans and zooms. Wraps
   rather than clips, since a phone is narrower than some of its lines. */
#perf-readout {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: 32px;
  z-index: 2;
  margin: 0;
  width: max-content;
  max-width: min(440px, calc(100% - 16px));
  padding: 6px 8px;
  border-radius: 6px;
  background: rgba(8, 9, 12, 0.78);
  color: #c9f5d0;
  font: 10.5px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}
#perf-readout[hidden] {
  display: none;
}
