/* meshsmith web UI — "the forge": deep charcoal, molten-amber accent, teal for validated. */

:root {
  --bg: #0a0b0f;
  --bg-2: #0f1117;
  --panel: rgba(255, 255, 255, 0.028);
  --panel-solid: #14161d;
  --hair: rgba(255, 255, 255, 0.08);
  --hair-strong: rgba(255, 255, 255, 0.14);
  --text: #eceef4;
  --text-dim: #a3a7b4;
  --text-faint: #6b6f7e;
  --accent: #ff7a2f;
  --accent-2: #ff4d3d;
  --accent-grad: linear-gradient(135deg, #ffa23d 0%, #ff5330 100%);
  --glow: rgba(255, 122, 47, 0.35);
  --ok: #33d6a0;
  --ok-dim: rgba(51, 214, 160, 0.14);
  --bad: #ff5f6d;
  --bad-dim: rgba(255, 95, 109, 0.14);
  --radius: 14px;
  --radius-sm: 9px;
  --font: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, monospace;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --topbar-h: 60px;
}

* { box-sizing: border-box; }

/* Must beat component display rules (.screen{display:grid}, .app{display:flex},
   .results{display:grid}, .viewer-toolbar{display:flex}). Author CSS otherwise
   overrides the UA's [hidden]{display:none} and "hidden" elements still render. */
[hidden] { display: none !important; }

html, body { height: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  color: var(--text);
  background:
    radial-gradient(1200px 700px at 78% -8%, rgba(255, 122, 47, 0.10), transparent 60%),
    radial-gradient(900px 600px at 8% 108%, rgba(90, 120, 255, 0.06), transparent 55%),
    var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ---------------- brand ---------------- */
.brand-mark {
  font-size: 44px;
  line-height: 1;
  background: var(--accent-grad);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  filter: drop-shadow(0 4px 18px var(--glow));
}
.brand-mark.sm { font-size: 20px; }
.brand-mark.denied { background: linear-gradient(135deg, #ff5f6d, #b03040); -webkit-background-clip: text; background-clip: text; }
.brand-word {
  font-weight: 800;
  letter-spacing: -0.02em;
  font-size: 30px;
  margin: 14px 0 0;
}
.brand-word.sm { font-size: 17px; margin: 0; font-weight: 750; }
.brand-word span { color: var(--accent); }

/* ---------------- screens (signin / denied) ---------------- */
.screen {
  min-height: 100dvh;
  display: grid;
  place-items: center;
  padding: 24px;
}
.signin-card {
  width: min(420px, 92vw);
  text-align: center;
  padding: 44px 36px;
  background: var(--panel-solid);
  border: 1px solid var(--hair);
  border-radius: 20px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
  animation: fadeInUp 0.5s var(--ease) both;
}
.signin-tag { color: var(--text-dim); margin: 12px 0 26px; line-height: 1.5; }
.signin-fine { color: var(--text-faint); font-size: 12.5px; margin: 16px 0 0; }

/* ---------------- buttons ---------------- */
.btn {
  font-family: inherit;
  font-weight: 650;
  font-size: 14px;
  border: 1px solid var(--hair-strong);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  padding: 11px 18px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 9px;
  transition: transform 0.15s var(--ease), background 0.2s, border-color 0.2s, box-shadow 0.2s, opacity 0.2s, filter 0.2s;
}
/* Hover means READY. It brightens and lifts; it never washes a control out —
   grey is spoken for, and it says something else (`:disabled` while a stage
   runs, `.opt-coming` for what is on the map but not built). The lighter film
   is the neutral button's whole hover, but on an accent-filled .btn it would
   REPLACE the gradient rather than sit on it, so the fill goes translucent grey
   and a live CTA reads as deactivated. Hence the film is aimed away from the
   accent variants, which are lit from their own rule below; the lift is shared
   because a lift is not a colour. */
.btn:not(.btn-primary):hover { background: rgba(255, 255, 255, 0.08); }
.btn:hover { transform: translateY(-1px); }
.btn:active { transform: translateY(0); }
.btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.btn-sm { padding: 7px 12px; font-size: 12.5px; }
.btn-wide { width: 100%; }
.btn-ghost { background: transparent; }
/* A .btn that is an <a> because it genuinely leaves for another app — so it
   keeps a real href (middle-click, copy-link) and loses only the underline.
   Hiding it is already covered: the global [hidden] rule above carries the
   !important that .btn's own `display` would otherwise outrank. */
.btn-door { text-decoration: none; }
.btn-primary {
  background: var(--accent-grad);
  border: none;
  color: #1a0d05;
  font-weight: 750;
  box-shadow: 0 8px 26px -6px var(--glow);
}
/* the accent answer to a hover: the same gradient, a step hotter, and more of
   the glow it already casts. `filter` is safe to spend here because :disabled
   below re-claims it — a primary that is mid-run stays desaturated under the
   cursor, which is the one time grey is the truth. */
.btn-primary:hover { background: var(--accent-grad); filter: brightness(1.07); box-shadow: 0 12px 34px -6px var(--glow); }
.btn:disabled { opacity: 0.4; cursor: not-allowed; transform: none; box-shadow: none; filter: saturate(0.5); }
/* sign-in form */
.signin-form { display: flex; flex-direction: column; gap: 10px; }
.signin-form input {
  width: 100%; padding: 12px 14px; font: inherit; font-size: 14px;
  background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text);
  transition: border-color 0.18s, background 0.18s;
}
.signin-form input::placeholder { color: var(--text-faint); }
.signin-form input:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }
.signin-form button { margin-top: 4px; }
.signin-error {
  color: var(--bad); font-size: 13px; margin: 14px 0 0; line-height: 1.45;
  background: var(--bad-dim); border-radius: var(--radius-sm); padding: 10px 12px;
}
.link-btn {
  background: none; border: none; color: var(--text-dim); font: inherit; font-size: 12.5px;
  cursor: pointer; margin-top: 14px; text-decoration: underline; text-underline-offset: 3px;
}
.link-btn:hover { color: var(--accent); }

/* ---------------- app shell ---------------- */
.app { min-height: 100dvh; display: flex; flex-direction: column; }
.topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 14px 22px;
  border-bottom: 1px solid var(--hair);
  background: rgba(10, 11, 15, 0.7);
  backdrop-filter: blur(14px);
  position: sticky; top: 0; z-index: 20;
}
/* Global activity bar: the per-card progress fill is easy to miss, so a stage in
   flight also claims a strip right under the topbar. --topbar-h is a fallback —
   app.js pins the real measured height when the bar appears. */
.activity {
  position: sticky; top: var(--topbar-h); z-index: 19;
  display: flex; align-items: center; gap: 10px;
  padding: 7px 22px;
  background: rgba(255, 122, 47, 0.08);
  border-bottom: 1px solid var(--accent);
  font-size: 12.5px; color: var(--text);
  overflow: hidden;
}
.activity::after {
  content: ""; position: absolute; left: 0; top: 0; height: 2px; width: 100%;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: sweep 1.3s linear infinite;
}
.act-dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none;
  box-shadow: 0 0 0 4px var(--glow); animation: pulse 1.2s infinite;
}
.act-pct { color: var(--text-dim); font-variant-numeric: tabular-nums; }

.brand { display: flex; align-items: center; gap: 10px; }
.brand-sub {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.16em;
  color: var(--text-faint); border-left: 1px solid var(--hair);
  padding-left: 10px; margin-left: 4px;
}
.user { display: flex; align-items: center; gap: 12px; }
.user-email { color: var(--text-dim); font-size: 13px; }

.layout.no-panel { grid-template-columns: 1fr; }
.layout {
  flex: 1;
  display: grid;
  grid-template-columns: 372px 1fr;
  gap: 20px;
  padding: 20px;
  max-width: 1500px;
  width: 100%;
  margin: 0 auto;
}

/* ---------------- panels ---------------- */
.panel {
  background: var(--panel);
  border: 1px solid var(--hair);
  border-radius: var(--radius);
  padding: 18px;
  backdrop-filter: blur(6px);
}
.panel-title {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.13em;
  color: var(--text-faint); margin: 0 0 12px; font-weight: 700;
}
.controls { align-self: start; display: flex; flex-direction: column; gap: 6px; }
.controls .panel-title:not(:first-child) { margin-top: 20px; }

