/* ============================================================
   controls.css — the shared control vocabulary. Used by BOTH
   the portal and analytic mode.

   Owns the STATUS BAR, the panel-internal vocabulary (buttons, inputs,
   chip/field/result/meta/delta/seg), the viewport-level furniture
   (`.tool-*` / `.legend` / `.mode-switch`), and the PANEL SHELL below —
   everything both modes need in one unscoped place, rather than
   duplicated per mode.
   ============================================================ */

/* ============================================================
   PAGE SHELL — the three horizontal bands every mode shares.

   .app is the flex column; .stage is the band between the top bar and the
   status bar, and the containing block for everything that floats over it.
   BOTH MODES USE THESE, which is why they are unscoped: portal fills
   .stage with #map, analytic fills it with #viewer-root, and the frame
   around them does not change. See portal.css's header comment on .app
   and .stage for what breaks if these rules are scoped to one mode only.

   The home page loads neither this file nor these class names, so leaving
   the rules unscoped cannot reach it.
   ============================================================ */
.app   { display: flex; flex-direction: column; height: 100vh; }
.stage { position: relative; flex: 1; min-height: 0; background: var(--bg-ground); }

/* ============================================================
   PANEL SHELL — the floating column and the cards inside it.

   Unscoped rather than [data-mode="portal"] rules, because analytic mode
   builds its left column out of exactly the same three pieces. The two
   modes must agree to the pixel, and one unscoped rule is the only way to
   make that structurally true rather than kept true by hand.

   RULE: SPECIFICITY MATTERS HERE. An unscoped bare .fpanel is (0,1,0); a
   scoped [data-mode="portal"] .fpanel would be (0,2,0) and any surviving
   `#controls …` rule elsewhere is (1,0,0) and would win over either. If a
   panel ever reverts to an older look after an edit here, look for an id
   selector first.
   ============================================================ */
.col-left {
  position: absolute; z-index: 4;
  left: var(--inset-overlay); top: var(--inset-overlay); bottom: var(--inset-overlay);
  width: var(--w-panel);
  display: flex; flex-direction: column; gap: var(--sp-3);
  /* Lets a drag that starts in the gap between the card and the mode switch
     reach the map/scene underneath, same reasoning as .vt-root's below. */
  pointer-events: none;
}
.col-left > * { pointer-events: auto; }

.fpanel {
  background: var(--overlay-bg);
  -webkit-backdrop-filter: var(--overlay-blur);
          backdrop-filter: var(--overlay-blur);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  display: flex; flex-direction: column;
  /* RULE: THE REAL SIZE OF EVERY BUTTON, CHIP AND INPUT IN THIS CARD.
     Without this, nothing inside sets its own line-height, so all of it
     inherits body's 1.75 (base.css) — a value sized for multi-line
     paragraph reading, not single-line UI chrome, leaving every control
     quietly taller than intended, with the extra space sitting invisibly
     above/below the glyphs rather than in the padding anyone can see and
     tune. Overriding here, once, tightens every control in BOTH modes
     (analytic shares this shell) without touching each selector's own
     padding. Multi-line captions are unaffected —
     they already set --lh-tight or their own literal explicitly, which
     wins on specificity regardless of what the card around them does. */
  line-height: var(--lh-tight);
  /* Without this, a flex child cannot shrink below its content's natural
     height — .fpanel-body's own overflow-y:auto would never actually
     trigger, and Step 4 fully expanded would push the whole card (and
     .col-left with it) taller than .stage instead of scrolling internally. */
  min-height: 0;
}
.fpanel--grow { flex: 1; min-height: 0; }
/* A readout card rather than a control column, so it takes the thinner of
   the two overlay grounds (tokens.css). Used by analytic mode's right
   column, whose three cards are title-only shells — see analytic.css. */
.fpanel--thin { background: var(--overlay-bg-thin); }

.fpanel-head {
  flex: none; padding: 12px 16px; display: flex; align-items: center; gap: var(--sp-3);
  border-bottom: var(--border) solid var(--line);
}
/* A head with nothing under it draws no rule: the border-bottom exists to
   separate the title from a body, and with no body it reads as an empty
   box someone forgot to fill. Reached by :only-child rather than by a
   modifier class so the title-only shells in analytic.css need no extra
   markup, and so a card stops looking unfinished the moment a body is
   added to it. */
.fpanel-head:only-child { border-bottom: 0; }

.fpanel-body {
  flex: 1; min-height: 0; padding: 16px;
  display: flex; flex-direction: column; gap: var(--sp-5);
  overflow-y: auto; overflow-x: hidden; overflow-wrap: break-word;
  overscroll-behavior: contain;
  scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent;
}
.fpanel-body::-webkit-scrollbar { width: var(--scrollbar-w); }
.fpanel-body::-webkit-scrollbar-track { background: transparent; }
.fpanel-body::-webkit-scrollbar-thumb {
  /* square, matching base.css's global thumb — the design has no rounded
     radius anywhere except 0 or 2px, and this scrollbar thumb must not be
     the one exception. */
  background: var(--scrollbar-thumb); border-radius: 0;
}
.fpanel-body::-webkit-scrollbar-thumb:hover { background: var(--scrollbar-hover); }

/* always-available utilities live in the panel's own foot, outside the
   scrolling body — Reset must stay reachable regardless of scroll position. */
.fpanel-foot {
  flex: none; padding: 14px 16px; display: flex; align-items: center; gap: var(--sp-3);
  border-top: var(--border) solid var(--line);
}

/* checkbox rows inside a panel body, using the SAME self-drawn look the
   .check rule further down this file defines — reached through the bare
   <label> both modes' markup already has, so no markup rewrite is needed
   to wire it up; analytic mode inherits it for free, which is most of why
   the shell lives in this shared file. */
