/* FORGE workbench — the product's primary surface.
 *
 * # The layout decision this file exists to express
 *
 * The PRD's model of this product is "talk while looking at the thing"
 * (§1.2, §5.3). The previous version of this stylesheet did not say that: voice
 * was a strip at the bottom of a side rail, the same size and weight as a search
 * box, while an empty grey viewport held the middle of the screen. The interface
 * put its emphasis on the surface that had nothing in it yet.
 *
 * So the voice surface now has two placements and one implementation:
 *
 *   hero  — centred in the stage, full size. This is the state before there is
 *           anything to look at, and it is the whole screen's subject.
 *   dock  — the bottom-left corner, compact. The moment geometry exists, the
 *           geometry is the subject and the voice surface steps aside without
 *           going away.
 *
 * They are the SAME element with a different `data-place`, not two components
 * that must be kept in sync. Everything reachable in one placement is reachable
 * in the other — the microphone, the text box, hands-free, and stop-speaking —
 * because PRD AUD-06 requires a non-audio path for every critical interaction
 * and AUD-07 requires stop to be always reachable. A compact placement is not a
 * reduced one.
 *
 * Palette is FORGE's own (see shell.css). Cyan means active; nothing new is
 * invented here.
 */

.wb {
  display: grid;
  grid-template-rows: auto 1fr;
  /* dvh, not vh. On a phone `100vh` is the viewport with the browser's address
   * bar RETRACTED, which is taller than what is actually on screen when the page
   * loads — and this element also sets `overflow: hidden`, so the difference is
   * not scrolled to, it is cut off. The bottom of the workbench, which is where
   * the console bar and the section switcher live, was below the fold with no
   * way to reach it. `vh` first so a browser without `dvh` keeps today's
   * behaviour rather than losing the height declaration entirely. */
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  padding: 0;
  place-items: initial;

  /* How tall the console bar currently is, measured rather than guessed.
   *
   * Below the three-column breakpoint the console bar is fixed to the bottom of
   * the viewport, so everything that scrolls above it needs to end that far up.
   * Its height depends on what is in it — an attachment strip, a voice warning —
   * so a constant here would be wrong in exactly the states where being wrong
   * hides something. workbench.js measures it; this value is the fallback for
   * the frame before the first measurement, and for a browser with no
   * ResizeObserver. */
  --wb-dock-h: 132px;
}

.wb-top {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 18px;
  border-bottom: 1px solid var(--edge-solid);
  background: var(--panel);
  z-index: 5;
}
.wb-top .spacer { margin-left: auto; }
.wb-top .who { font-size: 12px; color: var(--ink-dim); }

/* Presence in the header. A button rather than a logo, because it opens the one
 * thing a person talking to an agent has most reason to want: the commitments it
 * is operating under. */
.whois {
  display: flex; align-items: center; gap: 11px;
  padding: 4px 9px 4px 4px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  color: inherit; font: inherit;
  cursor: pointer;
  text-align: left;
}
.whois:hover { border-color: var(--edge-solid); background: var(--surface-2); }
.whois:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }
.top-sigil { display: block; line-height: 0; width: 26px; height: 26px; }
.top-sigil svg, .top-sigil img { width: 100%; height: 100%; display: block; }
.whois-txt { display: grid; gap: 1px; }
.whois-txt .wordmark { margin: 0; }
.whois-txt .tag { font-size: 10.5px; color: var(--ink-dim); letter-spacing: .01em; }
@media (max-width: 900px) { .whois-txt .tag { display: none; } }

/* ---- the soul ----------------------------------------------------------- */

.soul {
  position: fixed;
  z-index: 40;
  top: 58px; left: 14px;
  width: min(560px, calc(100vw - 28px));
  max-height: calc(100vh - 84px);
  max-height: calc(100dvh - 84px);
  display: flex; flex-direction: column;
  background: var(--panel);
  border: 1px solid var(--edge-solid);
  border-radius: 14px;
  box-shadow: var(--shadow-lift);
}
.soul.hidden { display: none; }
.soul-head {
  display: flex; align-items: flex-start; gap: 12px;
  padding: 15px 16px 12px;
  border-bottom: 1px solid var(--edge-solid);
}
.soul-head b { font-size: 14px; }
.soul-head .ghost { margin-left: auto; }
.soul-sub { font-size: 11.5px; color: var(--ink-dim); margin-top: 3px; }
.soul-body { overflow-y: auto; padding: 6px 16px 16px; }

.vow { padding: 13px 0; border-bottom: 1px solid var(--edge-solid); }
.vow:last-child { border-bottom: 0; }
.vow-id {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 700; color: var(--ink-dim);
}
/* The immutable ones are marked in gold — the character's trim, and the colour
 * this interface already uses for a boundary. PRD RSN-04: critique intensity is
 * tunable, safety-critical dissent is not, and the difference must be visible. */
.vow.fixed .vow-id { color: var(--gold); }
.vow-text { margin-top: 5px; font-size: 13px; line-height: 1.55; }
.vow-why { margin-top: 5px; font-size: 12px; line-height: 1.5; color: var(--ink-dim); }

.wb-body {
  display: grid;
  grid-template-columns: 340px 1fr 320px;
  min-height: 0;
}

/* ---- the stage --------------------------------------------------------- */

.stage {
  position: relative;
  min-width: 0;
  min-height: 0;
  background:
    radial-gradient(900px 520px at 50% 42%, var(--stage-wash) 0%, transparent 70%),
    var(--stage-bg);
}
.stage canvas#canvas { width: 100%; height: 100%; display: block; cursor: grab; }
.stage canvas#canvas:active { cursor: grabbing; }

/* .hidden must win here. `.stage-empty { display: grid }` and
 * `.hidden { display: none }` have equal specificity, so load order decides —
 * and workbench.css loads after console.css, which left the empty-state text
 * painted over the rendered model. Stated explicitly rather than relying on
 * which stylesheet happens to come last. */
.stage-empty.hidden { display: none; }

/* Reserved for a RENDERER FAILURE, not for "nothing yet". The empty case is the
 * hero orb saying what to do; a viewport that cannot initialise is a different
 * fact and gets its own words at the top of the stage. */
.stage-empty {
  position: absolute; left: 50%; top: 18px;
  transform: translateX(-50%);
  max-width: 70%;
  display: block;
  text-align: center;
  padding: 10px 14px;
  border: 1px solid var(--bad-edge);
  border-radius: 9px;
  background: var(--bad-wash);
  color: var(--bad-ink);
  font-size: 12.5px; line-height: 1.6;
  pointer-events: none;
  z-index: 4;
}

/* The provenance banner. This is not chrome — PRD VIS-06 forbids a render from
 * implying manufacturability, structural adequacy, or compliance, and a
 * photorealistic image is persuasive precisely in proportion to how little it
 * has been checked. So the banner is always present when geometry is, it is
 * legible rather than a faint watermark, and it is not dismissible.
 *
 * It sits bottom-RIGHT because the docked voice surface takes bottom-left, and
 * the width is bounded so the two can never overlap rather than merely usually
 * not overlapping. */
.provenance {
  position: absolute; right: 12px; bottom: 12px; left: auto;
  width: clamp(250px, calc(100% - 350px), 470px);
  background: var(--warn-wash);
  border: 1px solid var(--warn-edge);
  border-radius: 11px;
  padding: 11px 14px;
  font-size: 12px;
  line-height: 1.55;
  color: var(--warn-ink);
  max-height: 40%;
  overflow-y: auto;
  backdrop-filter: blur(6px);
  z-index: 3;
}
.provenance b { color: var(--warn-ink-strong); }
.provenance ul { margin: 5px 0 0; padding-left: 17px; }

.viewctl {
  position: absolute; top: 12px; left: 12px;
  display: flex; flex-wrap: wrap; gap: 6px;
  max-width: calc(100% - 24px);
  z-index: 3;
}
.viewctl button {
  padding: 5px 11px;
  font-size: 11.5px;
  font-weight: 600;
  border: 1px solid var(--edge-solid);
  border-radius: 7px;
  background: color-mix(in srgb, var(--panel) 82%, transparent);
  color: var(--ink-dim);
  cursor: pointer;
}
.viewctl button:hover { color: var(--ink); border-color: var(--core-dim); }
.viewctl button[aria-pressed="true"] { color: var(--on-accent); background: var(--core); border-color: transparent; }

.sliders {
  position: absolute; top: 12px; right: 12px;
  background: color-mix(in srgb, var(--panel) 90%, transparent);
  border: 1px solid var(--edge-solid);
  border-radius: 11px;
  padding: 11px 13px;
  width: 176px;
  z-index: 3;
  backdrop-filter: blur(6px);
}
.sliders label { display: block; font-size: 11px; color: var(--ink-dim); margin: 0 0 4px; }
.sliders input[type=range] { width: 100%; margin-bottom: 10px; accent-color: var(--core); }

/* The viewport's own controls are meaningless with nothing loaded, and an
 * interface offering to section an empty scene is offering something that does
 * not work. Hidden until there is geometry, which is the same moment the voice
 * surface docks. */
.stage:not(.building) .viewctl,
.stage:not(.building) .sliders { display: none; }

/* ---- the voice surface ------------------------------------------------- */