/* ---------------- dropzone ---------------- */
.dropzone {
  position: relative;
  border: 1.5px dashed var(--hair-strong);
  border-radius: var(--radius);
  min-height: 190px;
  display: grid; place-items: center;
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.2s, background 0.2s;
  background: rgba(255, 255, 255, 0.015);
}
.dropzone:hover, .dropzone:focus-visible { border-color: var(--accent); background: rgba(255, 122, 47, 0.05); outline: none; }
.dropzone.drag { border-color: var(--accent); background: rgba(255, 122, 47, 0.10); }
.dropzone.has-image { border-style: solid; border-color: var(--hair); }
.dz-empty { text-align: center; padding: 20px; pointer-events: none; }
.dz-icon {
  width: 44px; height: 44px; margin: 0 auto 12px; border-radius: 50%;
  display: grid; place-items: center; font-size: 20px; color: var(--accent);
  border: 1.5px solid var(--accent); box-shadow: 0 0 24px -6px var(--glow);
}
.dz-text { font-weight: 650; line-height: 1.35; }
.dz-text span { color: var(--text-dim); font-weight: 400; font-size: 13px; }
.dz-hint { margin-top: 10px; font-size: 11.5px; color: var(--text-faint); }
/* the size sanity warning — visible, never a silent bake (ASK-111 §size-first) */
.dz-hint.warn { color: var(--bad); }
.dz-thumb { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain;
  background: repeating-conic-gradient(#15171e 0 25%, #101219 0 50%) 0 0 / 22px 22px; }

/* ---------------- segmented + fields ---------------- */
.segmented { display: flex; gap: 6px; background: rgba(255,255,255,0.03);
  padding: 5px; border-radius: var(--radius-sm); border: 1px solid var(--hair); }
.seg {
  flex: 1; border: none; background: transparent; color: var(--text-dim);
  padding: 9px 8px; border-radius: 6px; cursor: pointer; font: inherit;
  font-size: 13px; font-weight: 600; transition: background 0.18s, color 0.18s; text-align: center;
}
.seg small { display: block; font-weight: 400; font-size: 10.5px; color: var(--text-faint); margin-top: 2px; }
.seg[aria-selected="true"] { background: var(--accent-grad); color: #1a0d05; }
.seg[aria-selected="true"] small { color: #3a1a08; }

/* ---------------- what is coming ---------------- */
/* One treatment for every option that is on the map but not yet built, on a
   <select>'s option, on a viewer chip and on a segmented tab alike. It is
   deliberately NOT hidden: an increment should read as an unlock, and it can
   only do that if the shape of the choice was visible beforehand. Dimmed, never
   focusable, and it does nothing at all when pressed — a greyed control that
   turned out to be a dead end would be worse than not showing it.
   The suffix ("· coming") is written into the label rather than added here,
   because a disabled <option> cannot be given one by CSS in every browser and
   half a convention is no convention. */
.opt-coming { opacity: 0.42; cursor: default; }
button.opt-coming { pointer-events: auto; }        /* so the tooltip still tells */
button.opt-coming:hover { color: var(--text-dim); border-color: var(--hair-strong); }
.seg.opt-coming, .seg.opt-coming:hover { background: transparent; color: var(--text-faint); }
.chip.opt-coming, .chip.opt-coming:hover {
  color: var(--text-faint); border-color: var(--hair); background: rgba(10, 11, 15, 0.72);
}

/* ---------------- object-type presets ---------------- */
/* The primary control: what KIND of thing this is. Two columns, odd counts left
   to flow — a fifth card on its own row reads as a card, not a mistake. */
.preset-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.preset {
  border: 1px solid var(--hair); border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.02); padding: 10px; cursor: pointer;
  text-align: left; font: inherit; color: var(--text);
  transition: border-color 0.15s, background 0.15s;
}
.preset:hover { border-color: var(--hair-strong); }
.preset:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.preset[aria-selected="true"] { border-color: var(--accent); background: rgba(255, 122, 47, 0.08); }
.preset .p-icon { font-size: 20px; line-height: 1.1; }
.preset .p-label { font-weight: 650; font-size: 12.5px; margin-top: 4px; }
.preset .p-note { font-size: 10.5px; color: var(--text-faint); margin-top: 2px; line-height: 1.3; }
#btn-wizard { align-self: flex-start; margin-top: 8px; }

/* ---------------- "help me choose" wizard ---------------- */
/* A modal, not a drawer: three questions is a moment, not a place. */
.wizard {
  position: fixed; inset: 0; z-index: 55; display: grid; place-items: center;
  background: rgba(0, 0, 0, 0.55); padding: 20px;
}
.wizard-card {
  width: min(400px, 92vw); max-height: 88dvh; overflow-y: auto;
  background: var(--panel-solid); border: 1px solid var(--hair);
  border-radius: 18px; padding: 22px 22px 20px;
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
  animation: fadeInUp 0.25s var(--ease) both;
}
.wiz-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.wiz-head .panel-title { margin: 0; }
.wiz-sub { color: var(--text-dim); font-size: 12.5px; margin: 6px 0 18px; line-height: 1.5; }
.wiz-q { margin-bottom: 16px; }
.wiz-ask { font-size: 13.5px; font-weight: 650; margin-bottom: 8px; }
.wiz-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.wiz-chips .chip { backdrop-filter: none; background: rgba(255, 255, 255, 0.03); }
#btn-wiz-done { margin-top: 4px; padding: 13px; }

.advanced { margin-top: 16px; border-top: 1px solid var(--hair); padding-top: 6px; }
.advanced summary {
  cursor: pointer; color: var(--text-dim); font-size: 13px; font-weight: 600;
  padding: 8px 0; list-style: none; user-select: none;
}
.advanced summary::-webkit-details-marker { display: none; }
.advanced summary::before { content: "▸ "; color: var(--text-faint); }
.advanced[open] summary::before { content: "▾ "; }
.adv-hint { font-weight: 400; font-size: 11px; color: var(--text-faint); margin-left: 6px; }

.adv-group { margin: 6px 0 14px; padding: 10px 12px; border: 1px solid var(--hair);
  border-radius: var(--radius-sm); background: rgba(255, 255, 255, 0.015); }
.adv-cap { display: flex; align-items: center; gap: 7px; font-size: 11px;
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--text-faint);
  font-weight: 700; margin-bottom: 4px; }
.cap-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--accent); opacity: 0.8; }

/* info tooltips */
.info {
  display: inline-grid; place-items: center; width: 15px; height: 15px;
  border-radius: 50%; border: 1px solid var(--hair-strong); color: var(--text-faint);
  font-size: 9.5px; font-style: normal; cursor: help; position: relative;
  margin-left: 5px; vertical-align: 1px; user-select: none;
}
.info:hover, .info:focus-visible { color: var(--accent); border-color: var(--accent); outline: none; }
[data-tip] { position: relative; }
[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%) translateY(3px);
  width: 240px; padding: 10px 12px; border-radius: 9px;
  background: #1b1e27; border: 1px solid var(--hair-strong); color: var(--text);
  font-size: 12px; font-weight: 400; line-height: 1.5; text-transform: none;
  letter-spacing: normal; text-align: left;
  opacity: 0; pointer-events: none; transition: opacity 0.15s var(--ease), transform 0.15s var(--ease);
  box-shadow: 0 14px 40px rgba(0, 0, 0, 0.5); z-index: 40;
}
[data-tip]:hover::after, [data-tip]:focus-visible::after { opacity: 1; transform: translateX(-50%) translateY(0); }

/* inline cost estimate */
.est {
  color: var(--text-dim); cursor: help;
  border-bottom: 1px dotted var(--text-faint);
}
.est:hover, .est:focus-visible { color: var(--accent); border-bottom-color: var(--accent); outline: none; }
.field label { display: inline-flex; align-items: center; }
input.switch { width: auto; accent-color: var(--accent); transform: scale(1.15); }
.field { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 10px 0; }
.field label { color: var(--text-dim); font-size: 13px; }
.field input, .field select {
  width: 110px; background: rgba(0,0,0,0.3); border: 1px solid var(--hair);
  color: var(--text); border-radius: 7px; padding: 8px 10px; font: inherit; font-size: 13px;
}
.field input:focus, .field select:focus { outline: none; border-color: var(--accent); }
.field-group-title { margin: 16px 0 4px; font-size: 11px; text-transform: uppercase;
  letter-spacing: 0.1em; color: var(--text-faint); }
/* the group's own caption already sits above the first heading */
.adv-cap + .field-group-title { margin-top: 6px; }
/* a heading that carries its track's own re-run: the verb sits AT the name of
   the thing it re-runs, so which half a press moves is never a guess */
.group-head { display: flex; align-items: center; gap: 8px; }
.track-rerun {
  border: 1px solid var(--hair); background: transparent; color: var(--text-dim);
  width: 22px; height: 22px; border-radius: 6px; cursor: pointer; font-size: 12px;
  line-height: 1; flex: none; padding: 0;
  transition: color 0.15s, border-color 0.15s;
}
.track-rerun:hover { color: var(--accent); border-color: var(--accent); }
.track-rerun:disabled { opacity: 0.4; cursor: not-allowed; }
/* a field whose control needs the full width (multi-line text) */
.field-stack { flex-direction: column; align-items: stretch; gap: 6px; }
.field-stack > label { align-self: flex-start; }
/* a stacked control names things rather than sizing them — "Extract from splat
   (TSDF)" does not fit the 110px a number does, and a clipped option name is a
   choice nobody can make */
.field-stack > select { width: 100%; }
#prompt-suffix, #asset-comment, #gg-desc {
  width: 100%; background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text-dim); font: inherit;
  font-size: 12px; padding: 10px 11px; resize: vertical;
  transition: border-color 0.18s, background 0.18s;
}
#prompt-suffix::placeholder, #asset-comment::placeholder, #gg-desc::placeholder { color: var(--text-faint); }
#prompt-suffix:focus, #asset-comment:focus, #gg-desc:focus {
  outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); color: var(--text);
}

.btn-forge { margin-top: 18px; padding: 14px; font-size: 15px; }
.forge-ico { font-size: 16px; }
.spin {
  display: inline-block; width: 12px; height: 12px; border-radius: 50%;
  border: 2px solid rgba(26, 13, 5, 0.28); border-top-color: #1a0d05;
  animation: rot 0.7s linear infinite; vertical-align: -1px;
}
.forge-note { text-align: center; color: var(--text-faint); font-size: 12px; margin: 10px 0 2px; }