.fpanel-body label {
  display: flex; align-items: center; gap: 10px;
  font-size: var(--fs-label); color: var(--text-2); cursor: pointer;
}
.fpanel-body input[type="checkbox"] {
  appearance: none; -webkit-appearance: none; flex: none;
  width: 14px; height: 14px; margin: 0; cursor: pointer;
  background: transparent; border-radius: var(--radius);
  border: var(--border) solid var(--line-strong);
}
.fpanel-body input[type="checkbox"]:checked {
  background: var(--accent); border-color: var(--accent);
}
.fpanel-body input[type="checkbox"]:checked::after {
  content: "✓"; display: block; color: var(--bg-ground);
  font-size: 9px; line-height: 12px; text-align: center;
}
.fpanel-body input[type="checkbox"]:disabled { border-color: var(--line-off); }

/* ============================================================
   CONTROL ROWS — the small layout pieces both panels build rows from.
   Shared, not portal-only: .wf-inline-label labels the analytic panel's
   per-surface sliders and .wf-readout carries its layer spacing value, so
   scoping either to one mode would break the other.
   ============================================================ */

/* a slider paired with a small number field or icon button, read as one
   control — hence the tightest gap on the 4/8/12/20/36 scale */
.wf-row {
  display: flex; align-items: center;
  gap: var(--sp-1); margin-top: var(--sp-1);
}
.wf-row .ui-slider { flex: 1; }

/* Site size (step 1) and Grid size (step 3.1) are the two places a
   slider+number row sits directly above a CTA button, with nothing
   between them. .btn/.wf-btn's own margin is --sp-1 — right for
   separating a button from its OWN label above it, but far too tight
   between two DIFFERENT controls, which is why "Confirm range" and
   "Start preliminary computation" read as glued to the row above them.
   Adjacent-sibling rather than widening .btn's margin everywhere: every
   other button (Reset workflow, Select site range under its own title
   row) is fine at the tighter spacing, and only this specific
   row-then-button adjacency was the bug. */
.wf-row + .btn,
.wf-row + .wf-btn { margin-top: var(--sp-4); }

/* RULE: THE COMPOUND SELECTOR IS LOAD-BEARING. Bare `.wf-number-sm` is
   (0,1,0) and loses to the `input[type="number"] { width: 100% }` rule
   further down this file at (0,1,1) — which no load order can fix,
   because specificity is decided before order. Without the compound
   selector, the four small number fields would take the full row width
   and squeeze the slider beside each of them to 0px: Site size, Grid
   size, Air temperature and Layer spacing would each have a working
   number box and an invisible, undraggable slider.
   --w-value is the right-aligned numeric column, and these four fields
   are exactly that. It also fits the 4 digits Site size can reach (max
   1000), which the old 52px literal did not. */
.ui-number.wf-number-sm { width: var(--w-value); flex-shrink: 0; }

.wf-unit { font-size: var(--fs-hint); color: var(--text-3); flex-shrink: 0; }

/* float:right does nothing for #wf-time-value/#wf-air-value, which sit
   inside .field-head (a flex container): float computes to none on a
   flex item per spec, so it is simply ignored there. It still does real
   work for the other caller, #viewer-xv-spacing-value, which is inline in
   a plain <label> in analytic mode. */
.wf-readout { float: right; font-variant-numeric: tabular-nums; color: var(--text-1); }

.wf-inline-label { margin-top: var(--sp-1); font-size: var(--fs-hint); color: var(--text-2); }

/* ============================================================
   BUTTONS — the panel's action buttons, plus the CTA's five states.

   .btn / .btn--primary are the design vocabulary's names. .wf-btn /
   .wf-btn-cta stay as ALIASES rather than being renamed: this is a pure
   appearance change, .wf-btn appears on 8 buttons in index.html, and a
   rename would buy nothing but a different word to type.
   ============================================================ */
.btn, .wf-btn {
  display: block; width: 100%;
  margin: var(--sp-1) 0; padding: 9px var(--sp-2);
  font-size: var(--fs-label); font-family: inherit; letter-spacing: var(--ls-button);
  /* Declared here, not just inherited from .fpanel: every .btn inside the
     panel already gets --lh-tight from that container rule, but .mode-switch
     and .wf-beta-tag below are NOT inside .fpanel (they're .col-left's other
     sibling / the viewer HUD), so anything relying on the ancestor alone
     stayed at body's loose 1.75 there. Declaring line-height on the
     component itself makes it correct wherever it is used, not just where
     it happens to be nested today — see the .chip/.mode-switch copies of
     this same note. */
  line-height: var(--lh-tight);
  text-align: center; cursor: pointer;
  background: transparent; color: var(--text-2);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
}
.btn:hover:not(:disabled), .wf-btn:hover:not(:disabled) { background: var(--bg-inset); color: var(--text-1); }

/* Disabled is a SHAPE difference (dashed border + dimmed text), never a
   brightness one — an opacity-based dim would make a live button and a
   disabled one look nearly the same brightness at a glance. */
.btn:disabled, .wf-btn:disabled {
  color: var(--text-off); border-style: dashed; border-color: var(--line-off);
  cursor: not-allowed;
}

.btn-row        { display: flex; gap: var(--sp-3); }
.btn-row > .btn { flex: 1; }

/* ---- the CTA's five states (.wf-btn-cta) ----
   Richer than a plain .btn--primary/:disabled pair: this is a working
   three-state machine (upcoming/running/done) plus two hand-tuned
   exceptions, and none of the five may be lost. Every state below reads
   purely off classes ui/workflow-controls.js already writes elsewhere
   (markStep(), the wf-substep-done toggle, wf-btn-cta-done); nothing here
   is a new JS contract, and the two `:has()` selectors read those same
   existing classes — only their declarations live here, as token colours. */