.voice {
  position: absolute;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: min(640px, 86%);
  display: grid;
  /* minmax(0, 1fr), NOT the default `auto` column.
   *
   * An `auto` grid column refuses to shrink below its content's min-content
   * width. The control pill's min-content is ~309px — the mic, the two icon
   * buttons and the stop button, none of which can shrink — so in the 306px
   * dock the pill burst 16px through the card's rounded border and sat outside
   * it. The card looked broken because its contents were, in fact, outside it.
   *
   * The children get min-width:0 for the same reason one layer down: a grid
   * item's automatic minimum size is also min-content, so constraining the
   * column alone is not enough. */
  grid-template-columns: minmax(0, 1fr);
  justify-items: center;
  gap: 4px;
  z-index: 4;
  transition:
    left .7s cubic-bezier(.62,.03,.2,1),
    top .7s cubic-bezier(.62,.03,.2,1),
    width .7s cubic-bezier(.62,.03,.2,1),
    transform .7s cubic-bezier(.62,.03,.2,1);
}

/* The corner. Anchored to the stage's bottom-left; the translate carries the
 * inset so both properties animate rather than one snapping. */
.voice[data-place="dock"] {
  left: 0; top: 100%;
  width: 306px;
  transform: translate(14px, calc(-100% - 14px));
  justify-items: stretch;
  background: color-mix(in srgb, var(--panel) 88%, transparent);
  border: 1px solid var(--edge-solid);
  border-radius: 14px;
  padding: 10px 12px 11px;
  backdrop-filter: blur(8px);
}
.voice > * { min-width: 0; max-width: 100%; }

.orb {
  position: relative;
  width: min(420px, 62vh);
  width: min(420px, 62dvh);
  aspect-ratio: 1;
  transition: width .7s cubic-bezier(.62,.03,.2,1);
}
.orb canvas { width: 100%; height: 100%; display: block; }
.voice[data-place="dock"] .orb { width: 92px; justify-self: center; }

/* FORGE, inside her own aura.
 *
 * 60% of the box, which is exactly where orb.js draws the rim (r = 0.30 of the
 * box's shorter side), so the bright cyan edge lands on the portrait's own gold
 * border rather than near it. If one of those numbers changes the other has to.
 *
 * .forge-portrait supplies the circular crop and the trim; only the size and
 * placement are stated here, because the character's presentation belongs to
 * avatar.css and must not be re-specified per page. */
.orb-face {
  position: absolute;
  left: 50%; top: 50%;
  width: 60%; height: 60%;
  transform: translate(-50%, -50%);
  pointer-events: none;
}
.orb-face img { width: 100%; height: 100%; }
.orb-face .forge-portrait__badge {
  width: 22%; height: 22%;
  min-width: 20px; min-height: 20px;
  right: 2%; bottom: 2%;
}
.orb-face .forge-portrait__badge svg,
.orb-face .forge-portrait__badge img { width: 74%; height: 74%; }

/* Docked, the badge would be a smudge at 20px inside a 92px orb, and the state
 * word beside it already says the same thing in words. The portrait stays,
 * because presence is the point of keeping her on screen at all. */
.voice[data-place="dock"] .orb-face .forge-portrait__badge { display: none; }

/* What was last said. Cyan, quiet, and above the state word — the reference for
 * "you said this, and this is what I am doing about it". */
.voice-said {
  color: var(--core);
  font-size: 13.5px;
  text-align: center;
  min-height: 20px;
  max-width: 90%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.voice-said.partial { color: var(--ink-dim); font-style: italic; }
.voice[data-place="dock"] .voice-said { display: none; }

.voice-state {
  font-size: 30px;
  font-weight: 300;
  letter-spacing: .01em;
  color: var(--ink);
  margin: 2px 0 10px;
  transition: font-size .5s ease, margin .5s ease;
}
.voice[data-place="dock"] .voice-state {
  font-size: 12px;
  font-weight: 650;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 6px 0 7px;
  text-align: center;
}
.voice-state.thinking, .voice-state.listening { color: var(--core); }

/* The console bar.
 *
 * Everything you can do with a turn — talk, type, send, stop — is one object
 * rather than four floating shapes at four different sizes. That is the whole
 * fix: the previous row read as unrelated widgets that happened to be adjacent,
 * because it was.
 *
 * The bar is a single capsule with the controls as NODES inside it, divided by
 * hairlines rather than by gaps. Depth comes from a one-pixel top highlight and
 * a long soft drop shadow — the same two tricks the sliders and provenance
 * panels use, so the stage's surfaces all read as one material.
 */
.voice-ctl {
  display: flex;
  align-items: center;
  gap: 0;
  width: 100%;
  max-width: 520px;
  padding: 5px;
  border-radius: 999px;
  border: 1px solid var(--edge-solid);
  background:
    linear-gradient(180deg, var(--sheen) 0%, transparent 42%),
    color-mix(in srgb, var(--panel) 92%, transparent);
  box-shadow:
    inset 0 1px 0 var(--sheen),
    0 0 0 1px color-mix(in srgb, var(--core) 7%, transparent),
    var(--shadow-drop);
  backdrop-filter: blur(10px);
  transition: border-color .2s, box-shadow .2s;
}
.voice-ctl:focus-within {
  border-color: var(--core-dim);
  box-shadow:
    inset 0 1px 0 var(--sheen),
    0 0 0 1px color-mix(in srgb, var(--core) 22%, transparent),
    var(--shadow-drop);
}
.voice[data-place="dock"] .voice-ctl { max-width: none; padding: 4px; }

/* A hairline, not a gap. */
.ctl-div {
  width: 1px;
  align-self: stretch;
  margin: 6px 4px;
  background: var(--edge-solid);
  flex: none;
}

/* Every round control in the bar is the same node, so hover, focus and the
 * active state are defined once and cannot drift between them. */
.node {
  width: 38px; height: 38px; flex: none;
  border-radius: 50%;
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  display: grid; place-items: center;
  transition: color .16s, background .16s, border-color .16s, box-shadow .16s;
}
.node svg { width: 17px; height: 17px; display: block; }
.node:hover { color: var(--ink); background: var(--hover-wash); }
.node:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }
.node:disabled { opacity: .35; cursor: not-allowed; background: transparent; }
.voice[data-place="dock"] .node { width: 32px; height: 32px; }
.voice[data-place="dock"] .node svg { width: 15px; height: 15px; }

/* Push-to-talk. Held down, not toggled: a button you hold cannot be left on by
 * accident, which is the failure mode that turns an always-listening product
 * into a privacy incident. It is the only node that starts lit, because it is
 * the one this interface is actually for. */
.mic {
  color: var(--core);
  border-color: color-mix(in srgb, var(--core-dim) 70%, transparent);
  background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--core) 16%, transparent), color-mix(in srgb, var(--core) 4%, transparent));
}
.mic:hover { color: var(--core-hover); background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--core) 26%, transparent), color-mix(in srgb, var(--core) 7%, transparent)); }
.mic[aria-pressed="true"] {
  background: var(--core);
  color: var(--on-accent);
  border-color: transparent;
}
.mic.listening { box-shadow: 0 0 0 4px color-mix(in srgb, var(--core) 24%, transparent); }
.mic:disabled { opacity: .35; cursor: not-allowed; }

/* Send lights up only when there is something to send. `:placeholder-shown` does
 * the work, so there is no script and no class that can fall out of step with
 * the field's actual contents.
 *
 * Verified by SCREENSHOT, not by getComputedStyle. That distinction cost an hour:
 * in a headless/hidden document `getComputedStyle` returned the unlit colour for
 * this element while the rendered pixels were plainly cyan, which reads exactly
 * like a `:has()` support gap and is not one. Anything asserted about CSS here
 * has to be asserted against what was drawn. */
.send { color: var(--ink-dim); }
.voice-ctl form:has(input:not(:placeholder-shown)) .send {
  color: var(--core);
  background: color-mix(in srgb, var(--core) 10%, transparent);
}

/* Stop is present at all times (AUD-07) and brightens while FORGE is actually
 * speaking, which is when it means something. */
.voice-state.speaking ~ .voice-ctl .stop { color: var(--core); }

.voice-ctl form {
  flex: 1; min-width: 0;
  display: flex; align-items: center; gap: 4px;
}
.voice-ctl input[type=text] {
  flex: 1; min-width: 0;
  padding: 10px 8px 10px 14px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink); font: inherit; font-size: 13.5px;
  letter-spacing: .005em;
}
.voice-ctl input[type=text]::placeholder { color: color-mix(in srgb, var(--ink-dim) 78%, transparent); }
/* The ring belongs to the whole bar (:focus-within above), not to the field
 * inside it — two nested focus rings on one object looks like a mistake. */
.voice-ctl input[type=text]:focus { outline: none; }
.voice[data-place="dock"] .voice-ctl input[type=text] { padding: 7px 6px 7px 11px; font-size: 12.5px; }

/* Kept for the soul panel's close button, which is not a console node. */
.ghost {
  padding: 7px 13px;
  border: 1px solid var(--edge-solid);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font: inherit; font-size: 12px; font-weight: 600;
  cursor: pointer;
}
.ghost:hover { color: var(--ink); border-color: var(--core-dim); }

.voice-opts {
  display: flex; align-items: center; justify-content: center;
  gap: 14px; flex-wrap: wrap;
  margin-top: 11px;
  font-size: 11.5px; color: var(--ink-dim);
}
.voice-opts label { display: flex; align-items: center; gap: 7px; margin: 0; font-size: 11.5px; }

/* Switches, not the operating system's checkboxes.
 *
 * The native control renders in the OS accent colour — a hard blue square on a
 * navy panel — which is the one element on this screen that was not FORGE's.
 * The real input is kept and restyled rather than hidden behind a decorative
 * span, so the label association, the keyboard behaviour and the focus ring are
 * the browser's own and not a reimplementation. */