/* ---------------- stage / viewer ---------------- */
.stage { display: flex; flex-direction: column; gap: 20px; min-width: 0; }
.viewer-wrap {
  position: relative; border-radius: var(--radius); overflow: hidden;
  border: 1px solid var(--hair); background: linear-gradient(180deg, #0d0f15, #0a0b10);
  aspect-ratio: 16 / 10; min-height: 380px;
  /* The aspect ratio is the PREFERRED size, never a promise about the page.
     Unclamped, a 1500px-wide stage is 940px tall and the viewer alone is taller
     than the window — Source, the pipeline and the results all land below the
     fold and the page reads as if nothing exists under the model. The cap keeps
     the fold honest: whatever the window is, the viewer stops short enough that
     the top of the next section is on screen with it. model-viewer fills its
     container, so a clamped box costs the model nothing but framing.
     It is spent through a variable rather than declared here so the narrower
     bands can re-price it (below) while FULLSCREEN's `max-height: none` still
     outranks a single 0-1-0 declaration and fills the screen. */
  --viewer-cap: max(380px, calc(100dvh - var(--stick-top, var(--topbar-h)) - 160px));
  max-height: var(--viewer-cap);
  /* The viewer owns every touch inside it. model-viewer's own touch-action
     attribute governs its canvas, but the browser decides per ELEMENT whether a
     drag scrolls the page — and touch-action does not inherit — so the wrapper
     and the element both have to claim it, or a drag that starts on the badge,
     the toolbar or a hair of padding pans the page instead of the model.
     overscroll-behavior stops any residual scroll chaining out to the document. */
  touch-action: none;
  overscroll-behavior: contain;
}
.viewer { width: 100%; height: 100%; touch-action: none; --poster-color: transparent;
  background: radial-gradient(120% 90% at 50% 15%, #171a22, #0b0d12 70%); }
.viewer-empty { position: absolute; inset: 0; display: grid; place-items: center; }
.ve-grid { position: absolute; inset: 0;
  background-image: linear-gradient(var(--hair) 1px, transparent 1px),
                    linear-gradient(90deg, var(--hair) 1px, transparent 1px);
  background-size: 40px 40px;
  mask-image: radial-gradient(70% 60% at 50% 45%, #000 20%, transparent 75%);
  opacity: 0.5; }
.ve-content { position: relative; text-align: center; }
.ve-ico { font-size: 40px; background: var(--accent-grad); -webkit-background-clip: text;
  background-clip: text; color: transparent; opacity: 0.85; animation: float 4s var(--ease) infinite; }
.ve-title { font-weight: 650; margin: 14px 0 4px; }
.ve-sub { color: var(--text-faint); font-size: 13px; margin: 0; }

.viewer-badge {
  position: absolute; top: 12px; left: 12px;
  background: rgba(10, 11, 15, 0.72); border: 1px solid var(--hair-strong);
  color: var(--text-dim); border-radius: 999px; padding: 7px 14px;
  font-size: 12px; backdrop-filter: blur(8px); pointer-events: none;
}
.viewer-badge b { color: var(--accent); font-weight: 700; }
/* the provenance line: quiet on purpose. The coordinate above it says what the
   thing is; this says which press made it, which is only interesting when you
   have decided you want it different. */
.badge-made { display: block; margin-top: 2px; font-size: 11px;
  color: var(--text-faint); letter-spacing: 0.01em; }

/* FULLSCREEN — two paths, one look.
   The real Fullscreen API gives us :fullscreen; iOS Safari has no such thing
   for a div, so .faux-full pins the viewer over the viewport instead. Both
   drop the rounded frame and the aspect ratio and fill the screen. */
.viewer-wrap:fullscreen,
.viewer-wrap:-webkit-full-screen,
.viewer-wrap.faux-full {
  aspect-ratio: auto; height: 100dvh; width: 100vw; max-height: none;
  border-radius: 0; border: 0; position: fixed; inset: 0; z-index: 200;
  padding-top: env(safe-area-inset-top);
  padding-bottom: env(safe-area-inset-bottom);
}
/* the mobile rules pin the viewer and cap its height — fullscreen outranks both */
.layout.has-model .viewer-wrap.faux-full { position: fixed; top: 0; height: 100dvh; }
body.fs-lock { overflow: hidden; }

/* download feedback for the mesh itself — it sits above the toolbar, never over it */
.viewer-loading {
  position: absolute; inset: auto 12px 56px 12px; z-index: 6;
  background: rgba(10, 11, 15, 0.8); backdrop-filter: blur(8px);
  border: 1px solid var(--hair-strong); border-radius: 10px; padding: 10px 14px;
  display: flex; flex-direction: column; gap: 6px;
  font-size: 12.5px; color: var(--text-dim);
}
.vl-bar { height: 4px; border-radius: 4px; background: rgba(255, 255, 255, 0.08); overflow: hidden; }
.vl-fill { height: 100%; width: 0%; background: var(--accent-grad); border-radius: 4px;
  transition: width 0.25s var(--ease); }
.viewer-loading.vl-error { border-color: var(--bad); color: var(--text); }
.viewer-loading.vl-error .vl-bar { display: none; }
#vl-retry { align-self: flex-start; }
.viewer-toolbar { position: absolute; left: 12px; right: 12px; bottom: 12px;
  /* flex-end, not center: Spin and Full line up with the step row, which is
     the row a person is actually working in */
  display: flex; align-items: flex-end; justify-content: space-between; gap: 10px;
  /* above the source-image and candidate overlays: the toolbar is how you get
     BACK from them, so an overlay that covers it strands you there */
  z-index: 8; }
/* THE PICKER — two questions, two rows: WHAT you are looking at (Mesh · Splat)
   over WHICH STEP it is (Extracted · Cleaned · Packaged, plus an LOD chip
   each, and each leading with its own card's dot). Each row
   wraps on its own before it starts pushing the toolbar around on a narrow
   screen. A session with one representation draws no WHAT row at all, so an
   upload or a prompt keeps the single row it always had. */
.vt-tabs { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.vt-actions { display: flex; align-items: center; gap: 8px; flex: none; }
.vt-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
/* the WHAT row is the quieter of the two: it names the thing once, the row
   under it is what you actually move around in */
.vt-what .chip { font-size: 12px; padding: 5px 11px; }
/* a variant rides at its step's shoulder — same row, visibly subordinate */
.chip-sub { font-size: 11.5px; padding: 5px 10px; }
/* A step chip leads with the dot its own pipeline card is wearing, in the same
   hues .ps-dot uses — grey waiting · amber next/running · teal done · red
   failed. It is the same statement the card makes, made where the step is
   named, so the chip and the card read as one thing without renaming either. */
.chip-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%;
  margin-right: 6px; vertical-align: 1px; background: var(--text-faint);
  transition: background 0.3s; }
.chip-dot.ready, .chip-dot.running, .chip-dot.blocked { background: var(--accent); }
.chip-dot.done { background: var(--ok); }
.chip-dot.failed { background: var(--bad); }
/* on the chip that IS the selection the fill already carries the accent, so a
   coloured dot on top of it is noise */
.chip[aria-selected="true"] .chip-dot { background: currentColor; opacity: 0.55; }
/* two rows stand taller than one: the download bar sits above whichever it is */
.viewer-wrap:has(.vt-what) .viewer-loading { bottom: 78px; }
.chip {
  background: rgba(10, 11, 15, 0.72); border: 1px solid var(--hair-strong);
  color: var(--text-dim); border-radius: 999px; padding: 7px 13px; font: inherit;
  font-size: 12.5px; font-weight: 600; cursor: pointer; backdrop-filter: blur(8px);
  transition: color 0.15s, border-color 0.15s, background 0.15s, filter 0.15s;
}
.chip:hover { color: var(--text); border-color: var(--accent); }
.chip[aria-selected="true"] { color: #1a0d05; background: var(--accent-grad); border-color: transparent; }
/* the one chip that commits: it carries the accent fill the way a primary
   button does, because that is what it is */
.chip-use { color: #1a0d05; background: var(--accent-grad); border-color: transparent; font-weight: 750; }
.chip-use:hover { color: #1a0d05; }
/* Every chip that wears the accent fill — the one that commits, the selected
   stage tab, a latched multi-select — answers a hover the way the primary
   button does: same fill, a step hotter. The generic .chip:hover above only
   ever restyles ink and border, so there is no background to rescue here; what
   was missing was any acknowledgement at all that the cursor had arrived. */
.chip-use:hover,
.chip[aria-selected="true"]:hover,
.chip[aria-pressed="true"]:hover { filter: brightness(1.07); }
.chip-use:disabled { opacity: 0.45; cursor: not-allowed; filter: none; }

/* credits balance in the topbar */
.chip-balance { font-variant-numeric: tabular-nums; color: var(--text); }
.chip-balance.neg { color: var(--bad); border-color: var(--bad); }

/* ---------------- describe (text-to-image) ---------------- */
#describe-pane textarea {
  width: 100%; background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text); font: inherit;
  font-size: 13.5px; padding: 11px 12px; resize: vertical; min-height: 74px;
  transition: border-color 0.18s, background 0.18s;
}
#describe-pane textarea::placeholder { color: var(--text-faint); }
#describe-pane textarea:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }

/* ---------------- capture (experimental) ---------------- */
/* The "experimental" word is a label, not a decoration: it keeps its own colour
   wherever it appears so nobody mistakes this path for the settled one. */
.exp { color: var(--accent); font-weight: 700; letter-spacing: 0.02em; }
/* on the chosen tab the accent IS the background — the word has to switch to
   the dark ink beside it or it reads as orange on orange */
.seg[aria-selected="true"] .exp { color: #3a1a08; }
#capture-pane { display: flex; flex-direction: column; gap: 10px; }
#capture-drop { min-height: 120px; }
.cap-strip {
  display: flex; flex-wrap: wrap; gap: 4px; padding: 8px; align-content: flex-start;
  max-height: 150px; overflow: auto; overscroll-behavior: contain;
}
.cap-thumb {
  width: 46px; height: 46px; object-fit: cover; border-radius: 5px;
  border: 1px solid var(--hair-strong); background: rgba(0, 0, 0, 0.3);
}
.cap-more { align-self: center; font-size: 12px; color: var(--text-faint); }
.cap-field { display: flex; align-items: center; justify-content: space-between; gap: 10px;
  font-size: 12.5px; color: var(--text-dim); }
.cap-field select {
  background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text); font: inherit;
  font-size: 12.5px; padding: 6px 8px;
}

/* The splat canvas takes the same box the mesh does — under the toolbar (z 8)
   and under the source/candidate overlays (z 5), so nothing that covers the
   mesh stops covering it, and the way BACK is always the toolbar. */
.splat-view { position: absolute; inset: 0; z-index: 4; touch-action: none;
  background: radial-gradient(120% 90% at 50% 15%, #171a22, #0b0d12 70%); }
.splat-view canvas { display: block; width: 100%; height: 100%; touch-action: none; }

/* ---------------- compare ---------------- */
/* Panes in a row, one hairline between them so the two images read as one
   picture cut in half rather than two widgets. Same box as the single viewer —
   over both renderers, under the toolbar, because the toolbar is the way out. */
.compare-view {
  position: absolute; inset: 0; z-index: 5; touch-action: none;
  display: grid; grid-template-columns: repeat(var(--cmp-cols, 2), minmax(0, 1fr));
  gap: 1px; background: var(--hair-strong);
}
.cmp-pane { position: relative; overflow: hidden; min-width: 0;
  background: radial-gradient(120% 90% at 50% 15%, #171a22, #0b0d12 70%); }
.cmp-body { position: absolute; inset: 0; touch-action: none; }
.cmp-body model-viewer, .cmp-body canvas {
  display: block; width: 100%; height: 100%; touch-action: none;
  --poster-color: transparent; background: transparent;
}
/* the pane says which coordinate it IS — a split view whose halves are not
   named is a puzzle, not a comparison */
.cmp-label {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  max-width: calc(100% - 16px); overflow: hidden;
  font-size: 11.5px; padding: 5px 10px; text-align: left; border-radius: 12px;
}
.cmp-label:disabled { cursor: default; opacity: 1; }
.cmp-title { display: block; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; }
/* the same quiet second line the badge carries, for the same reason */
.cmp-made { display: block; margin-top: 1px; font-size: 10.5px;
  font-weight: 600; color: var(--text-faint); white-space: nowrap; }
.chip[aria-selected="true"] .cmp-made { color: inherit; opacity: 0.6; }
.cmp-note { position: absolute; inset: auto 0 0 0; z-index: 2; text-align: center;
  padding: 6px 8px; font-size: 11.5px; color: var(--text-faint);
  background: rgba(10, 11, 15, 0.6); }
/* A/B: one pane, one camera, and the switch under it does the flicking. Two
   live renderers do not fit a phone's VRAM, and flicking between them on
   identical pixels is the better comparison anyway. */
.compare-view.cmp-ab { grid-template-columns: 1fr; }
.vt-lens .chip { font-size: 12px; padding: 5px 11px; }
.vt-ab .chip { font-size: 13.5px; padding: 9px 16px; }

/* ---------------- imagine candidates ---------------- */
.source-view {
  position: absolute; inset: 0; z-index: 5; display: grid; place-items: center;
  padding: 14px; background: rgba(10, 11, 15, 0.9); backdrop-filter: blur(10px);
  touch-action: none;
}
.source-view img { max-width: 100%; max-height: 100%; object-fit: contain;
  border-radius: var(--radius-sm); }

/* the source card carries a picture, so it is taller than its siblings */
.ps-source-thumb {
  /* same rule, same reason: this shows what the model was MADE from, so it must
     not crop it. Capped so a square image does not take over the row. */
  width: 100%; max-width: 200px; aspect-ratio: 1 / 1; object-fit: contain;
  border-radius: 7px; margin: 8px 0 2px; background: rgba(0, 0, 0, .28);
  display: block;
}
.ps-source-prompt {
  font-size: 11.5px; color: var(--text-faint); line-height: 1.4;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;
  overflow: hidden;
}

.candidates {
  position: absolute; inset: 0; z-index: 5; display: grid;
  /* Adaptive columns so four SQUARE tiles fit a wide viewer in one row and a
     narrow one in two — rather than two fixed columns whose cells are wider
     than they are tall, which is what forced the crop. */
  grid-template-columns: repeat(auto-fit, minmax(128px, 1fr));
  align-content: center; gap: 10px; padding: 14px;
  background: rgba(10, 11, 15, 0.88); backdrop-filter: blur(10px); overflow: auto;
  /* the picker grid is a LIST over the viewer — it has to keep scrolling, so it
     opts back in to vertical panning the wrapper just turned off */
  touch-action: pan-y;
  overscroll-behavior: contain;
}
.cand {
  /* the tile matches the image it holds — the generators are asked for 1024x1024
     and deliver it, so a square tile needs no crop and leaves no letterbox */
  aspect-ratio: 1 / 1;
  border: 1.5px solid var(--hair-strong); border-radius: var(--radius-sm);
  overflow: hidden; cursor: pointer; background: #000; padding: 0;
  transition: border-color 0.15s, transform 0.15s var(--ease);
}
/* CONTAIN, never cover: this is the surface you CHOOSE from, and the whole
   square is what goes to Generate. A cropped preview would hide the part of
   the image the mesh is actually built from. */
.cand img { width: 100%; height: 100%; object-fit: contain; display: block; }
.cand.picked { border-color: var(--ok); box-shadow: 0 0 0 3px var(--ok-dim); }

.gg-row { display: flex; gap: 10px; margin: 10px 0 4px; }
.gg-field { flex: 1 1 0; min-width: 0; font-size: 12px; color: var(--text-faint); }
.gg-field select {
  width: 100%; margin-top: 5px; background: rgba(0,0,0,.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text); font: inherit; font-size: 13px; padding: 8px 9px;
}
.gg-field select:focus { outline: none; border-color: var(--accent); }
.gg-hint { font-size: 12px; color: var(--text-dim); margin: 6px 0 2px; }

.scope-chip {
  font-size: 11.5px; font-weight: 700; letter-spacing: .04em;
  color: var(--accent); border: 1px solid var(--accent);
  border-radius: 999px; padding: 4px 11px;
}
.soc-recipe { margin-top: 12px; border-top: 1px solid var(--hair); padding-top: 10px; }
#soc-comments { max-height: 260px; overflow-y: auto; }

.ps-substate { font-size: 12px; font-weight: 650; }

/* the per-version values sit under the dial they belong to */
.sweep-values { display: flex; gap: 6px; padding: 4px 0 8px 14px; border-left: 2px solid var(--hair); }
.sv-input {
  flex: 1 1 0; min-width: 0; background: rgba(0, 0, 0, 0.32);
  border: 1px solid var(--hair); border-radius: 7px; color: var(--text);
  font: inherit; font-size: 12.5px; padding: 6px 8px; text-align: center;
}
.sv-input:focus { outline: none; border-color: var(--accent); }

/* a sub-setting reads as owned by the one above it, and greys out with it */
.field-sub { padding-left: 14px; border-left: 2px solid var(--hair); }
.field-off { opacity: 0.45; }

/* ---------------- takes (compare across re-runs) ---------------- */
/* A filmstrip directly under the viewer: earlier results on the left, the
   current one on the right, and one line naming what changed between them. */
.takes {
  background: var(--panel); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); padding: 12px 14px;
  display: flex; flex-direction: column; gap: 10px;
}
.takes-head { display: flex; align-items: center; gap: 8px; }
.takes-title { font-weight: 650; font-size: 13px; }
.takes-sub { color: var(--text-faint); font-size: 12px; }
.takes-strip { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 2px; }
.take {
  flex: 0 0 auto; width: 96px; padding: 0 0 7px; cursor: pointer;
  background: rgba(0, 0, 0, 0.32); border: 1.5px solid var(--hair-strong);
  border-radius: var(--radius-sm); overflow: hidden; color: var(--text-dim);
  font: inherit; display: flex; flex-direction: column; align-items: center;
  gap: 3px; transition: border-color 0.15s, color 0.15s, transform 0.15s var(--ease);
}
.take:hover { border-color: var(--accent); color: var(--text); transform: translateY(-2px); }
/* takes are 3D captures of the SAME framing, so cover is honest here — the
   crop removes empty backdrop, not content you are choosing between */
.take img { width: 96px; height: 76px; object-fit: cover; display: block; background: #000; }
.take-ph { width: 96px; height: 76px; display: grid; place-items: center;
  background: rgba(255, 255, 255, 0.04); color: var(--text-faint); font-size: 20px; }
.take-cap { font-size: 12px; font-weight: 650; }
.take-sub { font-size: 11px; color: var(--text-faint); font-variant-numeric: tabular-nums; }
.take.active { border-color: var(--accent); color: var(--text); }
.take.active .take-cap { color: var(--accent); }
.takes-diff { display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  border-top: 1px solid var(--hair); padding-top: 10px; }
.td-line { display: flex; flex-wrap: wrap; gap: 6px; flex: 1 1 auto;
  font-size: 12.5px; color: var(--text-dim); }
.td-chip { background: rgba(255, 255, 255, 0.05); border: 1px solid var(--hair);
  border-radius: 999px; padding: 4px 10px; }
.td-chip b { color: var(--text); font-weight: 650; margin-right: 4px; }

/* the run bar: flow sits with the pipeline it governs */
.run-bar { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
.run-note { margin: 0; font-size: 12.5px; color: var(--text-dim); flex: 1 1 220px; }

/* setup blocks re-homed into the Source/Generate cards */
.pstage .mountable { margin-top: 10px; }
.pstage .panel-title.mountable { margin: 12px 0 6px; }
.pstage .dropzone { min-height: 128px; }
.pstage .preset-grid { grid-template-columns: 1fr 1fr; }
.pstage .segmented { width: 100%; }
.pstage .name-row { margin: 10px 0 2px; }
.pstage .link-btn { margin-top: 8px; }
.pstage .ps-run { width: 100%; margin-top: 10px; }

/* ---------------- pipeline ---------------- */
/* A PIPELINE IS A SEQUENCE OF UNEVEN STEPS, so it is laid out as one — a
   vertical stepper on a status rail, not a grid of equal columns. A grid is for
   uniform cells; ours hold anything from a dropzone and five preset tiles to a
   two-line verdict, and forcing those into equal columns is the layout fighting
   the content. Same pattern as GitHub Actions job views, Vercel build steps and
   Blender's modifier stack, for the same reason.
   Collapsed, a row is one line: what it is on the left, what it produced on the
   right. Open one and it expands in place; opening another closes it. */
.pipeline { display: flex; flex-direction: column; gap: 0;
  background: var(--panel); border: 1px solid var(--hair);
  border-radius: var(--radius); overflow: hidden; }
.pstage {
  position: relative; background: transparent; border: 0;
  border-top: 1px solid var(--hair); border-radius: 0;
  padding: 12px 14px 12px 36px;
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  column-gap: 12px;
  transition: background 0.25s;
}
/* clip only while the sweep animation runs — completed cards must not clip
   their hover tooltips (cost estimate) */
.pstage.running { overflow: hidden; }
.pstage .ps-head { display: flex; align-items: center; gap: 9px; }
.ps-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--text-faint);
  flex: none; transition: background 0.3s, box-shadow 0.3s; }
.ps-name { font-weight: 650; font-size: 13.5px; flex: 1; }
.ps-meta { margin-top: 8px; font-size: 12px; color: var(--text-dim); min-height: 16px; font-variant-numeric: tabular-nums; }
.ps-actions { margin-top: 10px; display: flex; align-items: center; flex-wrap: wrap; gap: 10px; min-height: 4px; }
.ps-run { padding: 7px 12px; font-size: 12.5px; }
/* THE RE-RUN IS THE RUN CONTROL, AFTER THE FACT — so it is the same control in
   the same place at the same size: last in the action row, full width, at the
   foot of the card, exactly where "▶ Run Generate" sat while the stage was
   waiting. It used to be a 22px ↻ chip up in the header, the width of the card
   away from where the button it replaces had been, and a maker who had just
   pressed Run once could not find it a second time (UI #1004).
   It is NOT the accent primary: the hue answers "where is the run up to", and a
   finished stage is not the next thing to press. A neutral .btn is the whole
   difference between offering the verb and shouting it.
   `order` rather than DOM position alone, because the row is also filled by
   things that arrive LATER — the submission pill, the export door — and those
   belong beside the downloads they describe, not underneath the CTA. */
.pstage .ps-rerun { width: 100%; order: 1; }
/* the fixture export sits with the downloads — same quiet ghost as the re-run */
.ps-gg {
  border: 1px solid var(--hair); background: transparent; color: var(--text-dim);
  height: 26px; padding: 0 11px; border-radius: 7px; cursor: pointer;
  font: inherit; font-size: 11.5px; font-weight: 600; line-height: 1;
  white-space: nowrap; flex: none; transition: color 0.15s, border-color 0.15s;
}
.ps-gg:hover { color: var(--accent); border-color: var(--accent); }
.ps-gg:disabled { opacity: 0.4; cursor: not-allowed; }
.ps-dl { color: var(--accent); font-size: 12px; font-weight: 600; text-decoration: none; }
.ps-dl:hover { text-decoration: underline; }
/* while a stage runs, the rest of the pipeline is inert — say so visually */
.pstage.dimmed { opacity: 0.45; }
.pstage.done, .pstage.ready { cursor: pointer; }
/* STATE answers "where is the run up to" — it owns the HUE: grey waiting ·
   amber next/running · teal done · red failed, carried by the dot and a tinted
   border. SELECTION answers "what am I looking at" — a different question, so
   it gets a different channel entirely: a neutral bright ring, never a hue.
   Sharing the accent made "next to run" and "on screen" indistinguishable, and
   two lit cards is one too many to read at a glance. */
.pstage.ready { border-color: rgba(255, 122, 47, 0.45); }
.pstage.ready .ps-dot { background: var(--accent); box-shadow: 0 0 10px -2px var(--accent); }
.pstage.viewing { outline: 0; background: rgba(255, 255, 255, 0.06); }
.pstage.viewing::before { background: var(--text); opacity: 1; width: 3px; left: 16px; }
.pstage:first-child { border-top: 0; }

/* the rail: a continuous line the dots sit on, whose segments go teal as
   stages complete, so progress reads vertically without counting anything */
.pstage::before {
  content: ""; position: absolute; left: 17px; top: 0; bottom: 0; width: 2px;
  background: var(--hair);
}
.pstage:first-child::before { top: 18px; }
.pstage:last-child::before { bottom: calc(100% - 20px); }
.pstage.done::before { background: var(--ok); opacity: 0.55; }
.ps-dot { position: absolute; left: 12px; top: 16px; margin: 0; z-index: 1;
  box-shadow: 0 0 0 3px var(--bg-2); }

.ps-head { grid-column: 1; display: flex; align-items: center; gap: 8px; min-width: 0; }
.ps-meta { grid-column: 2; text-align: right; align-self: center;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 46vw; }
.ps-track, .pstage .mountable, .ps-fold, .ps-settings, .ps-actions,
.ps-source-thumb, .ps-source-prompt { grid-column: 1 / -1; }

/* collapsed rows keep their status, result and action; the bulk folds away */
.pstage:not(.open) .mountable,
.pstage:not(.open) .ps-fold,
.pstage:not(.open) .ps-settings { display: none; }
.pstage:not(.open) .ps-source-thumb { display: none; }
.pstage.open { background: rgba(255, 255, 255, 0.022); }
.pstage.done, .pstage.ready { cursor: pointer; }

/* a choice made once, folded to its answer */
.ps-fold { margin-top: 10px; }
.ps-fold > summary {
  cursor: pointer; list-style: none; font-size: 12px; font-weight: 650;
  color: var(--text-dim); padding: 8px 10px; border: 1px solid var(--hair);
  border-radius: var(--radius-sm); background: rgba(255, 255, 255, 0.03);
  display: flex; align-items: center; gap: 8px;
}
.ps-fold > summary::-webkit-details-marker { display: none; }
.ps-fold > summary::after { content: "▸"; margin-left: auto; color: var(--text-faint); }
.ps-fold[open] > summary::after { content: "▾"; }
.ps-fold > summary:hover { border-color: var(--hair-strong); color: var(--text); }
.fold-value { color: var(--accent); font-weight: 700; }
.ps-fold .preset-grid { margin-top: 10px; }

/* a stage's own dials, folded into its card.
   The ROW is the control, not the word in it. The disclosure's padding used to
   sit on the <details>, which left the summary a 17px strip of text: the arrow
   was the only part anyone could reliably hit, and the space beside SETTINGS —
   which looks exactly like the rest of the row — did nothing. The padding moves
   onto the summary, so the whole band from the hairline down is one target, and
   it is a flex row so the marker is a sibling of the word rather than a glyph
   glued to its front. */
.ps-settings { margin-top: 10px; border-top: 1px solid var(--hair); }
.ps-settings > summary {
  cursor: pointer; list-style: none; font-size: 11.5px; font-weight: 700;
  letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-faint);
  display: flex; align-items: center; gap: 6px; padding: 10px 0 8px;
  user-select: none; -webkit-user-select: none;
}
.ps-settings > summary::-webkit-details-marker { display: none; }
.ps-settings > summary::before { content: "▸"; }
.ps-settings[open] > summary::before { content: "▾"; }
.ps-settings > summary:hover { color: var(--text-dim); }
.ps-settings > summary:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
.ps-settings .adv-group { margin: 8px 0 2px; padding: 0; border: 0; }
.ps-settings .adv-cap { display: none; }   /* the card title already names it */

.ps-track { margin-top: 9px; height: 3px; border-radius: 3px;
  background: rgba(255, 255, 255, 0.07); overflow: hidden; }
.ps-fill { height: 100%; width: 0%; border-radius: 3px;
  background: var(--accent-grad); transition: width 1s linear; }
/* An upload reports real bytes, several times a second. The one-second ease
   that suits a stage's estimate makes real progress look like it is lagging. */
.up-track { margin: 10px 0 2px; }
.up-track .ps-fill { transition: width 0.15s linear; }
.pstage.running { border-color: var(--accent); }
.pstage.running .ps-dot { background: var(--accent); box-shadow: 0 0 0 4px var(--glow); animation: pulse 1.2s infinite; }
.pstage.running::after {
  content: ""; position: absolute; left: 0; bottom: 0; height: 2px; width: 100%;
  background: linear-gradient(90deg, transparent, var(--accent), transparent);
  animation: sweep 1.3s linear infinite;
}
.pstage.done { border-color: rgba(51, 214, 160, 0.4); }
.pstage.done .ps-dot { background: var(--ok); box-shadow: 0 0 12px -2px var(--ok); }
.pstage.failed { border-color: rgba(255, 95, 109, 0.5); }
.pstage.failed .ps-dot { background: var(--bad); }
/* blocked = the run never started (no credits). Amber, not red: nothing broke. */
.pstage.blocked { border-color: var(--accent); }
.pstage.blocked .ps-dot { background: var(--accent); }
/* CANCELLED IS ITS OWN STATE, and the palette has to say so (UI #1007). Never
   red — red means something broke, and a run somebody stopped on purpose did
   not. Never green — nothing was produced. So it takes the one channel left
   that reads at a glance: a HOLLOW dot on a strong hairline, drawn rather than
   filled, which sits legibly beside grey (waiting), amber (next/running), teal
   (done) and red (failed). Same distinction every CI conclusion draws between
   `cancelled` and `failure`, for the same reason. */
.pstage.cancelled { border-color: var(--hair-strong); }
.pstage.cancelled .ps-dot {
  background: var(--bg-2);
  box-shadow: 0 0 0 3px var(--bg-2), inset 0 0 0 2px var(--text-faint);
}
.pstage.cancelled .ps-meta { color: var(--text-faint); }
/* Interrupt sits with the progress it stops: quiet, bordered, never red — and
   never disabled while the stage runs, because it is the one control that must
   work at exactly the moment every other one is off. */
.ps-cancel {
  border: 1px solid var(--hair-strong); background: transparent;
  color: var(--text-dim); height: 28px; padding: 0 12px; border-radius: 7px;
  cursor: pointer; font: inherit; font-size: 11.5px; font-weight: 600;
  line-height: 1; white-space: nowrap; flex: none;
  transition: color 0.15s, border-color 0.15s;
}
.ps-cancel:hover { color: var(--text); border-color: var(--text-faint); }
.ps-cancel:disabled { opacity: 0.5; cursor: default; }

/* ---------------- results ---------------- */
.results { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }
.pill {
  display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px;
  border-radius: 999px; font-size: 12.5px; font-weight: 600; border: 1px solid var(--hair);
}
.pill.ok { background: var(--ok-dim); color: var(--ok); border-color: transparent; }
.pill.bad { background: var(--bad-dim); color: var(--bad); border-color: transparent; }
.pill::before { content: "✓"; font-weight: 800; }
.pill.bad::before { content: "✕"; }
/* collapsible result trays — a phone must be able to fold these away so the
   page stays short enough for the viewer to own one-finger drags */
.tray > summary {
  cursor: pointer; list-style: none; user-select: none;
  display: flex; align-items: baseline; margin: 0;
}
.tray > summary::-webkit-details-marker { display: none; }
.tray > summary::before { content: "▸ "; color: var(--text-faint); margin-right: 4px; }
.tray[open] > summary::before { content: "▾ "; }
.tray[open] > summary { margin-bottom: 12px; }
.tray-mini {
  font-size: 11px; margin-left: 8px; font-weight: 600;
  text-transform: none; letter-spacing: normal; color: var(--text-dim);
}
.tray-mini.ok { color: var(--ok); }
.tray-mini.bad { color: var(--bad); }

.measure-grid, .prov { display: grid; grid-template-columns: auto 1fr; gap: 7px 16px; font-size: 12.5px; }
.measure-grid dt, .prov dt { color: var(--text-faint); }
.measure-grid dd, .prov dd { margin: 0; text-align: right; font-family: var(--mono); color: var(--text); }
.prov dd { font-size: 11.5px; word-break: break-all; }
.prov-foot { margin: 14px 0 0; font-size: 11.5px; color: var(--text-faint); line-height: 1.5;
  border-top: 1px solid var(--hair); padding-top: 12px; }

/* ---------------- toast ---------------- */
.toast {
  position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(12px);
  background: var(--panel-solid); border: 1px solid var(--hair-strong); color: var(--text);
  padding: 12px 18px; border-radius: 10px; font-size: 13.5px; z-index: 60; opacity: 0;
  box-shadow: 0 20px 50px rgba(0,0,0,0.5); transition: opacity 0.25s, transform 0.25s; max-width: 90vw;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.err { border-color: var(--bad); }
/* a sticky toast waits for a tap, so it must be tappable */
.toast.sticky { cursor: pointer; }

/* ---------------- run library ---------------- */
.library {
  position: fixed; top: 0; right: 0; height: 100dvh;
  width: min(420px, 92vw);
  background: rgba(15, 17, 23, 0.92);
  backdrop-filter: blur(14px);
  border-left: 1px solid var(--hair-strong);
  z-index: 50; padding: 18px; overflow-y: auto;
  animation: slideInRight 0.28s var(--ease) both;
}
.lib-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.5); z-index: 45; }
.lib-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 14px; }
.lib-grid { display: grid; gap: 12px; }
.lib-empty { text-align: center; color: var(--text-faint); font-size: 13px; padding: 40px 12px; }
.lib-card {
  position: relative;
  width: 100%; display: flex; gap: 12px; align-items: center; text-align: left;
  background: var(--panel); border: 1px solid var(--hair); border-radius: var(--radius-sm);
  padding: 10px; cursor: pointer; font: inherit; color: inherit;
  transition: border-color 0.18s, background 0.18s;
}
.lib-thumb {
  width: 56px; height: 56px; border-radius: 8px; overflow: hidden; flex: none;
  background: repeating-conic-gradient(#15171e 0 25%, #101219 0 50%) 0 0 / 16px 16px;
}
.lib-thumb img { width: 100%; height: 100%; object-fit: contain;
  opacity: 0; transition: opacity 0.3s var(--ease); }
.lib-thumb img.loaded { opacity: 1; }
.lib-thumb-empty { width: 100%; height: 100%; display: grid; place-items: center;
  color: var(--text-faint); font-size: 20px; }
.lib-info { min-width: 0; flex: 1; }
.lib-name { font-weight: 650; font-size: 13.5px; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; }
.lib-date { font-size: 11.5px; color: var(--text-faint); margin: 2px 0 6px; }
.lib-cost { font-variant-numeric: tabular-nums; }
.lib-dots { display: flex; gap: 5px; }
.lib-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--text-faint); opacity: 0.35; }
.lib-dot.ok { background: var(--ok); opacity: 1; }
.lib-dot.bad { background: var(--bad); opacity: 1; }
.lib-pill { margin-top: 8px; }
/* Submission state (ASK-102 S3). It leads with its own ⬢ and most of its
   states are not verdicts at all, so it drops the pill's ✓ — "✓ in review"
   would claim an answer nobody has given yet. */
.sub-pill::before { content: none; }
.sub-pill { font-size: 12px; padding: 5px 10px; }
.sub-reason {
  margin-top: 4px; font-size: 11.5px; font-style: italic; color: var(--text-faint);
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The decision's date sits under the verdict and its reason, quieter than both:
   it qualifies the ruling, it is not the ruling. */
.sub-when { margin-top: 3px; font-size: 11px; color: var(--text-faint); opacity: 0.8; }
/* The door out (ASK-103 slice E) sits under the whole verdict block. Quiet and
   underlined rather than a button: it leads somewhere else, and it should not
   compete with the actions that act on THIS run. */
.sub-queue {
  display: inline-block; margin-top: 5px; font-size: 11.5px;
  color: var(--text-faint); text-decoration: underline;
  text-underline-offset: 2px; opacity: 0.85;
}
.sub-queue:hover { color: var(--text); opacity: 1; }
/* loading states: placeholders on a cold open, dimmed real cards on a refetch —
   the grid is never blank, so nothing ever reads as "my models are missing" */
.lib-skel { pointer-events: none; }
.lib-skel .lib-thumb, .skel-line {
  background: linear-gradient(100deg,
    rgba(255, 255, 255, 0.05) 40%, rgba(255, 255, 255, 0.12) 50%, rgba(255, 255, 255, 0.05) 60%);
  background-size: 200% 100%;
  animation: shimmer 1.2s linear infinite;
}
.skel-line { height: 11px; border-radius: 5px; margin: 5px 0; }
.skel-line.w60 { width: 60%; }
.skel-line.w40 { width: 40%; }
.skel-line.w80 { width: 80%; }
.lib-grid.lib-stale { opacity: 0.55; pointer-events: none; transition: opacity 0.2s; }
.lib-spin { flex: none; margin-right: 8px;
  border-color: rgba(255, 255, 255, 0.18); border-top-color: var(--accent); }
.lib-del {
  flex: none; border: 1px solid var(--hair); background: transparent; color: var(--text-faint);
  width: 28px; height: 28px; border-radius: 7px; cursor: pointer; font-size: 13px;
  transition: color 0.15s, border-color 0.15s, width 0.2s var(--ease);
}
.lib-del:hover { color: var(--bad); border-color: var(--bad); }
/* rename sits beside delete and behaves like it, minus the danger colour */
.lib-ren {
  flex: none; border: 1px solid var(--hair); background: transparent; color: var(--text-faint);
  width: 28px; height: 28px; border-radius: 7px; cursor: pointer; font-size: 13px;
  transition: color 0.15s, border-color 0.15s;
}
.lib-ren:hover { color: var(--accent); border-color: var(--accent); }
.lib-name-edit {
  width: 100%; background: rgba(0, 0, 0, 0.45); border: 1px solid var(--accent);
  border-radius: 6px; color: var(--text); font: inherit; font-size: 13px;
  font-weight: 650; padding: 3px 7px; margin-bottom: 3px;
}
.lib-name-edit:focus { outline: none; }

/* the name field in the control panel */
.name-row { display: flex; align-items: center; gap: 10px; margin: 12px 0 4px; }
.name-label { font-size: 12px; color: var(--text-faint); flex: none; }
.name-row input {
  flex: 1 1 auto; min-width: 0; background: rgba(0, 0, 0, 0.32);
  border: 1px solid var(--hair); border-radius: var(--radius-sm);
  color: var(--text); font: inherit; font-size: 13px; padding: 8px 10px;
  transition: border-color 0.18s, background 0.18s;
}
.name-row input::placeholder { color: var(--text-faint); }
.name-row input:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }
.lib-del.confirm {
  width: auto; padding: 0 10px; color: #fff; background: var(--bad); border-color: transparent;
  font-size: 11.5px; font-weight: 700;
}
/* community "All" view — every account browses every model */
.seg-mini { padding: 3px; margin-left: auto; margin-right: 8px; flex: none; }
.seg-mini .seg { padding: 5px 12px; font-size: 12px; }
.lib-owner {
  font-size: 11px; color: var(--text-faint); margin-bottom: 6px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.lib-stars { display: block; font-size: 11.5px; color: var(--text-dim); margin-bottom: 6px; }
.lib-stars b { color: var(--accent); font-weight: 600; }
/* scene budget: one plain number on the card, the meter in the asset panel */
.lib-scene { font-size: 11.5px; color: var(--text-dim); display: block; margin-top: 2px; }
.scene-over { color: var(--bad); }
.meter {
  height: 8px; border-radius: 6px; background: rgba(255, 255, 255, 0.07);
  overflow: hidden; margin: 8px 0 6px;
}
.meter-fill {
  height: 100%; border-radius: 6px; background: var(--accent-grad);
  transition: width 0.4s var(--ease);
}
.meter-fill.over { background: var(--bad); }
.scene-line { font-size: 13px; color: var(--text-dim); line-height: 1.5; }
/* someone else's run: openable, never reloadable — the chip says so up front */
.lib-view {
  position: absolute; top: 7px; right: 7px;
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--text-faint); border: 1px solid var(--hair);
  border-radius: 4px; padding: 1px 5px;
}
.lib-foreign .lib-name { padding-right: 64px; }
.lib-more {
  flex: none; border: 1px solid var(--hair); background: transparent;
  color: var(--text-faint); border-radius: 7px; padding: 6px 8px; cursor: pointer;
  font: inherit; font-size: 11px; white-space: nowrap;
  transition: color 0.15s, border-color 0.15s;
}
.lib-more:hover { color: var(--accent); border-color: var(--accent); }

/* ---------------- settings drawer ---------------- */
/* #settings reuses .library for the drawer shell; these are its own innards. */
.set-title {
  font-size: 12px; text-transform: uppercase; letter-spacing: 0.13em;
  color: var(--text-faint); font-weight: 700; margin: 24px 0 10px;
}
.set-title:first-of-type { margin-top: 4px; }
.set-error {
  color: var(--bad); background: var(--bad-dim); border-radius: var(--radius-sm);
  font-size: 12.5px; line-height: 1.45; padding: 9px 11px; margin: 10px 0 0;
}
.key-row {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  border: 1px solid var(--hair); border-radius: var(--radius-sm);
  background: rgba(255, 255, 255, 0.015); padding: 9px 10px 9px 12px;
}
.key-state { font-size: 13px; color: var(--text-dim); }
.key-state b { font-family: var(--mono); color: var(--text); font-weight: 600; }
.key-entry { display: flex; gap: 8px; }
.key-entry input {
  flex: 1; min-width: 0; background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text); font: inherit; font-size: 13.5px;
  padding: 10px 12px; transition: border-color 0.18s, background 0.18s;
}
.key-entry input::placeholder { color: var(--text-faint); }
.key-entry input:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }
.set-fine {
  margin: 16px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--text-faint);
  border-top: 1px solid var(--hair); padding-top: 12px;
}

/* ---------------- credits (settings drawer) ---------------- */
.cred-balance {
  font-size: 28px; font-weight: 800; margin: 4px 0 10px;
  font-variant-numeric: tabular-nums; letter-spacing: -0.02em;
}
.cred-balance.neg { color: var(--bad); }
.cred-sub {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--text-faint); font-weight: 700; margin: 14px 0 2px;
}
.cred-row {
  display: flex; align-items: baseline; gap: 10px;
  font-size: 12px; padding: 7px 0; border-bottom: 1px solid var(--hair);
  color: var(--text-dim); line-height: 1.4;
}
.cred-when { color: var(--text-faint); flex: none; min-width: 62px; }
.cred-what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cred-amt { flex: none; font-variant-numeric: tabular-nums; font-family: var(--mono); font-size: 11.5px; }
.cred-amt.pos { color: var(--ok); }
.cred-empty { font-size: 12px; color: var(--text-faint); padding: 10px 0; }