/* a. upcoming — press this next. font-weight 500, not 700/bold: this
   design only ever uses 400/500/600. */
.wf-step:not(.wf-step-done) > .wf-btn-cta:not(:disabled),
.wf-substep:not(.wf-substep-done) > .wf-btn-cta:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 500;
}

/* c. done — still enabled means still re-runnable (select a new site, redo
   the preliminary pass, rerun the prediction), so this recedes to 400
   rather than reading as dead. */
.wf-step.wf-step-done > .wf-btn-cta:not(:disabled),
.wf-substep.wf-substep-done > .wf-btn-cta:not(:disabled) {
  border-color: var(--done);
  color: var(--done);
  font-weight: 400;
}

/* c'. done, but disabled — wf-confirm-range's one-shot case (it disables
   itself the instant it is pressed, so it is not re-runnable the way
   Query/Preliminary/Run are). Dashed, not the old opacity:.85 — the same
   shape signal every other disabled control now uses, instead of a bespoke
   fade tuned just to keep --done legible through it. */
.wf-step.wf-step-done > .wf-btn-cta:disabled {
  border-color: var(--done);
  border-style: dashed;
  color: var(--done);
}

/* a'. site placed, not yet confirmed — #wf-select-site's OWN done-ness
   (.wf-btn-cta-done), independent of its step's, so the moment a point
   lands, Select goes green and Confirm is left as the only orange button.
   ID selector is a known, accepted exception here.
   TODO: replace with a class selector once the panel's markup is
   reworked. */
#wf-select-site.wf-btn-cta-done:not(:disabled) {
  border-color: var(--done);
  color: var(--done);
  font-weight: 400;
}

/* b. running — :has(), not :disabled or :not(...-done): wf-preliminary does
   NOT disable itself while its SVF phase runs, and 3.1's substep can
   already read "done" while that phase is still busy. Deliberately the
   SAME colour as (a) upcoming: this design allows exactly three interface
   state colours, and running IS live — distinguished instead by a
   brighter background, the spinner, and cursor:wait. Comes last in the
   cascade so it outranks both (a) and (c) regardless of source order. */
.wf-step:has(> .wf-status.wf-status-busy) > .wf-btn-cta,
.wf-substep:has(> .wf-status.wf-status-busy) > .wf-btn-cta {
  border-color: var(--accent);
  background: var(--accent-fill-hi);
  color: var(--accent);
  font-weight: 500;
  cursor: wait;
}

/* Spinner — the ONE place a circular radius survives the flat-corners
   design (var(--radius) everywhere else): a square spinner reads as a
   render bug, not a running process. Shared by the CTA and .wf-status-busy
   below via the same @keyframes. */
.wf-step:has(> .wf-status.wf-status-busy) > .wf-btn-cta::before,
.wf-substep:has(> .wf-status.wf-status-busy) > .wf-btn-cta::before {
  content: '';
  display: inline-block;
  width: 9px; height: 9px;
  margin-right: var(--sp-2);
  vertical-align: -1px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: wf-spin 0.7s linear infinite;
}
@keyframes wf-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .wf-btn-cta::before { animation: none; }
}

/* ---- secondary / icon / toggle buttons ---- */

/* Dashed border already reads as "lower commitment than a solid one" — no
   opacity needed on top of it (was opacity:.7). */
.wf-btn-ghost {
  border-style: dashed;
  color: var(--text-2);
  font-size: var(--fs-hint);
}

/* Square button inside a .wf-row next to a slider, rather than spanning
   the panel like every other .btn/.wf-btn. */
.wf-btn-icon {
  display: inline-block; width: auto; flex: none;
  margin: 0; padding: var(--sp-1) var(--sp-3);
  line-height: 1.1;
}

/* Playing — same accent as the CTA's upcoming state, so "this control is
   doing something right now" reads the same way everywhere in the panel. */
.wf-btn-active:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}

/* The step 4 "Settings" toggle (.wf-btn-advanced-toggle) was removed with
   the fold it drove — the layer controls now live in the icon column's
   Layer popover (js/ui/view-tools.js). */

/* ---- status line (.wf-status) ----
   Base tone is --text-3; -warn/-ok just set colour, with no opacity:1
   "reset" needed since the base rule does not dim anything to reset FROM.
   Shares @keyframes wf-spin with the CTA spinner above. */
.wf-status {
  font-size: var(--wf-status-font-size);
  color: var(--text-3);
  white-space: normal;
  line-height: 1.4;
}
.wf-status.wf-status-warn { color: var(--accent); }
.wf-status.wf-status-ok   { color: var(--done); }

/* Step 2 downloads and decodes a few hundred orthophotos before it can
   sample them, which is slow by nature rather than by fault. The spinner
   sits inline in the status line so the progress count stays readable
   next to it. */
.wf-status.wf-status-busy::before {
  content: '';
  display: inline-block;
  width: 9px; height: 9px;
  margin-right: var(--sp-2);
  vertical-align: -1px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: wf-spin 0.7s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
  .wf-status.wf-status-busy::before { animation: none; }
}

/* ============================================================
   INPUTS — range / number / text / select.

   .ui-slider / .ui-number are the portal's own names (20 / 4 uses across
   the Simulation panel + the 3D viewer panel) kept as aliases alongside
   the bare tag selectors, same reasoning as .wf-btn above.
   WARNING: removing or mistyping either class does NOT throw and does NOT
   show up in the console — it just drops all 24 controls that carry it
   back to the browser default. A computed-style assertion (verified via
   `appearance: none`) is the only way to catch that regression.
   ============================================================ */