.sw { cursor: pointer; user-select: none; }
.sw input[type=checkbox] {
  appearance: none; -webkit-appearance: none;
  width: 30px; height: 16px; margin: 0; flex: none;
  border-radius: 999px;
  border: 1px solid var(--edge-solid);
  background: var(--well);
  position: relative;
  cursor: pointer;
  transition: background .18s, border-color .18s;
}
.sw input[type=checkbox]::after {
  content: "";
  position: absolute; top: 2px; left: 2px;
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--ink-dim);
  transition: transform .18s cubic-bezier(.5,0,.2,1), background .18s, box-shadow .18s;
}
.sw input[type=checkbox]:checked {
  background: color-mix(in srgb, var(--core) 20%, var(--well));
  border-color: color-mix(in srgb, var(--core-dim) 85%, transparent);
}
.sw input[type=checkbox]:checked::after {
  transform: translateX(14px);
  background: var(--core);
  box-shadow: 0 0 9px color-mix(in srgb, var(--core) 65%, transparent);
}
.sw input[type=checkbox]:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }
.sw:hover input[type=checkbox] { border-color: var(--core-dim); }
.sw input[type=checkbox]:checked + span { color: var(--ink); }
.voice[data-place="dock"] .voice-opts { margin-top: 8px; gap: 10px; font-size: 10.5px; }
.voice-opts .bad { color: var(--bad-ink); }

.pill {
  padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--edge-solid); font-size: 10.5px;
  letter-spacing: .05em; text-transform: uppercase; font-weight: 700;
}
.pill.live { border-color: var(--core-dim); color: var(--core); }
.pill.off { color: var(--ink-dim); }

/* ---- narrow: the console bar leaves the stage --------------------------
 *
 * Both placements collapse to one fixed bar above the section switcher, and
 * that is the point rather than a side effect.
 *
 * `hero` and `dock` are positions WITHIN the stage, and they work because on a
 * wide screen the stage is one of three things on screen at once. In a single
 * switched column the stage is one of three things you can be LOOKING at, and a
 * console bar that lives inside it would disappear whenever the reader opened
 * the conversation or the work rail — taking the microphone, the text field and
 * stop-speaking with it. AUD-06 requires a non-audio path at all times and
 * AUD-07 requires stop to be always reachable, so the bar has to outlive the
 * pane it used to sit in.
 *
 * It is presentation only: nothing here changes `data-place`, and stage.js still
 * decides which placement is in force exactly as it does on a wide screen. The
 * two placements simply render the same way at this width.
 */
@media (max-width: 1199px) {
  .voice[data-place="hero"],
  .voice[data-place="dock"] {
    /* Absolute within the stage, NOT fixed to the viewport.
     *
     * Both would look right, and absolute is the one that cannot go wrong. `.wb`
     * is `100dvh` with `overflow: hidden` and the stage is its middle row, so the
     * bottom of the stage IS the top of the section switcher — the anchor this
     * bar wants is already there, and using it means the bar's position is
     * derived from the layout rather than from a copy of the switcher's height
     * that would have to be kept in step with it.
     *
     * `fixed` also resolves against the initial containing block, which is the
     * real window and not always what the page has been laid out into. */
    left: 0; right: 0; top: auto;
    bottom: 0;
    width: auto;
    transform: none;
    z-index: 20;
    /* The wide layout animates between the two placements. There is no move to
     * animate here, and leaving the transition on made the bar slide across the
     * screen when the panel changed. */
    transition: none;
    justify-items: stretch;
    align-items: center;
    gap: 4px 10px;
    padding: 8px 12px 9px;
    border: 0;
    border-top: 1px solid var(--edge-solid);
    border-radius: 0;
    background: var(--panel);
    backdrop-filter: blur(8px);
    /* Her, then what she is doing, then the controls. Two columns so the orb
     * and the state words share a line instead of stacking into a third of the
     * screen. */
    grid-template-columns: 40px minmax(0, 1fr);
  }
  .voice[data-place="hero"] .orb,
  .voice[data-place="dock"] .orb { width: 40px; grid-column: 1; grid-row: 1 / 3; }
  .voice[data-place="hero"] .voice-said,
  .voice[data-place="dock"] .voice-said {
    display: block;
    grid-column: 2; grid-row: 1;
    justify-self: start;
    max-width: 100%;
    text-align: left;
    font-size: 12px;
    min-height: 0;
  }
  .voice[data-place="hero"] .voice-state,
  .voice[data-place="dock"] .voice-state {
    grid-column: 2; grid-row: 2;
    justify-self: start;
    margin: 0;
    text-align: left;
    font-size: 11px;
    font-weight: 650;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--ink-dim);
  }
  .voice[data-place="hero"] .voice-ctl,
  .voice[data-place="dock"] .voice-ctl { grid-column: 1 / -1; grid-row: 3; max-width: none; }
  .voice[data-place="hero"] .voice-opts,
  .voice[data-place="dock"] .voice-opts { grid-column: 1 / -1; grid-row: 4; margin-top: 4px; }
  /* The badge is a smudge at this size and the state word beside it already
   * says the same thing in words. Same rule the wide dock uses. */
  .voice[data-place="hero"] .orb-face .forge-portrait__badge { display: none; }

  .provenance { left: 12px; right: 12px; width: auto; bottom: calc(var(--wb-dock-h) + 12px); }
}

/* ---- narrow: the stage's own floating controls -------------------------
 *
 * `.viewctl` wraps to two or three rows at this width, and `.sliders` and
 * `.states` were both anchored into the band it wraps into — all three were
 * pinned near `top: 12px`, so at 375px the transparency sliders sat on top of
 * the view buttons. Nothing was unreachable in theory and nothing was usable in
 * practice.
 *
 * They go side by side underneath instead, each half the width, which is the
 * only arrangement where the count of panels showing does not change where any
 * of them is: `.states` is present only for an assembly that HAS states, so
 * anything stacked in DOM order would move under it. */
@media (max-width: 700px) {
  .viewctl { max-width: calc(100% - 24px); }
  .states,
  .sliders {
    top: 112px; bottom: auto;
    width: calc(50% - 18px);
    max-height: 45%;
    overflow-y: auto;
  }
  .states  { left: 12px; right: auto; }
  .sliders { left: auto; right: 12px; }
}

/* ---- rails ------------------------------------------------------------- */

.rail { overflow-y: auto; min-height: 0; padding: 14px; }
.wb-left {
  border-right: 1px solid var(--edge-solid);
  background: var(--surface-1);
  display: grid; grid-template-rows: auto 1fr;
  padding: 0; min-height: 0;
}
.wb-right { border-left: 1px solid var(--edge-solid); background: var(--surface-1); }

.railhead {
  padding: 13px 16px 9px;
  font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
  color: var(--ink-dim); font-weight: 650;
}

.transcript { overflow-y: auto; padding: 0 14px 16px; min-height: 0; }

/* Turns are cards rather than a wall of text: a spoken conversation replayed as
 * an unbroken column is unreadable at a glance, and glancing is what this rail
 * is for while someone is looking at the middle of the screen. */
.turn {
  margin-bottom: 10px;
  font-size: 13.5px; line-height: 1.6;
  padding: 11px 13px;
  border-radius: 11px;
  border: 1px solid var(--edge-solid);
  background: var(--surface-3);
}
.turn.you { background: var(--surface-2); }
.turn.forge { border-color: color-mix(in srgb, var(--core-dim) 45%, var(--edge-solid)); }
.turn .lbl {
  font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  font-weight: 700; margin-bottom: 5px;
}
.turn.you .lbl { color: var(--ink-dim); }
.turn.forge .lbl { color: var(--core); }
.turn .body { color: var(--ink); }
.turn .detail {
  margin-top: 8px; padding: 9px 11px;
  background: var(--surface-4); border-radius: 8px;
  font-size: 12.5px; color: var(--ink-soft); white-space: pre-wrap;
}
.turn.partial { opacity: .75; }
.turn.partial .body { color: var(--ink-dim); font-style: italic; }

/* Recalled figures. Gold, because gold is this interface's boundary colour —
 * the sigil's ring, the immutable commitments in the soul panel, and now the
 * edge of what FORGE actually knows. Distinct from the red of a failure: a
 * quoted standard is usually right, and styling it as an error would train
 * people to dismiss it. */
.recalled {
  margin-top: 9px;
  padding: 9px 11px;
  border: 1px solid color-mix(in srgb, var(--gold) 45%, transparent);
  border-radius: 8px;
  background: color-mix(in srgb, var(--gold) 7%, transparent);
  font-size: 12px;
  line-height: 1.5;
}
.recalled .rc-h {
  font-size: 10px; letter-spacing: .09em; text-transform: uppercase;
  font-weight: 700; color: var(--gold);
}
.recalled .rc-i { margin-top: 6px; color: var(--warn-ink); }
.recalled .rc-i b { color: var(--warn-ink-strong); }
.recalled .rc-q {
  margin-top: 3px;
  font-size: 11.5px; line-height: 1.45;
  color: var(--ink-dim);
  font-style: italic;
}
/* A claim from a typed field is a VALUE, not a sentence somebody wrote, so it is
 * set upright and monospaced rather than in the italic used for quoted prose.
 * The two must not look alike: "42.3 mm" in a parameter and "42.3 mm" in a
 * sentence are different kinds of claim, and the panel's whole job is telling
 * kinds of claim apart. */
.recalled .rc-q.rc-field {
  font-style: normal;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  font-size: 11px;
  color: var(--warn-ink);
}
/* How a derived figure got here: the expression, and the parameters it rests
 * on. Quieter than the value itself — it is the answer to "where did that come
 * from", which is asked second. */
.recalled .rc-v {
  margin-top: 2px;
  font-size: 11px; line-height: 1.4;
  color: var(--ink-dim);
}
.recalled .rc-w { font-size: 11px; color: var(--ink-dim); }
.recalled .rc-f { margin-top: 7px; font-size: 11.5px; color: var(--ink-dim); }