.cred-admin { margin-top: 4px; }
.cred-scroll { overflow-x: auto; }
.cred-users { width: 100%; border-collapse: collapse; font-size: 12px; }
.cred-users tr { border-bottom: 1px solid var(--hair); cursor: pointer; }
.cred-users td { padding: 6px 8px 6px 0; }
.cu-email {
  max-width: 190px; overflow: hidden; text-overflow: ellipsis;
  white-space: nowrap; color: var(--text-dim);
}
.cu-bal { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; white-space: nowrap; }
.cu-bal.neg { color: var(--bad); }
.cu-add { padding: 4px 9px; font-size: 11.5px; }
.cred-form { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
.cred-form input {
  flex: 1 1 100%; min-width: 0; background: rgba(0, 0, 0, 0.32);
  border: 1px solid var(--hair); border-radius: var(--radius-sm); color: var(--text);
  font: inherit; font-size: 13.5px; padding: 9px 11px;
}
.cred-form #cred-amount { flex: 0 0 110px; }
.cred-form #cred-note { flex: 1 1 120px; }
.cred-form input::placeholder { color: var(--text-faint); }
.cred-form input:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }

/* ---------------- asset panel (community view of one model) ---------------- */
/* #asset reuses .library for the drawer shell (and its phone bottom-sheet). */
.asset-sub { font-size: 12px; color: var(--text-faint); margin: -6px 0 14px; }
.asset-note {
  font-size: 12px; color: var(--text-dim); background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--hair); border-radius: var(--radius-sm);
  padding: 9px 11px; margin: 0 0 4px;
}
.stars { display: flex; gap: 4px; }
.star {
  background: none; border: none; font-size: 28px; line-height: 1;
  color: var(--text-faint); cursor: pointer; padding: 6px;
}
.star.on { color: var(--accent); text-shadow: 0 0 12px var(--glow); }
.star:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
.asset-avg { font-size: 12px; color: var(--text-faint); margin-top: 4px; }