/* Track is 2px, but the input's own hit area is taller (12px) so the
   slider is still easy to grab with a mouse or finger — margin:0 also
   subsumes the old #controls-scoped `margin-inline: 0` fix for the 2px
   native side margin that used to overflow the fixed-width panel by 4px;
   that ID rule is removed now that every range input gets margin:0 here. */
input[type="range"], .ui-slider {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 12px; background: transparent; margin: 0;
}
input[type="range"]::-webkit-slider-runnable-track,
.ui-slider::-webkit-slider-runnable-track { height: var(--slider-track-height); background: var(--slider-track-color); }
input[type="range"]::-moz-range-track,
.ui-slider::-moz-range-track { height: var(--slider-track-height); background: var(--slider-track-color); }

/* Square thumb (border-radius:0), not round — spec's own deliberate
   choice for range inputs specifically; the CTA/status spinners are the
   one place a circle survives. margin-top centres the thumb on the 2px
   track within the taller 12px hit box: -(thumb - track) / 2. */
input[type="range"]::-webkit-slider-thumb,
.ui-slider::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: var(--slider-thumb-size); height: var(--slider-thumb-size);
  margin-top: -4px; border: var(--slider-thumb-border);
  border-radius: 0; background: var(--slider-thumb-color);
  cursor: pointer;
}
input[type="range"]::-moz-range-thumb,
.ui-slider::-moz-range-thumb {
  width: var(--slider-thumb-size); height: var(--slider-thumb-size);
  border: var(--slider-thumb-border); border-radius: 0;
  background: var(--slider-thumb-color); cursor: pointer;
}

/* Disabled — track/thumb step down to --line/--line-strong. A range input
   has no border of its own to dash, so this is spec's own given exception
   to "disabled is always a shape difference": the closest shape-like signal
   a track can carry is losing contrast against its own thumb. */
input[type="range"]:disabled::-webkit-slider-runnable-track,
.ui-slider:disabled::-webkit-slider-runnable-track { background: var(--line); }
input[type="range"]:disabled::-webkit-slider-thumb,
.ui-slider:disabled::-webkit-slider-thumb { background: var(--line-strong); }
input[type="range"]:disabled::-moz-range-track,
.ui-slider:disabled::-moz-range-track { background: var(--line); }
input[type="range"]:disabled::-moz-range-thumb,
.ui-slider:disabled::-moz-range-thumb { background: var(--line-strong); }

/* ---- text / number / select ----
   .ui-number / .wf-select stay as aliases, same reasoning as above.
   background is var(--bg-inset), never transparent: a <select>'s open
   popup list inherits the control's background in Chrome, and a
   transparent one renders the open list as unreadable text on the page
   behind it (this bit them once already, hence --bg-inset being fully
   opaque rather than a translucent panel tone). */
input[type="number"], input[type="text"], .ui-number,
select, .wf-select {
  width: 100%; min-height: var(--h-control);
  /* Flatter box: was 8px 10px. --h-control (32px) is the intended floor —
     it barely mattered before because 8px padding plus body's inherited
     1.75 line-height on 12px text (21px) already exceeded it (measured
     39px, not 32). Now that line-height is tight (below), 6px padding
     lands almost exactly on --h-control instead of blowing past it. */
  padding: 6px 10px;
  line-height: var(--lh-tight);
  font-size: var(--fs-label); font-family: inherit; color: var(--text-1);
  background: var(--bg-inset);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  font-variant-numeric: tabular-nums;
}
/* No spinner arrows. They are a browser widget this design never sized for:
   Chrome reserves their strip inside the box whether or not they are drawn,
   so a narrow number field like #wf-site-size-value can clip its own value
   ("300" showing as "30") if the widget's reserved width is not accounted
   for. Both vendor forms are needed — the pseudo-element for WebKit, the
   appearance keyword for Gecko. */
input[type="number"] { -moz-appearance: textfield; }
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

select, .wf-select {
  appearance: none; -webkit-appearance: none; padding-right: 26px; cursor: pointer;
  background-image: linear-gradient(45deg, transparent 50%, var(--text-3) 50%),
                    linear-gradient(135deg, var(--text-3) 50%, transparent 50%);
  background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
  background-size: 5px 5px, 5px 5px; background-repeat: no-repeat;
}

input:disabled, select:disabled, .ui-number:disabled, .wf-select:disabled {
  color: var(--text-off);
  /* background-color, not the `background` shorthand: a shorthand here would
     reset select's own background-image (the dropdown arrow, set above) to
     none, silently losing the arrow on every disabled dropdown. */
  background-color: #141617; /* one-off, slightly darker than --bg-inset for
                                 a disabled field's own bed */
  border-color: var(--line-off);
  cursor: not-allowed;
}

input:focus-visible, select:focus-visible,
.btn:focus-visible, .wf-btn:focus-visible,
.tool-btn:focus-visible, .vt-btn:focus-visible,
.mode-switch:focus-visible {
  outline: none;
  border-color: var(--accent);
}

/* ---- checkbox ----
   A self-drawn .check (appearance:none + a drawn checkmark) is written
   here but has zero callers today — the panel deliberately keeps the
   native checkbox in the meantime (accent-color already pointed at
   --accent). Self-drawing needs `<label><input>text</label>` to be
   rewrapped as `<label class="check"><input>...<span class="t-label">
   text</span></label>`; defined now so that markup rework has the CSS
   ready when it happens. */
.check        { display: flex; align-items: center; gap: 10px; }
.check input  { appearance: none; -webkit-appearance: none; flex: none;
                width: 14px; height: 14px; margin: 0; cursor: pointer;
                background: transparent; border-radius: var(--radius);
                border: var(--border) solid var(--line-strong); }