/* The parameter panel (waves 10, 11).
 *
 * Set inside a variant row rather than in a modal: the numbers belong to THAT
 * variant, and a dialog floating over the rail loses which one it is editing the
 * moment a second variant appears. */
.variant .params {
  margin-top: 8px; padding: 8px 10px;
  border: 1px solid var(--edge-solid); border-radius: 6px;
  background: var(--stripe);
  font-size: 12px;
}
.variant .params.hidden { display: none; }
.params .p-h {
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
  font-weight: 700; color: var(--ink-dim); margin: 4px 0 5px;
}
.params .p-t { width: 100%; border-collapse: collapse; }
.params .p-t td { padding: 2px 6px 2px 0; vertical-align: top; }
.params .p-n { font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); }
.params .p-t input {
  width: 8em; padding: 2px 5px;
  background: var(--bg); color: var(--ink);
  border: 1px solid var(--edge-solid); border-radius: 4px;
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace); font-size: 12px;
}
.params .p-u { color: var(--ink-dim); width: 3em; }
.params .p-w { color: var(--ink-dim); font-size: 11px; }
/* A recalled figure carries the same gold the provenance banner uses, so the
 * label means the same thing in both places. */
.params .p-rec { color: var(--gold); font-weight: 700; }
.params .p-e {
  font-family: var(--mono, ui-monospace, SFMono-Regular, Menlo, monospace);
  color: var(--ink-dim);
}
.params .p-y { font-family: inherit; font-size: 11px; opacity: .8; margin-top: 1px; }
.params .p-a { margin-top: 8px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.params .p-msg { margin-top: 6px; font-size: 11.5px; color: var(--ink-dim); }
/* Caveats are the one thing here that must not read as decoration: they say what
 * re-deriving surfaced that the original never showed. */
.params .p-cav { color: var(--warn-ink); }
.params .p-cav ul { margin: 4px 0 0; padding-left: 18px; }
.params .p-cav li { margin-bottom: 3px; }

/* The epistemic ledger. Deliberately quieter than the recalled-figures block:
 * it is a standing record rather than a warning, and styling every turn's
 * ledger as an alarm would make the actual alarm invisible. */
.claims {
  margin-top: 9px;
  padding: 9px 11px;
  border: 1px solid var(--edge-solid);
  border-radius: 8px;
  background: var(--surface-4);
  font-size: 12px;
}
.claims .cl-h {
  font-size: 10px; letter-spacing: .09em; text-transform: uppercase;
  font-weight: 700; color: var(--ink-dim); margin-bottom: 6px;
}
.claims .cl-i {
  display: grid;
  grid-template-columns: 74px 1fr;
  gap: 4px 9px;
  padding: 4px 0;
  border-top: 1px solid var(--edge-solid);
  line-height: 1.45;
}
.claims .cl-i:first-of-type { border-top: 0; }
.claims .cl-t {
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  font-weight: 700; padding-top: 2px;
}
.claims .cl-ok  .cl-t { color: var(--ok); }
.claims .cl-warn .cl-t { color: var(--gold); }
.claims .cl-dim .cl-t { color: var(--ink-dim); }
.claims .cl-s { color: var(--ink); }
.claims .cl-src { grid-column: 2; font-size: 11px; color: var(--ink-dim); }

/* ---- parts list -------------------------------------------------------- */

.part {
  display: flex; gap: 9px; align-items: baseline;
  padding: 9px 10px; border-radius: 9px; cursor: pointer;
  font-size: 12.5px;
  border: 1px solid transparent;
}
.part:hover { background: var(--surface-hover); }
.part[aria-current="true"] { background: var(--surface-hover); border-color: var(--core-dim); }
.part .sw { width: 11px; height: 11px; border-radius: 3px; flex: none; }
.part .nm { flex: 1; min-width: 0; }
.part .dim { color: var(--ink-dim); font-size: 11px; margin-top: 2px; }

.h {
  font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--core); font-weight: 650; margin: 0 0 10px;
}
.h + .h { margin-top: 20px; }

/* ---- proposed work ----------------------------------------------------- */

/* A proposal is a proposal until a person acts on it. The card carries the
 * plan, the button, and — after it is started — what still has to be true for
 * anything to actually run. PRD AGT-08: proposed, planned and running are
 * distinct states and this card never blurs them. */
.proposal {
  border: 1px solid var(--edge-solid);
  border-radius: 11px;
  padding: 14px;
  background: var(--surface-3);
}
.proposal.armed { border-color: var(--warn); background: color-mix(in srgb, var(--warn) 7%, var(--surface-3)); }
.proposal.live { border-color: var(--core-dim); background: color-mix(in srgb, var(--core) 6%, var(--surface-3)); }
.proposal .tier {
  display: inline-block;
  font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--warn); font-weight: 700;
}
.proposal.live .tier { color: var(--core); }
.proposal .ttl { margin-top: 6px; font-weight: 650; font-size: 13.5px; line-height: 1.4; }
.proposal .say { margin-top: 6px; font-size: 12.5px; line-height: 1.55; color: var(--ink-dim); }
/* The industry a NEW project will be created in (PRD §"Domain packs").
 *
 * Every value below is taken from a control that already exists on this page —
 * the label and select from `.states`, the explanatory line from
 * `.proposal .foot` — rather than chosen fresh. A second visual vocabulary for
 * the same kind of control is how a surface stops looking like one product.
 *
 * It lives in its own side-panel section rather than on the proposal card: a
 * project is created by the first KEPT VARIANT, which happens before any work is
 * proposed, so a control that only appeared with the proposal arrived too late
 * to decide anything. */
#industry .industry { margin-top: 2px; }
/* Air before the next section. `.h` carries no top margin except after another
 * `.h`, so a section whose last element is a PARAGRAPH runs straight into the
 * heading below it. Fixed on this section rather than on `.h`, which every
 * other panel already lays out correctly. */
#industry { margin-bottom: 14px; }
#industry .industry select {
  width: 100%;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--edge-solid);
  border-radius: 6px;
  padding: 4px;
}
/* The boundary line under the control. Same values as `.proposal .foot`, which
 * is where this text lived before the picker moved out of the proposal card —
 * copied rather than re-selected because the two are different sections and a
 * shared selector would tie their spacing together for no reason. */
/* The settled domain: a statement rather than a control. Uses `.tier`'s
 * treatment from the proposal card — small, uppercase, coloured — because it is
 * the same kind of fact: what this work is being done under. */
#industry .settled {
  /* `.proposal .ttl`'s treatment, not `.h`'s. This is the VALUE under the
   * "Domain" heading — what this project is worked as — and giving it the
   * heading's uppercase-and-core styling stacked two headings on top of each
   * other, which reads as a mistake even when the words differ. */
  font-weight: 650; font-size: 13.5px; line-height: 1.4;
}
/* A raised ceiling is marked, because it is the one state where the limit is not
 * the build's own. Warn rather than core: it rests on a claim nothing verified. */
#industry .foot.raised {
  color: var(--warn);
  border-top: 1px solid var(--edge-solid);
  padding-top: 8px;
}
/* Who is in this project. The row shape follows `.proposal .steps li` — a flex
 * row with the name taking the slack and the status at the end — because it is
 * the same kind of list and a second layout for it would be a second thing to
 * keep looking right. */
#members { margin-bottom: 14px; }
.members { margin: 0; padding: 0; list-style: none; }
.members li {
  display: flex; gap: 8px; align-items: center;
  padding: 7px 0; border-top: 1px solid var(--edge-solid);
  font-size: 12.5px;
}
.members li:first-child { border-top: none; }
.members .who { flex: 1; min-width: 0; }
.members .addr {
  color: var(--ink-dim); font-size: 11px; margin-top: 2px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The caller's own row, marked so somebody can find themselves in a long list
 * without matching their own name. */
.members .you {
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--core); margin-left: 5px;
}
.members .role { color: var(--ink-dim); font-size: 11.5px; }
.members select, .member-add select, .member-add input[type=text] {
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--edge-solid);
  border-radius: 6px;
  padding: 3px 5px;
  font: inherit;
  font-size: 11.5px;
}
.member-add {
  display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center;
}
.member-add input[type=text] { flex: 1; min-width: 130px; padding: 5px 7px; font-size: 12.5px; }
.member-add input[type=text]:focus, .members select:focus { outline: none; border-color: var(--core-dim); }
#members .foot {
  margin-top: 8px; font-size: 11.5px; line-height: 1.55; color: var(--ink-dim);
}

/* Recording an authority. Inputs borrow `.voice-ctl input[type=text]`'s
 * treatment and the buttons are the page's own `.btn-sm`, so nothing new is
 * invented for a form that appears in one place. */
#industry .authority-act { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
#industry .authority-form { margin-top: 10px; }
#industry .authority-form label {
  display: block;
  font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--ink-dim);
  margin: 8px 0 4px;
}
#industry .authority-form input[type=text] {
  width: 100%;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--edge-solid);
  border-radius: 6px;
  padding: 5px 7px;
  font: inherit;
  font-size: 12.5px;
}
#industry .authority-form input[type=text]:focus {
  outline: none;
  border-color: var(--core-dim);
}
#industry .foot {
  margin-top: 10px;
  font-size: 11.5px;
  line-height: 1.55;
  color: var(--ink-dim);
}