.cmt-list { max-height: 240px; overflow-y: auto; margin-bottom: 10px; }
.cmt {
  padding: 8px 0; border-bottom: 1px solid var(--hair);
  font-size: 12.5px; line-height: 1.45;
}
.cmt.mine { border-left: 2px solid var(--accent); padding-left: 8px; }
.cmt-meta { font-size: 11px; color: var(--text-faint); margin-bottom: 2px; }
.cmt-text { overflow-wrap: anywhere; }
.cmt-form { display: grid; gap: 8px; justify-items: end; }
.cmt-form textarea { width: 100%; }

#asset-settings dd { text-align: right; }
#asset-settings .asset-prompt {
  grid-column: 1 / -1; text-align: left; font-family: inherit; font-style: italic;
  color: var(--text-dim); white-space: pre-wrap; overflow-wrap: anywhere; word-break: normal;
}

/* ---------------- GG fixture export drawer ---------------- */
/* #gg-export reuses .library for the drawer shell; the fields reuse .field /
   .field-stack from the controls panel so the two read as one app. */
.field-stack .gg-in {
  width: 100%; background: rgba(0, 0, 0, 0.32); border: 1px solid var(--hair);
  border-radius: var(--radius-sm); color: var(--text); font: inherit;
  font-size: 13.5px; padding: 10px 12px;
  transition: border-color 0.18s, background 0.18s;
}
.field-stack .gg-in::placeholder { color: var(--text-faint); }
.field-stack .gg-in:focus { outline: none; border-color: var(--accent); background: rgba(0, 0, 0, 0.45); }
.gg-hint { margin: 0; font-size: 11.5px; color: var(--text-faint); }
.gg-fit { font-size: 12px; margin: 6px 0 12px; color: var(--text-dim); line-height: 1.45; }
.gg-fit.ok { color: var(--ok); }
.gg-fit.bad { color: var(--bad); }
/* a class whose own measured pack is over its cap — still selectable (the red
   line explains it), never the class the export can run at */