.check input:checked        { background: var(--accent); border-color: var(--accent); }
.check input:checked::after { content: "✓"; display: block; color: var(--bg-ground);
                              font-size: 9px; line-height: 12px; text-align: center; }
.check input:disabled       { border-color: var(--line-off); }
.check .t-label             { flex: 1; }

/* ============================================================
   FIELD — a labelled control. Value sits ABOVE the track in its own
   right-aligned column, never beside the label where a long label and a
   wide value would collide. Used instead of the older .wf-readout
   `float: right` pattern by Time of day and Air temperature offset.
   ============================================================ */
.field       { display: flex; flex-direction: column; gap: var(--sp-2); }
.field-head  { display: flex; align-items: baseline; gap: var(--sp-3); }
.field-head .t-label { flex: 1; }
.field-head .t-value { width: var(--w-value); text-align: right; }
.field-head .t-unit  { width: var(--w-unit); }
.field-bounds{ display: flex; justify-content: space-between;
               font-size: var(--fs-eyebrow); color: #575c5e;
               font-variant-numeric: tabular-nums; }
.field[data-disabled] .t-label,
.field[data-disabled] .t-value { color: var(--text-off); }
.field[data-disabled] .t-unit  { color: #3c4042; }

/* ============================================================
   SCENARIO — WAS HERE, and the class it styled has no users left.

   `.scenario` / `.scenario-note` marked a control that RECOMPUTED rather
   than one that looked: an inset bed with a left rule, plus a caption
   explaining that it re-ran the whole day. The panel has two kinds of
   control and they cost wildly different amounts —

     view       time of day, layer toggles, opacity — instant, a lookup
     scenario   material edits — seconds, and a progress line while it works

   — and without a mark on the page that difference reads as a system that
   is randomly fast or slow.

   Both users of the class were the air-temperature offset, in the map
   panel and in Analytic Mode, and that feature is retired: the diurnal
   curve is the EPW file's own hours and nothing in either panel moves it.
   Material edits are the only scenario control left, and they have never
   used this treatment — they live in their own card with their own status
   line, which is a stronger version of the same idea.

   WARNING: if a second scenario control is ever added, the distinction is
   worth marking again, and it should NOT be a colour. --accent means
   LIVE/actionable/selected and is spent on the CTA; borrowing it would
   make an ordinary slider compete with the button you are meant to press
   next.
   ============================================================ */

/* ============================================================
   CHIP — a small state tag: one per group, carrying its own state.
   .wf-beta-tag (used in #mode-dock and the 3D viewer's HUD) is an ALIAS
   sharing this selector rather than its own bespoke 9px/blue-outline pill.
   It gets one extra rule below for the inline positioning that only IT
   needs (sitting next to running text, not standalone).
   ============================================================ */
.chip, .wf-beta-tag {
  font-size: var(--fs-micro); font-weight: var(--fw-micro);
  letter-spacing: var(--ls-micro); text-transform: uppercase;
  padding: 2px 6px; color: var(--text-3);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  /* RULE: declared here, not left to the ambient container, precisely
     because .wf-beta-tag is used in TWO places that are NOT inside
     .fpanel: #wf-enter-viewer's own "beta" (a .mode-switch sibling of
     .fpanel, not a descendant) and #viewer-beta-tag (inside .viewer-hud).
     Without this, both would inherit body's loose 1.75 line-height while
     the in-panel chip stays tight, producing a visibly taller, deformed
     badge in those two places. */
  line-height: var(--lh-tight);
}
/* .chip[data-state="active"/"done"/"locked"] is gone: their only callers
   were the Simulation panel's four step chips, and the step rail
   (styles/portal.css) now carries that state instead. The BASE rule
   above stays — .wf-beta-tag is an alias of it and is still on screen in
   two places. If a chip ever needs to carry state again, it comes back
   here, not into a page sheet. */

.wf-beta-tag {
  display: inline-block;
  margin-left: 4px;
  vertical-align: 1px;
}

/* ============================================================
   RESULT — a label/figure list, one alignment for every row. Used for
   Simulation's per-step results once a completed step collapses to
   "result + EDIT".
   ============================================================ */
.result     { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2) 14px;
              font-size: var(--fs-hint); margin: 0; }
.result dt  { margin: 0; color: var(--text-3); text-transform: uppercase; letter-spacing: 0.08em; }
.result dd  { margin: 0; color: var(--text-1); font-variant-numeric: tabular-nums; }

/* ============================================================
   META — secondary facts (counts, extents, orientations) on ONE line
   instead of one row each. Step 1's "Locked at 51.51, -0.13 · 300×300 m"
   status line is ALREADY one line (a single setStatus() string, not
   separate DOM rows), so there is nothing to collapse there — this
   pattern is for other multi-row facts as they get flattened.
   Markup:  <p class="meta"><span><b>76</b> photos</span><i></i>…</p>
   ============================================================ */
.meta       { display: flex; flex-wrap: wrap; align-items: center;
              gap: var(--sp-1) 10px; margin: 0;
              font-size: var(--fs-hint); color: var(--text-3);
              text-transform: uppercase; letter-spacing: 0.04em; }
.meta b     { font-weight: 400; color: var(--text-1); font-variant-numeric: tabular-nums; }
.meta i     { flex: none; width: 1px; height: 10px; background: var(--line); }

/* ============================================================
   DELTA — a signed figure, coloured ONLY when the number carries
   direction. Its one caller is Analytic Mode's ANALYSIS card
   (js/viewer/panel/metric-select.js), which prints the change an edit
   made and always shows it: there is no state in which that card hides
   the difference, which is why nothing here needs a mode.
   ============================================================ */