.proposal .acts { display: flex; gap: 8px; margin-top: 12px; flex-wrap: wrap; }
.proposal .foot { margin-top: 10px; font-size: 11.5px; line-height: 1.55; color: var(--ink-dim); }
.proposal .foot code {
  font-size: 11px; background: var(--surface-4); padding: 1px 5px; border-radius: 4px;
  display: inline-block; margin-top: 3px;
}
.proposal .steps { margin: 10px 0 0; padding: 0; list-style: none; }
.proposal .steps li {
  display: flex; gap: 8px; align-items: baseline;
  padding: 6px 0; border-top: 1px solid var(--edge-solid);
  font-size: 12px; line-height: 1.45;
}
.proposal .steps li .rt {
  flex: none; font-size: 10px; font-weight: 700; letter-spacing: .06em;
  text-transform: uppercase; color: var(--ink-dim); min-width: 18px;
}
.proposal .steps li .gate { flex: none; font-size: 10px; color: var(--warn); font-weight: 700; }
.proposal .note { margin-top: 10px; }

/* ---- variants and side-by-side (PRD VIS-04) ----------------------------- */

/* A variant row is a PROPOSAL WITH PROVENANCE, not a thumbnail. The things that
 * decide between two designs — what units it is in, whether anything checked it,
 * whether a person ruled on it — are on the row, because a list that shows only
 * names makes the reader open each one to find out they are not comparable. */
.variant {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 9px 10px; border-radius: 9px;
  font-size: 12.5px;
  border: 1px solid transparent;
}
.variant:hover { background: var(--surface-hover); }
.variant.picked { background: var(--surface-hover); border-color: var(--core-dim); }
.variant .nm { flex: 1; min-width: 0; }
.variant .meta { color: var(--ink-dim); font-size: 11px; margin-top: 3px; line-height: 1.5; }
/* An unstated unit is called out rather than left blank: a blank reads as
 * millimetres to anyone who has been looking at millimetres. */
.variant .meta .nounit { color: var(--warn); }
.variant .acts { display: flex; gap: 7px; margin-top: 7px; flex-wrap: wrap; }
.variant .acts a, .variant .acts button {
  font-size: 11px; padding: 3px 8px; border-radius: 6px;
  border: 1px solid var(--edge-solid); background: var(--surface-2); color: var(--ink-dim);
  text-decoration: none; cursor: pointer;
}
.variant .acts a:hover, .variant .acts button:hover { color: var(--ink); border-color: var(--core-dim); }
/* A format this deployment cannot write. Present on purpose — a person who
 * cannot find STEP concludes it was forgotten — and visibly not a thing to
 * click, with the server's own reason on hover. */
.variant .acts button.unavail {
  opacity: .45; cursor: not-allowed; border-style: dashed;
}
.variant .acts button.unavail:hover { color: var(--ink-dim); border-color: var(--edge-solid); }

.variant-bar { display: flex; gap: 8px; align-items: center; margin: 10px 0 4px; flex-wrap: wrap; }
.variant-bar .node { flex: none; }
.variant-bar .hint { font-size: 11px; color: var(--ink-dim); flex: 1 1 100%; }

/* The export label. Shown BEFORE the download link is used, never after: a
 * label a person reads on their way out of the page is a label they have
 * already acted without. */
.exportlabel {
  margin-top: 8px; padding: 9px 10px; border-radius: 8px;
  background: var(--surface-4); border: 1px solid var(--edge-solid);
  font-size: 11px; line-height: 1.55; color: var(--ink-dim);
}
.exportlabel b { color: var(--warn-ink-strong); }
.exportlabel ul { margin: 4px 0 8px; padding-left: 16px; }
.exportlabel .go { display: inline-block; margin-top: 2px; color: var(--core); }

.compare {
  position: fixed; inset: 0; z-index: 60;
  background: var(--well);
  display: flex; flex-direction: column;
}
.compare.hidden { display: none; }
.compare-head {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 18px; border-bottom: 1px solid var(--edge-solid);
}
.compare-title { font-size: 13px; font-weight: 650; flex: 1; }
.compare-body { flex: 1; overflow: auto; padding: 16px 18px 28px; }

/* One column per variant, side by side, each with its own viewport and its own
 * provenance directly beneath it. The provenance is NOT collected into a single
 * table below the row: a fact about column 2 has to be next to column 2, or the
 * reader is matching indices instead of comparing designs. */
.cmp-cols { display: flex; gap: 14px; align-items: stretch; overflow-x: auto; padding-bottom: 6px; }
.cmp-col { flex: 1 1 0; min-width: 250px; display: flex; flex-direction: column; }
.cmp-col .cap { font-size: 12px; font-weight: 650; margin-bottom: 6px; }
.cmp-col .cap .n { color: var(--core); margin-right: 6px; }
/* The slot is sized, not the canvas: the canvas element is pooled and moved
 * between openings (see workbench.js), so its dimensions have to come from
 * wherever it currently lives rather than from itself. */
.cmp-view { height: 230px; }
.cmp-view canvas {
  width: 100%; height: 100%; display: block;
  border-radius: 10px; border: 1px solid var(--edge-solid); background: var(--well);
}
.cmp-col .prov { margin-top: 8px; font-size: 11px; line-height: 1.6; color: var(--ink-dim); }
.cmp-col .prov div { padding: 2px 0; }
.cmp-col .prov .k { color: var(--ink-dim); }
.cmp-col .prov .differs { color: var(--warn-ink-strong); }

.cmp-note { font-size: 11.5px; color: var(--ink-dim); line-height: 1.6; margin: 4px 0 14px; }

.cmp-table { width: 100%; border-collapse: collapse; font-size: 12px; margin-top: 18px; }
.cmp-table th, .cmp-table td {
  text-align: left; padding: 7px 10px; border-top: 1px solid var(--edge-solid);
  vertical-align: top;
}
.cmp-table th { color: var(--core); font-weight: 650; font-size: 11px;
  letter-spacing: .06em; text-transform: uppercase; }
/* A marker, not a colour alone: this table gets screenshotted into incident
 * notes and pasted into terminals, and colour survives neither. */
.cmp-table tr.differs td:first-child::before { content: "≠ "; color: var(--warn-ink-strong); }
.cmp-table td.absent { color: var(--warn); }
.cmp-table .why { display: block; margin-top: 3px; color: var(--ink-dim); font-size: 11px; line-height: 1.5; }
.cmp-table .diffs { margin: 5px 0 0; padding-left: 16px; color: var(--ink-dim); font-size: 11px; }

/* Kept apart from the differences on purpose. "These differ" and "these could
 * not be compared" are different answers, and only one of them is a finding. */
.cmp-uncompared {
  margin-top: 18px; padding: 11px 13px; border-radius: 9px;
  border: 1px solid var(--warn); background: color-mix(in srgb, var(--warn) 7%, var(--surface-3));
  font-size: 11.5px; line-height: 1.6;
}
.cmp-uncompared b { color: var(--warn); }
/* A qualified judgement, not a withheld one — dimmer than the box beside it,
 * because it says the comparison was made and how, rather than that it was not
 * made at all. */
.cmp-uncompared.matched { border-color: var(--edge-solid); background: var(--surface-3); }
.cmp-uncompared.matched b { color: var(--warn-ink-strong); }
.cmp-uncompared ul { margin: 5px 0 0; padding-left: 17px; }

/* Engineering overlays (PRD VIS-03).
 *
 * The layer never takes pointer events: the model underneath is draggable, and
 * a dimension that swallows a drag makes the viewport feel broken in a way
 * nobody attributes to the label they were nowhere near.
 */
.dimlayer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

/* Every selector below is scoped to the layer. `.dim` is already the parts
 * panel's class for a part's dimensions (see .part .dim above), and a bare
 * rule here absolutely-positioned every one of them — the panel collapsed into
 * a pile the moment overlays shipped. Specificity did not save it: .part .dim
 * overrides colour and size and says nothing about position or border, so the
 * properties it does not mention still applied. */
.dimlayer .dim {
  position: absolute;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 3px 7px;
  border-radius: 5px;
  white-space: nowrap;
  font-size: 11.5px;
  line-height: 1.3;
  /* Derived by default: muted, dashed border, and it reads as an annotation
   * rather than as a fact about a part. The solid variant below is the
   * exception, not the base case — a number is FORGE's own arithmetic until
   * something says otherwise. */
  background: var(--chip-bg);
  border: 1px dashed var(--edge-solid);
  color: var(--ink-dim);
}

/* Stated: it came from outside FORGE, so it is drawn the way a real dimension
 * is — solid, brighter, and carrying the word for where it came from. */
.dimlayer .dim-stated {
  border-style: solid;
  border-color: var(--core-dim);
  color: var(--ink-soft);
  background: var(--chip-bg);
}

.dimlayer .dim-label {
  font-size: 10.5px;
  letter-spacing: .04em;
  text-transform: uppercase;
  opacity: .72;
}

.dimlayer .dim-value { font-variant-numeric: tabular-nums; }
.dimlayer .dim-value b { font-weight: 600; }

/* The provenance chip. Not decoration: VIS-03 is "overlays WITHOUT confusing
 * appearance with validated data", and a number floating over a render with no
 * statement of where it came from is that confusion exactly. */
.dimlayer .dim-src, .dimlayer .dim-model {
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 1px 5px;
  border-radius: 3px;
}
.dimlayer .dim-src   { background: var(--badge-src-bg); color: var(--core); }
.dimlayer .dim-model { background: var(--surface-hover); color: var(--ink-dim); }

/* Assembly states (PRD VIS-02). Sits under the view buttons, above the
 * sliders — it changes WHAT is shown, where the sliders change how. */
.states {
  position: absolute;
  top: 52px;
  left: 14px;
  z-index: 3;
  width: 210px;
  padding: 10px 12px;
  border-radius: 9px;
  background: var(--chip-bg);
  border: 1px solid var(--edge-solid);
}

.states label {
  display: block;
  font-size: 10.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 5px;
}