.seg.over small { color: var(--bad); }
/* multi-select: the same chip, latched rather than exclusive */
.gg-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.gg-chips .chip { backdrop-filter: none; background: rgba(255, 255, 255, 0.03); }
.chip[aria-pressed="true"] {
  color: #1a0d05; background: var(--accent-grad); border-color: transparent;
}
#gg-export .advanced { margin-top: 20px; }
#btn-gg-run { margin-top: 18px; padding: 13px; }
/* The export's answer, laid out as what it is: a VERDICT and, when there is
   one, the ACTION that resolves it. Two blocks, each the full width of the
   panel, with real space between them — flush against each other they read as
   one control with a red caption, which is how "red error message under run and
   re run message under run" became a single unreadable slab (UI #1012). The
   panel grows taller to fit them and never wider (#1000's fold work stands).
   Margins live on the children so an outcome with nothing to say takes no
   space at all, and the flex gap carries the separation when both are up. */
/* scroll-margin so being scrolled to lands it clear of the fold rather than
   flush against it — a verdict whose last line is the last row of pixels is
   readable by measurement and not by eye. */
.gg-outcome { display: flex; flex-direction: column; gap: 16px;
  scroll-margin: 12px 0 20px; }
.gg-outcome > .set-error {
  margin: 16px 0 0; border-left: 3px solid var(--bad);
}
.gg-outcome > .btn { margin-top: 16px; }
.gg-outcome > .set-error:not([hidden]) ~ .btn { margin-top: 0; }