.delta                    { font-variant-numeric: tabular-nums; text-align: right; }
/* RULE: better/worse, NOT cooler/warmer. This line says whether a change
   was an improvement, and improvement is not always cooling — a wall that
   sends less radiation into the street is BETTER, and it can be WARMER
   while doing it. Borrowing --delta-cooler for "better" would render
   improvement in a blue that reads as "colder", which is a different
   claim. The literal cooler/warmer tokens stay in tokens.css for a caller
   that genuinely means temperature. */
.delta[data-dir="better"] { color: var(--better); }
.delta[data-dir="worse"]  { color: var(--worse); }
.delta[data-dir="none"]   { color: var(--text-3); }

/* ============================================================
   SEG — a segmented control (isolate method, comparison selector, view
   mode).
   ============================================================ */
.seg        { display: flex; border: var(--border) solid var(--line-strong);
              border-radius: var(--radius); overflow: hidden; }
.seg button { flex: 1; padding: 7px 0; font-size: var(--fs-hint);
              letter-spacing: var(--ls-button); line-height: var(--lh-tight);
              text-align: center; cursor: pointer;
              background: transparent; color: var(--text-2); border: 0;
              text-transform: uppercase; }
.seg button + button             { border-left: var(--border) solid var(--line-strong); }
.seg button:hover:not(:disabled) { background: var(--bg-inset); color: var(--text-1); }
.seg button[aria-pressed="true"] { color: var(--accent); background: var(--accent-soft); }

/* Disabled — used by the criterion row, where a criterion is off until
   the step that produces it has run (js/viewer/metrics.js's
   CRITERIA[].ready). Same rule as .btn:disabled: a SHAPE difference (dashed
   separator) plus the off text colour, never a brightness change, so a live
   segment and a dead one are not two shades of the same thing.
   :hover above is guarded rather than overridden here — a disabled segment
   that still lit up on hover would read as pressable. */
.seg button:disabled                { color: var(--text-off); cursor: not-allowed; }
.seg button:disabled + button,
.seg button + button:disabled       { border-left-style: dashed;
                                      border-left-color: var(--line-off); }

/* ============================================================
   VIEWPORT RAIL — the right-side tool column. Same 34px box, same
   20px inset from its corner, in both the portal map and the
   Analytic viewer (js/ui/view-tools.js and js/viewer/tools.js
   each build their own instance of this DOM from string literals).

   .tool-btn / .toolbar / .tool-menu / .tool-menu-item are the design
   vocabulary's names. .vt-btn / .vt-root / .vt-popover / .vt-popover-btn
   stay as ALIASES — view-tools.js has 12 occurrences and tools.js has 6,
   all string literals building DOM at init, so renaming buys a different
   word at the cost of ~18 JS edits. Every .vt-* class name is unchanged;
   only declarations live here.
   ============================================================ */
.toolbar, .vt-root {
  position: absolute;
  top: var(--inset-overlay); right: var(--inset-overlay);
  display: flex; flex-direction: column; gap: var(--sp-4);
  align-items: center; z-index: var(--vt-z-index);
  /* The column's empty space must pass map drags through — .vt-root used
     to span inset:0 the whole viewport for this reason, and even docked to
     a corner the flex column below can still leave gaps between groups. */
  pointer-events: none;
}
.vt-col {
  display: flex; flex-direction: column; gap: var(--sp-2);
  align-items: center; pointer-events: none;
}
.vt-item { position: relative; }

.tool-btn, .vt-btn {
  width: var(--w-toolbtn); height: var(--w-toolbtn);
  display: flex; align-items: center; justify-content: center;
  background: var(--overlay-bg); color: var(--text-2); cursor: pointer;
  -webkit-backdrop-filter: var(--overlay-blur);
          backdrop-filter: var(--overlay-blur);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  padding: 0;
  pointer-events: auto; /* the column above is deliberately none */
}
.tool-btn svg, .vt-btn svg { width: var(--vt-icon-glyph-size); height: var(--vt-icon-glyph-size); }

/* The compass carries .vt-btn/.tool-btn as well and has no size/shape rules
   of its own — see the needle-fill comment further down, transform-origin
   is aligned to icons.js's 24x24 viewBox. */