.states select {
  width: 100%;
  background: var(--surface-2);
  color: var(--ink);
  border: 1px solid var(--edge-solid);
  border-radius: 6px;
  padding: 4px;
}

/* What the state claims, next to the control that applies it. */
.statenote {
  margin: 7px 0 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-dim);
}

/* The material line in the parts panel. Its provenance is set apart rather than
 * run into the name, so "aluminium 6061" and "FORGE guessed" cannot be read as
 * one phrase. */
.part .dim.mat i {
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  padding: 1px 4px;
  margin-left: 4px;
  border-radius: 3px;
  background: var(--surface-hover);
  color: var(--ink-dim);
}

/* Attaching a sketch (PRD VIS-01). The control sits in the composer beside
 * Send, because a picture is another way of saying the same thing rather than
 * a separate mode. */
.node.attach { cursor: pointer; }
.node.attach svg { width: 17px; height: 17px; }

.attached {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-top: 8px;
}

.att { position: relative; display: inline-flex; }

.att img {
  width: 46px;
  height: 46px;
  object-fit: cover;
  border-radius: 7px;
  border: 1px solid var(--edge-solid);
}

/* Removing an attachment has to be possible before it is sent: an image
 * attached by mistake is otherwise on its way to a model with no way back. */
.att button {
  position: absolute;
  top: -6px;
  right: -6px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  border: 1px solid var(--edge-solid);
  background: var(--surface-2);
  color: var(--ink-dim);
  font-size: 12px;
  line-height: 1;
  cursor: pointer;
}

.attproblem { font-size: 11.5px; color: var(--bad-ink); }

/* Recorded requirements a turn can be built from (PRD VIS-01). */
.req {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 6px 0;
  font-size: 12.5px;
  cursor: pointer;
  border-bottom: 1px solid var(--edge-solid);
}

.req span { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.req b { font-weight: 600; color: var(--ink); }

/* The kind is set apart because "requirement" and "constraint" mean different
 * things to what gets built, and running it into the title reads as part of it. */
.req i {
  font-style: normal;
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-dim);
}

/* ---- the stage's panels (PRD WRK-01) ------------------------------------
 *
 * The stage used to be one thing — the model — and WRK-01 asks it to be the
 * shared canvas: code, previews, diagrams, telemetry, requirements, diffs,
 * simulations, test results. These are the other panels.
 *
 * The layout rule that matters: the model layer is ALWAYS laid out. It is
 * hidden with `visibility`, never `display`, because a display:none ancestor
 * collapses the canvas's client box to zero and the WebGL viewport sizes itself
 * from that box. Switching away and back would return a 640x480 fallback
 * viewport with the wrong aspect. The other panels are plain content and are
 * display:none when inactive, which is what keeps them out of the tab order and
 * out of the accessibility tree.
 *
 * The voice surface is NOT a panel and is never hidden by one. It lives beside
 * them in the stage, and every panel docks it (see stage.js's onPanel), because
 * AUD-06 requires a non-audio path at all times and AUD-07 requires stop to
 * always be reachable — including while somebody is reading a diff.
 */

.stagebar {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 44px;
  /* Queried on its OWN width, not the window's. The stage is a column in a
   * three-column layout, so a wide window can still leave this bar narrow — and
   * a viewport media query hid the gloss on a phone while squeezing it to four
   * characters on a desktop, which is the case that actually happens. */
  container-type: inline-size;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 12px;
  background: color-mix(in srgb, var(--stage-bg) 88%, transparent);
  border-bottom: 1px solid var(--edge-solid);
  backdrop-filter: blur(6px);
  z-index: 6;
}

.stagetabs { display: flex; gap: 4px; flex: 0 0 auto; }

.stagetab {
  padding: 5px 12px;
  font-size: 12px;
  font-weight: 600;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.stagetab:hover { color: var(--ink); background: var(--hover-wash); }
.stagetab:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }
.stagetab[aria-selected="true"] {
  color: var(--on-accent);
  background: var(--core);
  border-color: transparent;
}

/* A panel this build cannot fill is marked BEFORE it is opened, so that opening
 * it is a choice to read why rather than a discovery that it is blank. It stays
 * fully operable — the explanation is the content, and a disabled tab would hide
 * the one thing the panel exists to say. */
.stagetab--empty::after {
  content: "";
  display: inline-block;
  width: 5px; height: 5px;
  margin-left: 6px;
  vertical-align: middle;
  border-radius: 50%;
  background: var(--ink-dim);
}
.stagetab--empty[aria-selected="true"]::after { background: var(--on-accent); }

.stagegloss {
  font-size: 11.5px;
  color: var(--ink-dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The gloss goes when the tabs need the room. It is a one-line summary and the
 * panel below it carries the substance; each tab also keeps the same text in its
 * title, so it is reachable at every width. */
@container (max-width: 900px) { .stagegloss { display: none; } }

.wbpanel {
  position: absolute;
  inset: 44px 0 0 0;
  display: none;
  overflow-y: auto;
  min-height: 0;
}
.wbpanel.on { display: block; }

/* Scroll runway under the docked voice surface.
 *
 * The dock sits in the bottom-left corner of the stage and does not move — it
 * holds stop-speaking, which PRD AUD-07 requires to be in the same place every
 * time somebody reaches for it. So a panel behind it must be able to scroll its
 * last row clear rather than leaving it permanently covered: content hidden
 * under a fixed island reads as content that is not there.
 *
 * Runway rather than a bottom inset, which was the other option: reserving the
 * dock's full height would take 250px off a 650px stage for a panel whose whole
 * job is showing a long diff, and it would do it across the full width to solve
 * an overlap that is half of it. */
.wbpanel:not(.wbpanel-model) { padding-bottom: 276px; }

/* Except where the panel's own columns scroll, which is the split below: the
 * runway belongs to whichever box actually scrolls. */
.wbpanel:has(.wbsplit) { overflow: hidden; padding-bottom: 0; }

/* See the section note: laid out always, hidden by visibility. */
.wbpanel-model { display: block; visibility: hidden; overflow: visible; }
.wbpanel-model.on { visibility: visible; }

/* ---- Files: the list, and one file's history --------------------------- */

.wbsplit {
  display: grid;
  grid-template-columns: minmax(180px, 260px) 1fr;
  height: 100%;
  min-height: 0;
}
@media (max-width: 900px) { .wbsplit { grid-template-columns: 1fr; } }

.wblist {
  border-right: 1px solid var(--edge-solid);
  padding: 10px 10px 276px;
  overflow-y: auto;
  align-content: start;
}
.wbdetail { padding: 14px 16px 276px; overflow-y: auto; min-width: 0; }

.wbfile {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 9px;
  margin-bottom: 2px;
  border: 1px solid transparent;
  border-radius: 7px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.wbfile:hover { background: var(--hover-wash); color: var(--ink); }
.wbfile:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 1px; }
.wbfile[aria-current="true"] { background: var(--surface-2); border-color: var(--core-dim); color: var(--ink); }
.wbfile-p {
  display: block;
  font-size: 12.5px;
  font-weight: 600;
  word-break: break-all;
}
.wbfile-m { display: block; margin-top: 2px; font-size: 10.5px; color: var(--ink-dim); }

.wbhead { margin-bottom: 14px; }
.wbhead-t { font-size: 14px; font-weight: 600; margin: 0; word-break: break-all; }
.wbhead-s { margin-top: 3px; font-size: 11.5px; color: var(--ink-dim); }

.wbvers { list-style: none; margin: 0; padding: 0; display: grid; gap: 12px; }

.wbver {
  border: 1px solid var(--edge-solid);
  border-radius: 10px;
  padding: 11px 13px;
  background: var(--surface-1);
}
.wbver-h { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.wbver-n { font-size: 12.5px; font-weight: 700; color: var(--ink); font-variant-numeric: tabular-nums; }
.wbver-w { font-size: 11px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
.wbver-a {
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.wbver-f { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 7px; }

/* The server's own sentence about whether this version may be relied on. Not
 * green by default: "accepted and passed" is the only state that earns it, and
 * that state is rare enough that colouring the rest would be reassuring noise. */
.wbver-u { margin: 8px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--ink-dim); }
.wbver-u.ok { color: var(--ok-ink); }

.wbver-m {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
  margin-top: 7px;
  font-size: 10.5px;
  color: var(--ink-dim);
}
.wbver-r { margin: 6px 0 0; font-size: 11.5px; color: var(--ink); }
.wbver-i { margin-top: 8px; font-size: 11.5px; color: var(--ink-dim); }
.wbver-i summary { cursor: pointer; }
.wbver-nodiff { margin: 8px 0 0; font-size: 11.5px; color: var(--ink-dim); font-style: italic; }

.wbjson, .wbdiff {
  margin: 7px 0 0;
  padding: 9px 11px;
  border: 1px solid var(--edge-solid);
  border-radius: 8px;
  background: var(--well);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
  line-height: 1.55;
  overflow-x: auto;
  max-height: 460px;
  overflow-y: auto;
  white-space: pre;
  color: var(--ink-dim);
}
.wbd-l { display: block; }
.wbd-add { color: var(--ok-ink); background: var(--ok-wash); }
.wbd-del { color: var(--bad-ink); background: var(--bad-wash-soft); }
.wbd-hunk { color: var(--core); }
.wbd-file { color: var(--ink); font-weight: 600; }

/* ---- Checks ------------------------------------------------------------- */

.wbchecks { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 9px; }
.wbcheck {
  border: 1px solid var(--edge-solid);
  border-radius: 9px;
  padding: 9px 12px;
  background: var(--surface-1);
}
.wbcheck-f {
  display: block;
  width: 100%;
  text-align: left;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--ink);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
  word-break: break-all;
}
.wbcheck-f:hover { color: var(--core); }
.wbcheck-f:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }

.wbsum {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--ink-dim);
}
.wbsum-n { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }
.wbsum-c { display: flex; gap: 6px; margin-left: auto; }

.wbnote {
  margin: 9px 0 0;
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink);
}