/* ---------------- validator messages ---------------- */
.vmsgs { margin-top: 12px; border-top: 1px solid var(--hair); padding-top: 10px; }
.vmsgs summary {
  cursor: pointer; color: var(--text-dim); font-size: 12px; font-weight: 600;
  list-style: none; user-select: none;
}
.vmsgs summary::-webkit-details-marker { display: none; }
.vmsg {
  font-size: 12px; padding: 7px 0; border-bottom: 1px solid var(--hair); line-height: 1.45;
}
.vmsg-sev {
  display: inline-block; border-radius: 4px; padding: 1px 6px; font-size: 9.5px;
  font-weight: 800; letter-spacing: 0.08em; margin-right: 7px;
}
.vmsg-sev.sev0 { color: var(--bad); background: var(--bad-dim); }
.vmsg-sev.sev1 { color: var(--accent); background: rgba(255, 122, 47, 0.14); }
.vmsg-sev.sev2 { color: var(--text-faint); }
.vmsg-code { font-family: var(--mono); font-size: 11px; color: var(--text-dim); margin-right: 6px; }
.vmsg-ptr {
  font-family: var(--mono); font-size: 10.5px; color: var(--text-faint); display: block;
  margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------------- animations ---------------- */
@keyframes fadeInUp { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes slideInRight { from { opacity: 0; transform: translateX(12px); } to { opacity: 1; transform: none; } }
@keyframes slideUpSheet { from { opacity: 0; transform: translateY(18px); } to { opacity: 1; transform: none; } }
@keyframes pulse { 0%, 100% { box-shadow: 0 0 0 4px var(--glow); } 50% { box-shadow: 0 0 0 7px rgba(255,122,47,0.08); } }
@keyframes sweep { from { transform: translateX(-100%); } to { transform: translateX(100%); } }
@keyframes float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-8px); } }
@keyframes rot { to { transform: rotate(360deg); } }
@keyframes shimmer { to { background-position: -200% 0; } }
@keyframes readyGlow {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 122, 47, 0); }
  50% { box-shadow: 0 0 22px -6px var(--glow); }
}