.vt-compass-north { fill: #e2793a; stroke: none; }
.vt-compass-south { fill: #d8d8d8; stroke: none; }

.tool-btn:hover, .vt-btn:hover { color: var(--text-1); }

.tool-btn[aria-pressed="true"], .vt-btn.vt-active {
  border-color: var(--accent); color: var(--accent); background: var(--accent-soft);
}

/* Disabled is a shape difference — dashed + --text-off — never opacity. */
.vt-btn:disabled {
  border-style: dashed; border-color: var(--line-off);
  color: var(--text-off); cursor: not-allowed;
}

.vt-btn.vt-busy svg { animation: vt-spin 0.9s linear infinite; }
@keyframes vt-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .vt-btn.vt-busy svg { animation: none; }
}

/* Recording — the one place this column uses red: a running recording is
   the only state here with a cost to not noticing, so the colour carries
   the meaning on its own even when motion is reduced. */
.vt-btn.vt-recording {
  color: #ff4d4d; background: color-mix(in srgb, #ff4d4d 18%, transparent);
}
.vt-btn.vt-recording svg { animation: vt-rec-pulse 1.4s ease-in-out infinite; }
@keyframes vt-rec-pulse { 50% { opacity: 0.35; } } /* the flicker itself IS the signal, not hierarchy — kept */
@media (prefers-reduced-motion: reduce) {
  .vt-btn.vt-recording svg { animation: none; }
}

.tool-menu, .vt-popover {
  min-width: var(--vt-popover-width);
  /* RULE: vertical-only padding here is not enough. .tool-menu-item /
     .vt-popover-btn / .vt-mode-row each bake their own 8-12px horizontal
     padding into the item, for full-bleed hover, so a vertical-only inset
     works for them — but the basemap popover's content is
     #group-basemap-body moved in wholesale (plain <label> checkboxes and
     a bare slider with no padding of their own), and it would sit flush
     against the popover's left and right edges without a horizontal
     inset on the container itself. Same for the measure popover's two
     text lines and the bookmark popover's status line. The buttons' hover
     highlight stops var(--sp-3) short of the edge instead of touching it
     — a small, deliberate trade for content that isn't a button never
     being flush against the frame. */
  padding: var(--sp-2) var(--sp-3);
  background: var(--bg-panel);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  color: var(--text-2); overflow-wrap: break-word;
}
.vt-popover {
  position: absolute; top: 0; right: calc(100% + var(--sp-2));
  max-width: var(--vt-popover-width);
  font: var(--fs-label)/var(--lh-body) var(--font-mono);
  pointer-events: auto;
}
.vt-popover[hidden] { display: none; }
.vt-popover label { display: block; white-space: normal; cursor: pointer; }
.vt-popover { accent-color: var(--accent); }

.tool-menu-item, .vt-popover-btn {
  display: block; width: 100%; padding: 8px 12px; margin-bottom: var(--sp-2);
  font-size: var(--fs-label); font: inherit; color: var(--text-2);
  background: transparent; border: 0; border-radius: var(--radius);
  cursor: pointer; text-align: left;
}
.vt-popover-btn:last-child { margin-bottom: 0; }
.tool-menu-item:hover, .vt-popover-btn:hover { background: var(--bg-inset); color: var(--text-1); }
.tool-menu-item[aria-selected="true"] { background: var(--bg-inset); color: var(--text-1); }

.vt-mode-row {
  display: flex; align-items: center; justify-content: space-between;
  padding: 6px var(--sp-2); border-radius: var(--radius); cursor: pointer;
}
.vt-mode-row:hover { background: var(--bg-inset); }
.vt-mode-row.vt-active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
/* Dashed, not opacity:0.4 — same shape rule as every other disabled control. */
.vt-mode-row.vt-disabled { border: var(--border) dashed var(--line-off); color: var(--text-off); cursor: not-allowed; }
.vt-mode-row.vt-disabled:hover { background: none; }

/* .wf-beta-tag already covers this look (see the CHIP section above) —
   .vt-beta-tag only needs its own positioning, same pattern as that alias. */
.vt-beta-tag { display: inline-block; margin-left: var(--sp-2); vertical-align: 1px; }

/* Section headings inside the column — not interactive, so they inherit
   .vt-col's pointer-events:none and never opt back in (a label must never
   eat a map drag). */
.vt-section {
  box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  width: var(--w-toolbtn); height: calc(var(--w-toolbtn) / 3 + 5px);
  border-radius: var(--radius); background: var(--overlay-bg);
  font-size: var(--fs-micro); letter-spacing: var(--ls-micro); text-transform: uppercase;
  color: #575c5e; user-select: none;
}
.vt-sep { width: var(--w-toolbtn); height: 1px; margin: 10px 0 8px; background: var(--line); }

.vt-status { font-size: var(--fs-hint); color: var(--text-3); margin-top: var(--sp-2); }
.vt-status.vt-status-error { color: var(--accent); }

.vt-measure-total { font-size: var(--fs-title); font-weight: var(--fw-title); }
.vt-measure-note { font-size: var(--fs-eyebrow); color: var(--text-3); margin-top: var(--sp-1); }
.vt-measure-hint { font-size: var(--fs-hint); color: var(--text-3); }

/* ============================================================
   LEGEND — bottom-right, fixed 300px (not --panel-width: legend is
   legend, panel is panel). #legend-dock is the live id in markup, kept
   as an ALIAS alongside .legend.
   ============================================================ */
.legend, #legend-dock {
  width: 300px; padding: var(--sp-3) var(--sp-4);
  display: flex; flex-direction: column; gap: var(--sp-2);
  background: var(--overlay-bg-thin);
  -webkit-backdrop-filter: var(--overlay-blur);
          backdrop-filter: var(--overlay-blur);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
  color: var(--text-2);
}
.legend-head, .legend-dock-title {
  font-size: var(--fs-eyebrow); letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase; color: var(--text-3);
}
#legend-dock-body { display: flex; flex-direction: column; gap: var(--sp-2); }
#legend-dock-body .wf-legend { margin: 0; }

.legend-ramp, .wf-legend-bar {
  height: 8px; border-radius: var(--radius);
  border: var(--border) solid var(--line-strong);
  /* No background here on purpose: ui/legend.js writes the gradient inline,
     generated by the SAME function that colours the walls, so the legend
     can never disagree with what is on screen. A static --ramp-flux/-svf
     here would silently become a second source of truth. */
}
.legend-ramp[data-ramp="flux"] { background: var(--ramp-flux); } /* no caller — kept for spec completeness, see tokens.css */
.legend-ramp[data-ramp="svf"]  { background: var(--ramp-svf); }  /* no caller — kept for spec completeness, see tokens.css */

.legend-scale, .wf-legend-scale {
  display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2);
  font-size: var(--fs-eyebrow); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.wf-legend-caption { color: var(--text-3); text-align: center; white-space: normal; line-height: var(--lh-tight); }