/* ---- chips: what a machine found, what a person decided ------------------
 *
 * Two vocabularies, deliberately not one palette. Verification is a machine's
 * finding and disposition is a person's decision (PRD SAF-05), and the reason
 * they are separate columns in the ledger is the reason they must not read as
 * two values of the same thing here. Verification carries the colour; a
 * disposition is outlined, whatever it says.
 *
 * `unverified` and `pending` are the honest defaults and are NOT green and NOT
 * silent — they are the states this build is almost always in, and a grey chip
 * that blends into the row is how "nothing checked this" comes to look like
 * "nothing wrong with this".
 */
.wbchip {
  display: inline-flex;
  align-items: baseline;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  border: 1px solid var(--edge-solid);
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--ink-dim);
}
.wbchip i { font-style: normal; font-weight: 400; opacity: .85; }

.wbchip-v-passed { color: var(--ok-ink); border-color: var(--ok-edge); background: var(--ok-wash); }
.wbchip-v-failed { color: var(--bad-ink); border-color: var(--bad-edge); background: var(--bad-wash-soft); }
.wbchip-v-unverified { color: var(--warn-ink); border-color: var(--warn-edge); background: var(--warn-wash-soft); }

.wbchip-d-pending { color: var(--ink-dim); }
.wbchip-d-accepted { color: var(--ink); border-color: var(--core-dim); }
.wbchip-d-rejected { color: var(--bad-ink); border-color: var(--bad-edge); }
.wbchip-d-superseded { color: var(--ink-dim); border-style: dashed; }

/* ---- shared states ------------------------------------------------------ */

.wbempty {
  padding: 18px;
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-dim);
  /* Centred in whatever room the panel has, rather than pinned to its top.
   *
   * An empty panel is the LARGEST this text will ever have to hold, and pinned
   * to the top it left a single line above several hundred pixels of nothing,
   * with the docked voice card marooned at the bottom. The page read as broken
   * when it was merely empty — and "empty" is the state a new user sees first.
   *
   * min-height rather than height: when the panel does have content this rule
   * still applies to the empty case only, and nothing else moves. */
  min-height: min(340px, 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  text-align: center;
  max-width: 46ch;
  margin: 0 auto;
}

/* A failure to read, said where the answer was expected. This panel is what the
 * person opened; an empty one would answer their question wrongly. */
.wbproblem {
  margin: 12px 0;
  padding: 10px 13px;
  border: 1px solid var(--bad-edge);
  border-radius: 9px;
  background: var(--bad-wash);
  color: var(--bad-ink);
  font-size: 12px;
  line-height: 1.55;
}

/* ---- a panel this build cannot fill -------------------------------------
 *
 * Rendered by the server from the connectors' own refusals (see stage.go). It
 * is prose, not an error state: nothing went wrong, and a red box would say
 * something did. What it must not look like is a feature that is loading. */
.wbrefusal { max-width: 62ch; padding: 26px 22px 34px; }
.wbrefusal-h { font-size: 14.5px; font-weight: 600; margin: 0 0 12px; color: var(--ink); }
.wbrefusal-p { margin: 0 0 11px; font-size: 12.5px; line-height: 1.65; color: var(--ink-dim); }

.wbrefused { margin-top: 20px; border-top: 1px solid var(--edge-solid); padding-top: 15px; }
.wbrefused-h {
  font-size: 9.5px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-bottom: 10px;
}
.wbconn { margin-bottom: 13px; }
.wbconn-n {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 11.5px;
  color: var(--warn-ink);
}
.wbconn-r { margin: 4px 0 0; font-size: 12px; line-height: 1.6; color: var(--ink-dim); }

/* ---- Diagram: the project graph, drawn (PRD WRK-01, WRK-03) --------------
 *
 * Boxes are HTML buttons and lines are SVG underneath them. Text in SVG neither
 * wraps nor ellipsises, so a title would have to be truncated by counting
 * characters and guessing at the font; buttons also bring keyboard focus and
 * hit-testing with them, which AUD-06 would otherwise have to be built.
 *
 * Colour carries the COLUMN, not the kind. Fourteen kinds would need fourteen
 * colours nobody could tell apart, and the column is the thing a reader is
 * scanning for: what must it do, what is being built, what checks it.
 */

.wbdia { position: relative; margin: 12px 0 6px; }
.wbedges { position: absolute; left: 0; top: 0; }

.wbe {
  fill: none;
  stroke: var(--edge-solid);
  stroke-width: 1.5;
}
.wbarrowhead { fill: var(--edge-solid); }

/* Focus on one node dims the rest rather than removing it. A relation that
 * disappears while you are looking at its neighbour reads as a relation that
 * does not exist. */
.wbe.on { stroke: var(--core); stroke-width: 2; }
.wbe.off { opacity: .22; }

.wbcolhead {
  position: absolute;
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  padding-bottom: 6px;
}

.wbn {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 2px;
  text-align: left;
  padding: 6px 10px;
  border: 1px solid var(--edge-solid);
  border-left-width: 3px;
  border-radius: 8px;
  background: var(--surface-2);
  color: var(--ink);
  cursor: pointer;
  overflow: hidden;
}
.wbn:hover { border-color: var(--core-dim); }
.wbn:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }
.wbn.sel { border-color: var(--core); background: var(--core-wash); }
.wbn.off { opacity: .35; }
.wbn-k {
  font-size: 9px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.wbn-t {
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.25;
  overflow: hidden;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  /* An anchored node's label is "decision dec_01M1H2W957FSP1R3E8W50D8RSC" — one
   * long token with no break opportunity in it, which without this does not wrap
   * and is simply cut off mid-identifier. */
  overflow-wrap: anywhere;
}

/* One colour per column. TestEveryDiagramColumnHasANodeColour holds this list
 * against stageNodeRoles(), so a column added in Go without a colour here fails
 * rather than rendering as an unmarked box. `other` is the column a kind lands
 * in when it matches none, and it is deliberately the loud one. */
.wbn-intent  { border-left-color: var(--role-intent); }
.wbn-build   { border-left-color: var(--role-build); }
.wbn-check   { border-left-color: var(--role-check); }
.wbn-risk    { border-left-color: var(--role-risk); }
.wbn-account { border-left-color: var(--role-account); }
.wbn-other   { border-left-color: var(--bad-ink); border-style: dashed; }

.wbrel { margin-top: 18px; border-top: 1px solid var(--edge-solid); padding-top: 14px; }
.wbrel-h {
  display: flex;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
  font-size: 12.5px;
  color: var(--ink);
  margin-bottom: 10px;
}
.wbrel-c { font-size: 11px; color: var(--ink-dim); }
.wbrel-k {
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin-top: 10px;
}
.wbrel-l { margin: 4px 0 0; padding-left: 18px; font-size: 12px; line-height: 1.6; color: var(--ink-dim); }
.wbrel-l b { color: var(--ink); }
.wbrel-l i { font-style: normal; opacity: .75; }

.wbopen {
  padding: 3px 9px;
  font-size: 11px;
  border: 1px solid var(--edge-solid);
  border-radius: 6px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
}
.wbopen:hover { color: var(--ink); border-color: var(--core-dim); }
.wbopen:focus-visible { outline: 2px solid var(--core-dim); outline-offset: 2px; }

/* A node joined to nothing is invisible as an absence — it looks exactly like a
 * node whose relations are off screen. Named, because "what has nobody connected
 * up yet" is one of the two questions a project graph is kept for. */
.wbiso { margin-top: 16px; border-top: 1px dashed var(--edge-solid); padding-top: 13px; }
.wbiso-h {
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--warn-ink);
  margin-bottom: 6px;
}

/* The roles the diagram reads its columns from. Data, not display. */
.wbroles { display: none; }

/* ---- Telemetry (PRD NFR-05, AUD-02) ------------------------------------- */

.wbtel-h {
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  margin: 20px 0 9px;
}
.wbtel-h:first-child { margin-top: 4px; }

.wbtel-g {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
  gap: 9px;
}

.wbstat {
  border: 1px solid var(--edge-solid);
  border-radius: 9px;
  padding: 10px 12px;
  background: var(--surface-1);
}
.wbstat-n { font-size: 19px; font-weight: 600; color: var(--ink); font-variant-numeric: tabular-nums; }
.wbstat-l { margin-top: 2px; font-size: 11px; color: var(--ink); }
/* The reason a figure is missing, in the same box as the figure. An em dash with
 * no explanation is indistinguishable from a measurement of nothing. */
.wbstat-w { margin-top: 3px; font-size: 10.5px; color: var(--ink-dim); line-height: 1.4; }

.wbwarn { color: var(--warn-ink); }

.wbmeas {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 4px 14px;
  font-size: 12px;
  align-items: baseline;
}
.wbmeas-k {
  font-size: 9.5px;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--ink-dim);
}
.wbmeas-v { color: var(--ink); font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px; }