/* ---------------- pointer capability ---------------- */
/* Hover lifts and accent highlights are mouse affordances. A touch tap latches
   :hover until the next tap elsewhere, so they are scoped to real pointers. */
@media (hover: hover) {
  .cand:hover { border-color: var(--accent); transform: translateY(-2px); }
  .lib-card:hover { border-color: var(--accent); }
}
@media (hover: none) {
  /* Unlatching a tap-held hover means putting each control back to its RESTING
     look, which is not the same look for all of them: the neutral button's rest
     is the faint film, the accent ones' rest is the accent. Handing every .btn
     the neutral background here would leave a tapped Forge greyed out until the
     next tap somewhere else — the desktop bug, latched. */
  .btn:not(.btn-primary):hover { transform: none; background: rgba(255, 255, 255, 0.04); }
  .btn-primary:hover { transform: none; filter: none; box-shadow: 0 8px 26px -6px var(--glow); }
  .btn-ghost:hover { background: transparent; }
  .dropzone:hover { border-color: var(--hair-strong); background: rgba(255, 255, 255, 0.015); }
  .chip:hover { color: var(--text-dim); border-color: var(--hair-strong); }
  /* same story on the chips that carry the accent fill: the neutral reset would
     put dim grey ink on orange and hold it there */
  .chip-use:hover,
  .chip[aria-selected="true"]:hover,
  .chip[aria-pressed="true"]:hover { color: #1a0d05; border-color: transparent; filter: none; }
  .lib-del:hover { color: var(--text-faint); border-color: var(--hair); }
  .lib-more:hover { color: var(--text-faint); border-color: var(--hair); }
}

/* ---------------- responsive ---------------- */
@media (max-width: 980px) {
  .layout { grid-template-columns: 1fr; gap: 16px; padding: 16px; }
  /* Setup leads while there is nothing to look at — the source picker is the
     only thing you can act on, so it IS the hero. The moment a mesh exists the
     order flips: the model becomes the first thing on screen and the controls
     move under it, because from then on every control exists to change the
     thing above it. */
  .controls { order: -1; }

  /* `display: contents` dissolves the stage wrapper so the viewer, the pipeline
     and the controls become siblings in ONE column. That is what lets the viewer
     stay pinned while you scroll the dials — sticky can only hold inside its own
     containing block, and inside .stage it would let go the moment you reached
     the controls, which is exactly where you want to still see the model. */
  .layout.has-model .stage { display: contents; }
  .layout.has-model .viewer-wrap { order: -3; }
  .layout.has-model .takes      { order: -2; }
  .layout.has-model .pipeline   { order: -1; }
  .layout.has-model .controls   { order: 0; }
  .layout.has-model .results    { order: 1; }
  .layout.has-model > .stage > * { min-width: 0; }

  .layout.has-model .viewer-wrap {
    position: sticky; top: var(--stick-top, var(--topbar-h)); z-index: 6;
    box-shadow: 0 16px 34px -20px rgba(0, 0, 0, 0.92);
    /* A pinned viewer earns its space in the same way the phone one does: 4/3
       across a ~900px window is 650px of model over a window that has to also
       show the controls the pinning exists to let you scroll. Half the viewport
       keeps the model the hero and still lets the first controls peek under it,
       which is the whole point of pinning rather than scrolling it away.
       The band's own `min-height` below is the floor, so this needs none.
       `width: 100%` is what makes the cap a CROP rather than a shrink: with the
       stage dissolved this box is a grid item, and a stretched item whose height
       gets clamped has its width pulled in to keep the ratio instead — a 600px
       viewer floating in an 868px column. A definite width settles the inline
       axis first, and the cap then only ever takes height off. */
    --viewer-cap: 50dvh;
    width: 100%;
  }
  .results { grid-template-columns: 1fr; }
  .viewer-wrap { aspect-ratio: 4 / 3; min-height: 320px; }
}

/* phones: one adaptive UI, not a separate mobile skin */
@media (max-width: 640px) {
  .layout { grid-template-columns: 1fr; gap: 14px; padding: 12px; }

  /* topbar stays a single row; identity lives in Settings on small screens */
  .topbar { padding: 10px 12px; gap: 8px; }
  .topbar {
    padding-left: max(12px, env(safe-area-inset-left));
    padding-right: max(12px, env(safe-area-inset-right));
  }
  .activity { padding: 7px 12px; }
  .brand-sub, .user-email { display: none; }
  .brand { gap: 8px; }
  .user { gap: 6px; }
  /* the balance is small enough to keep even here — it's the one number that
     decides whether a run will start */
  .chip-balance { padding: 7px 10px; font-size: 12px; }
  .cu-email { max-width: 150px; }

  .viewer-wrap { aspect-ratio: 1 / 1; min-height: 300px; }
  /* A pinned viewer has to earn its space: square is right when it is the thing
     you are looking at, too tall when it is a header over the controls you came
     to change. Leading = shorter, so the page under it stays usable. */
  .layout.has-model .viewer-wrap {
    aspect-ratio: auto; height: 38dvh; min-height: 210px;
  }

  .pstage { padding: 11px 12px 11px 34px; }
  .ps-name { font-size: 13px; }
  .results { grid-template-columns: 1fr; }

  .candidates { gap: 8px; padding: 10px; }

  /* the preset grid stays two columns — one column turns the panel into a
     scroll marathon before anything has been chosen */
  .preset-grid { grid-template-columns: 1fr 1fr; }
  .preset { padding: 9px; }
  .wizard-card { width: min(360px, 92vw); padding: 18px; }

  /* drawers become bottom sheets — thumb-reachable, no horizontal slide */
  .library, #settings {
    width: 100vw; height: min(78dvh, 100dvh);
    top: auto; right: 0; bottom: 0; left: 0;
    border-left: none; border-top: 1px solid var(--hair-strong);
    border-radius: 16px 16px 0 0;
    animation: slideUpSheet 0.28s var(--ease) both;
  }
  .library, #settings {
    padding-left: max(16px, env(safe-area-inset-left));
    padding-right: max(16px, env(safe-area-inset-right));
    padding-bottom: calc(16px + env(safe-area-inset-bottom));
  }
  .toast { bottom: calc(18px + env(safe-area-inset-bottom)); }

  /* touch targets */
  .btn-sm { min-height: 40px; }
  .seg { padding: 11px 8px; }
  .seg-mini .seg { padding: 9px 12px; }
  .chip { padding: 9px 14px; }
  .ps-run { min-height: 38px; }
  .ps-cancel { min-height: 38px; }
  .lib-del { width: 34px; height: 34px; }
  .lib-more { min-height: 34px; }
  /* 16px is the threshold below which iOS Safari zooms on focus */
  input, select, textarea,
  .field input, .field select,
  .signin-form input, .key-entry input, #describe-pane textarea { font-size: 16px; }

  /* Forge stays reachable while the long controls panel scrolls past */
  .btn-forge {
    position: sticky; bottom: calc(10px + env(safe-area-inset-bottom)); z-index: 15;
  }

  /* belt and braces for touch UAs that still report hover */
  .cand:hover { transform: none; }
  .lib-card:hover { border-color: var(--hair); }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 0.001ms !important; animation-iteration-count: 1 !important;
      transition-duration: 0.001ms !important; }
}

/* Viewing history (ASK-111 finding #3): the stage chips describe the pipeline
   HEAD, and while the viewer shows an earlier take they visibly detach rather
   than sit there claiming the two are the same artifact. */
#vt-tabs.history .chip-dot { opacity: 0.3; }
#vt-tabs.history > button { opacity: 0.55; }