.wf-legend-swatches { display: flex; flex-wrap: wrap; gap: var(--sp-1) 10px; }
.wf-legend-swatch { display: inline-flex; align-items: center; gap: var(--sp-1); font-size: var(--fs-hint); color: var(--text-2); }
.wf-legend-swatch-chip {
  width: 9px; height: 9px; border-radius: var(--radius);
  border: var(--border) solid rgb(0 0 0 / 0.25); flex-shrink: 0;
  /* background NOT set here either — ui/legend.js writes each swatch's
     material colour inline, same reasoning as .legend-ramp above. */
}

/* ============================================================
   MODE SWITCH — bottom-left, the only navigation inside the
   viewport. One line of text in an identical box across all three
   states so the corner never jumps.

     :disabled            dashed, --text-locked        → not yet
     [data-state=ready]    accent + arrow (+ one glow)  → next move
     [data-state=return]   --line-bright + white label  → any time

   .mode-switch is a REAL rename of .mode-btn (not an alias): the old
   name only ever had two states, and this control needs a third,
   [data-state] is the only clean way to say that in CSS. Renamed only
   once confirmed that no JS reads .mode-btn.
   ============================================================ */
.mode-switch {
  display: flex; align-items: center; justify-content: center;
  box-sizing: border-box;
  width: 100%; min-height: var(--h-mode); padding: 9px var(--sp-3);
  /* Same reason as .btn/.chip above: this element sits in .col-left as a
     SIBLING of .fpanel, never a descendant, so it never inherited that
     container's tightened line-height. */
  line-height: var(--lh-tight);
  font-size: var(--fs-label); letter-spacing: var(--ls-button);
  text-transform: uppercase; text-align: center; cursor: pointer;
  background: var(--overlay-bg); color: var(--text-1);
  -webkit-backdrop-filter: var(--overlay-blur);
          backdrop-filter: var(--overlay-blur);
  border: var(--border) solid var(--line-strong); border-radius: var(--radius);
}

/* locked: simulation incomplete, the default on first load. Deliberately
   NOT --text-off (the grey used for dead controls) — this is a destination
   the user is working towards, not a failed control, so it stays legible
   at --text-locked while the dashed border still says "not yet". */
.mode-switch:disabled {
  color: var(--text-locked); border-style: dashed; border-color: var(--line-locked);
  cursor: not-allowed;
}

/* ready: the run finished, entering Analytic is the next action */
.mode-switch[data-state="ready"] {
  color: var(--accent); border-color: var(--accent); background: var(--accent-fill);
}
.mode-switch[data-state="ready"]:hover { background: var(--accent-fill-hi); }
/* Added once, on the disabled->enabled transition; removed on animationend
   (workflow-controls.js). Two 900ms cycles, then silence — a permanent
   pulse becomes furniture, not a signal. */
.mode-switch--unlocked { animation: mode-unlock 900ms ease-out 2; }
@keyframes mode-unlock {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 138, 61, 0); }
  40%      { box-shadow: 0 0 0 3px rgba(255, 138, 61, 0.18); }
}
@media (prefers-reduced-motion: reduce) { .mode-switch--unlocked { animation: none; } }

/* return: available at ANY time, so never dashed and never dimmed — but
   never accent either, because going back is not the next step and would
   compete with whatever is live inside the panel. This is the system's
   fourth signal, reserved for this one control. */
.mode-switch[data-state="return"] { color: var(--text-1); border-color: var(--line-bright); }
.mode-switch[data-state="return"]:hover { background: var(--bg-inset); border-color: var(--text-2); }

/* ============================================================
   STATUS BAR — shared shell, 26px, same field order in both modes.
   Its three fields (step, grid count, photo count) and the dot's
   colour are all driven read-only from ui/workflow-controls.js's
   updateStatusBar() — see that function's own header comment for
   why it writes nothing back into `state`.
   ============================================================ */
.statusbar      { flex: none; height: var(--h-status); display: flex; align-items: center;
                  gap: var(--sp-4); padding: 0 var(--sp-4);
                  background: var(--bg-panel); border-top: var(--border) solid var(--line);
                  font-size: var(--fs-eyebrow); letter-spacing: 0.12em; color: var(--text-3); }
.statusbar-state{ display: flex; align-items: center; gap: var(--sp-2); color: var(--text-1); }

/* Base colour is the "idle" state (no step running, workflow not yet
   finished) — updateStatusBar() sets data-state to override it while a step
   is busy or once the whole workflow is done. Expressed as a data attribute,
   not an inline style: CSS owns appearance, JS owns behaviour. */
.statusbar-dot  { width: 5px; height: 5px; flex: none; background: var(--line-strong); }
.statusbar-dot[data-state="busy"] { background: var(--accent); }
.statusbar-dot[data-state="done"] { background: var(--done); }

.statusbar-sep    { width: 1px; height: 11px; background: var(--line); }
.statusbar b      { font-weight: 400; color: var(--text-2); font-variant-numeric: tabular-nums; }
.statusbar-spacer { flex: 1; }
/* Analytic Mode's key reminder, parked in the bar both modes share.

   RULE: HIDDEN HERE, REVEALED IN analytic.css. This sheet is loaded by
   both pages, so the default has to be the one that is true for the map
   — where a line of viewer key bindings would be describing a camera
   that page does not have. The reveal is one scoped rule in
   styles/analytic.css; putting the default there instead would leave the
   map showing it until that sheet loaded.

   One line, always: it sits between two flexible spacers with the
   attribution to its right, and a wrapped reminder would push the bar's
   height around as the fields beside it change width. It gives up
   characters rather than growing, the same bargain the strip over the
   scene used to make. */
.statusbar-hint {
  display: none;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* #575c5e is a deliberate one-off, not a missed token: one notch darker
   than --text-3, reserved for text that is always on screen but never
   worth reading twice — the attribution credit here is the only caller
   so far. */
.statusbar-credit { color: #575c5e; }