.wbturns { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.wbturn {
  border: 1px solid var(--edge-solid);
  border-radius: 9px;
  padding: 9px 12px;
  background: var(--surface-1);
}
.wbturn.bad { border-color: var(--bad-edge); }
.wbturn-h { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.wbturn-m {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 6px;
  font-size: 11px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
/* Which clock produced each number, beside the number. Two clocks answer
 * different questions about the same turn and a reader cannot tell them apart
 * from the value. */
.wbturn-m i { font-style: normal; opacity: .6; }
.wbturn-t { margin-top: 5px; font-size: 10.5px; color: var(--ink-dim); }

.wbunmeas { margin-top: 22px; border-top: 1px solid var(--edge-solid); padding-top: 14px; }
.wbunmeas-h {
  font-size: 9.5px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--warn-ink);
  margin-bottom: 8px;
}

/* ---- the conversation record (PRD RSN-07, AUD-07) ------------------------
 *
 * A restored turn is marked, and the mark is not decoration. A live reply
 * arrives with its epistemic labels, the standards it quoted from memory and
 * the provenance of anything it drew; those are derived as the reply lands and
 * are not stored, so they do not come back. A restored turn rendered as an
 * ordinary one would say "FORGE made no claims here" — which is a different
 * statement from "nobody kept them", and a false one.
 */

.railhead { display: flex; align-items: center; gap: 10px; }

.forget {
  margin-left: auto;
  padding: 3px 9px;
  font-size: 10.5px;
  letter-spacing: .02em;
}
.forget.armed { color: var(--bad-ink); border-color: var(--bad-edge); }
.forget:disabled { opacity: .45; cursor: not-allowed; }

.turn.restored { opacity: .88; border-style: dashed; }
.from-record {
  margin-left: 8px;
  font-size: 9px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-dim);
  font-weight: 500;
}

/* What the record does NOT contain, said once at the join between the restored
 * turns and the live ones. */
.turn.note {
  border-style: dashed;
  background: transparent;
}
.turn.note .body { color: var(--ink-dim); font-size: 11.5px; line-height: 1.55; }

/* ---- narrow: one column, and a way back to the other two ----------------
 *
 * Every narrow-width rule in this file lives BELOW the base rules it
 * overrides, and that is load-bearing rather than tidiness. A media query
 * carries no extra specificity, so `.wb-left { display: none }` inside one
 * loses to a plain `.wb-left { display: grid }` that happens to appear later
 * in the file. Written higher up, this block silently did nothing: the phone
 * layout computed correctly, the switcher worked, and the conversation rail
 * stayed on top of everything regardless of which section was selected.
 *
 * # The defect this replaces
 *
 * These two breakpoints used to read:
 *
 *     @media (max-width: 1200px) { .wb-right { display: none; } }
 *     @media (max-width:  820px) { .wb-left  { display: none; } }
 *
 * with nothing that brought either back. On a phone the workbench was a header
 * and a stage: the conversation, the delete-session control that AUD-07 requires
 * to be reachable at all times, Parts, Variants, People, and the accept/reject
 * controls for proposed work were all simply gone. Not collapsed, not behind a
 * drawer — absent, with no affordance suggesting anything was missing. A product
 * whose whole model is "talk while looking at the thing" had dropped the talking
 * half on the device most likely to be used away from a desk.
 *
 * See docs/bugfix/2026-09-07-the-workbench-hid-half-itself-on-a-phone.md.
 *
 * # Why one breakpoint rather than two
 *
 * The old arrangement had a middle band (820–1200px) where the right rail was
 * already gone and nothing said so. Collapsing to a single switched column at
 * one width means there is exactly one narrow behaviour to reason about and to
 * test, instead of three states two of which silently lose content.
 *
 * # Why the stage is never display:none
 *
 * It holds the WebGL canvas, and a `display: none` ancestor collapses that
 * canvas's client box to zero — the viewport would come back at 640x480 with the
 * camera somewhere else (the same reason the model PANEL is hidden with
 * visibility; see the note further down). So all three regions share one grid
 * cell, the stage stays laid out at the bottom of the stack, and the rails cover
 * it opaquely when selected.
 */
@media (max-width: 1199px) {
  .wb { grid-template-rows: auto 1fr auto; }

  .wb-body { grid-template-columns: minmax(0, 1fr); }
  .wb-left,
  .stage,
  .wb-right { grid-area: 1 / 1; min-width: 0; }

  .wb-left,
  .wb-right {
    display: none;
    z-index: 12;
    background: var(--surface-1);
    border-left: 0;
    border-right: 0;
  }
  .wb-body[data-view="talk"] .wb-left  { display: grid; }
  .wb-body[data-view="work"] .wb-right { display: block; }

  /* Everything that scrolls has to end above the fixed console bar. */
  .wb-left .transcript { padding-bottom: calc(var(--wb-dock-h) + 12px); }
  .wb-right.rail { padding-bottom: calc(var(--wb-dock-h) + 14px); }
}

/* ===== The section switcher ==============================================
 *
 * Three regions, one column, and this is how a reader reaches the two that are
 * not on top. See the note at the `max-width: 1199px` block for why the layout
 * collapses at all; this is the control that makes the collapse survivable.
 *
 * Buttons with `aria-pressed`, not a tablist. The stage already contains a real
 * tablist — Model, Diagram, Files, Checks, Telemetry — and nesting one set of
 * tabs inside a region selected by another set reads, to a screen reader, as
 * tabs inside tabs. `aria-controls` still names what each button reveals, so the
 * relationship is stated without claiming a structure the page does not have.
 *
 * It is present only below the three-column breakpoint. Above it all three
 * regions are on screen at once and a control for switching between things you
 * can already see is noise.
 */
/* Hidden by default and shown by the media query below it. Stated in this
 * order deliberately: the two rules have equal specificity, so whichever comes
 * last wins — with the base rule further down the file the switcher stayed
 * hidden at every width and the two rails were unreachable, which is the exact
 * defect this control exists to fix. */
.wbmobile {
  display: none;
  grid-template-columns: repeat(3, 1fr);
  gap: 4px;
  padding: 4px 8px calc(4px + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--edge-solid);
  background: var(--panel);
  z-index: 30;
}
.wbmobile-tab {
  /* 44px, which is the smallest target a finger hits reliably. The rest of the
   * narrow layout is brought up to the same floor under `pointer: coarse`
   * further down; this one is stated outright because it is only ever touched. */
  min-height: 44px;
  padding: 6px 8px;
  border: 1px solid transparent;
  border-radius: 9px;
  background: transparent;
  color: var(--ink-dim);
  font: inherit;
  font-size: 12px;
  font-weight: 650;
  letter-spacing: .04em;
  cursor: pointer;
}
.wbmobile-tab:hover { color: var(--ink); }
.wbmobile-tab:focus-visible { outline: 2px solid var(--core-dim); outline-offset: -2px; }
.wbmobile-tab[aria-pressed="true"] {
  color: var(--on-accent);
  background: var(--core);
  border-color: transparent;
}

/* ===== Narrow-width corrections =========================================== */

/* The stage's tab strip had `flex: 0 0 auto` and no overflow, so six tabs in a
 * 44px bar simply ran off the right edge of a phone with nothing to scroll.
 * The last two panels — including the ones that explain why they are empty —
 * could not be reached at all. */
.stagetabs { min-width: 0; overflow-x: auto; scrollbar-width: none; }
.stagetabs::-webkit-scrollbar { display: none; }

/* The header is a flex row with no wrap holding three spans that JavaScript
 * fills with an account address and two model identifiers. None of them had a
 * width bound, so a long model name pushed the row — and the Operations link at
 * the end of it — past the right edge of the screen. */
.wb-top { flex-wrap: wrap; row-gap: 4px; }
.wb-top #meta,
.wb-top #models,
.wb-top .who {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 700px) {
  /* Both are diagnostics rather than controls, and both are also in the
   * telemetry panel. The header keeps identity and the way out. */
  .wb-top #meta,
  .wb-top #models { display: none; }
}

/* Full-screen dialogs on a small screen. A 560px panel inset 14px from the top
 * left of a 375px viewport is neither a dialog nor a page. */
@media (max-width: 700px) {
  .soul {
    top: 0; left: 0; right: 0; bottom: 0;
    width: auto;
    max-height: none;
    border: 0;
    border-radius: 0;
  }
  .compare-body { padding: 12px; }
}

/* Touch targets.
 *
 * Scoped to `pointer: coarse` rather than to a width, because the thing that
 * decides how big a control has to be is what is pointing at it, not how wide
 * the window is — a small window on a laptop is still driven by a mouse, and a
 * tablet in landscape is wide and still driven by a finger.
 *
 * Only the box grows. Type sizes, colours and spacing are untouched, so the
 * interface does not become a different one when opened on a phone. */
@media (pointer: coarse) {
  .node { width: 44px; height: 44px; }
  .voice[data-place="dock"] .node { width: 44px; height: 44px; }
  .voice[data-place="dock"] .node svg { width: 17px; height: 17px; }
  .stagetab { min-height: 44px; padding: 5px 14px; }
  .viewctl button { min-height: 44px; padding: 5px 14px; }
  .ghost { min-height: 44px; }
  .forget { min-height: 44px; padding: 3px 14px; }
  .whois { min-height: 44px; }
  .part { min-height: 44px; }
  .theme-toggle { width: 44px; height: 44px; }
  .voice-ctl input[type=text] { min-height: 44px; }
  .voice[data-place="dock"] .voice-ctl input[type=text] { min-height: 44px; }
  /* The remove-attachment control was 18px square. It deletes something, which
   * is the worst size of target for the worst kind of mistake. */
  .att button { width: 30px; height: 30px; top: -10px; right: -10px; }
  .sw input[type=checkbox] { width: 44px; height: 24px; }
  .sw input[type=checkbox]::after { width: 16px; height: 16px; top: 3px; left: 3px; }
  .sw input[type=checkbox]:checked::after { transform: translateX(20px); }
}

/* Last word on whether the section switcher is on screen. See .wbmobile above. */
@media (max-width: 1199px) {
  .wbmobile { display: grid; }
}
