/* ---------------------------------------------------------------------------
   Mesh Diet — all styling lives here. To restyle the app, this is the only
   file you need to open.

   COLOUR SYSTEM. One neutral ramp, one accent, nothing else.

   The neutrals are TRUE neutral — red, green and blue are equal in every one
   of them. That is the Nike relationship: pure black, pure white, and greys
   with no cast at all, so the only colour anywhere on screen is the accent and
   the model itself. An earlier version tinted the greys slightly cool, which
   is the usual dark-UI trick, and next to a saturated accent it reads as dirty
   rather than as considered.

   The ramp starts at literal #000000 rather than a near-black. On a dark UI
   that is a real decision, not a shortcut: pure black gives the volt accent and
   white type the maximum separation available, and it is what makes the shell
   recede behind the 3D view instead of competing with it.

   The accent is VOLT — the yellow-green Nike puts on black. It is the single
   most legible colour there is against black (a luminance near white's, at
   full saturation), it takes BLACK ink on top rather than white, and it does
   not appear in any model anyone will load, so nothing in the viewport can be
   mistaken for UI or the reverse.

   Every pairing that carries meaning is MEASURED with the WCAG formula, not
   eyeballed. These are read back out of the live stylesheet, not estimated —
   the estimates were wrong by up to 1.4 the last time anyone checked. If you
   change --bg-1 or the accent, re-measure.

     --fg-0 on --bg-1   19.44:1     --fg-1 on --bg-1     9.27:1
     --fg-1 on --bg-2    8.39:1     --fg-1 on --bg-3     7.22:1
     --fg-2 on --bg-1    5.63:1     --fg-2 on --bg-2     5.09:1
     --accent on --bg-1 15.60:1     --accent on --bg-0  16.86:1
     --accent-ink on --accent      16.86:1
     --line-2 on --bg-2  3.30:1     --line-1 on --bg-1   1.28:1

   Body and secondary text clear 7:1 on every surface it can land on, --bg-3
   included — that one is the hover state and is the tightest at 7.22:1.
   Small-caps section labels (--fg-2) clear 4.5:1, the right floor for text
   that is short, uppercase and never load-bearing.

   --line-2 is the CONTROL EDGE and clears 3:1 on --bg-2, so buttons, selects
   and inputs wear it. --line-1 is decorative separation at 1.28:1 and must
   never be the only thing marking something interactive; it is for the
   hairlines between sections.

   WHITE ON THE ACCENT MEASURES 1.25:1 — invisible. That is not a flaw in the
   colour, it is what saturated volt is: its luminance is near white's. Every
   accent surface therefore takes --accent-ink, which is pure black, at
   16.86:1. If you ever find white text on a volt fill, it is a bug.

   TYPE. One scale, six sizes, no half-pixels. Sizes below 11px are not used
   anywhere — if something needs to be smaller, it needs to be shorter instead.
--------------------------------------------------------------------------- */

:root {
  /* surfaces, darkest first. True neutral: R = G = B in every one. */
  --bg-0:   #000000;
  --bg-1:   #0d0d0d;
  --bg-2:   #191919;
  --bg-3:   #262626;

  /* lines: -1 is decorative separation, -2 is a real control edge */
  --line-1: #262626;
  --line-2: #6b6b6b;

  /* text, brightest first */
  --fg-0:   #ffffff;
  --fg-1:   #b3b3b3;
  --fg-2:   #8a8a8a;

  /* Volt. --accent-ink is the text colour that goes ON the accent, and it is
     BLACK — volt is bright enough that white on it is unreadable (1.2:1). */
  --accent:      #c9f73f;
  --accent-hi:   #d9ff66;
  --accent-dim:  #2e3a12;
  --accent-ink:  #000000;

  --ok:    #c9f73f;
  --warn:  #ffc043;
  --error: #ff6b5e;

  /* Neutral mid-grey behind anything showing an image or a model, so both
     white and black content show their edges. Do not tint this. */
  --viewport-bg: #808080;

  /* Type scale. Every font-size in this file is one of these. */
  --t-xs:   11px;    /* mono metadata, chips, section headers */
  --t-sm:   12px;    /* the workhorse: control labels, buttons, body */
  --t-md:   13px;    /* section titles, emphasis */
  --t-lg:   14px;    /* rare — panel headings */
  --t-xl:   16px;
  --t-2xl:  20px;

  /* CONTROL HEIGHT. One number, because a toolbar reads as built rather than
     assembled only when every control in it is the same height. 28px is the
     design-tool convention — Figma, Blender and After Effects all sit within a
     pixel of it — and it is the height at which a 12px label still has room to
     breathe. Buttons were 38px here and looked like a web form. */
  --h:      28px;
  --h-sm:   22px;

  --r:      6px;
  --r-sm:   4px;
  --font:   system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --mono:   ui-monospace, "Cascadia Code", "SF Mono", Menlo, Consolas, monospace;

  --shadow: 0 12px 32px rgba(0, 0, 0, .55);
}

* { box-sizing: border-box; }

/* TRAP — do not remove.
   The `hidden` attribute gets `display: none` from the browser's own default
   stylesheet, which ANY class rule that sets `display` will beat on
   specificity. Several elements here are laid out with `display: grid` or
   `display: flex`, and each of those silently made `el.hidden = true` do
   nothing — the empty-state panel stayed on top of the 3D view forever.
   This rule makes `hidden` mean hidden regardless of what else is declared. */
[hidden] { display: none !important; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg-0);
  color: var(--fg-0);
  font: 14px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
}
body.is-modal { overflow: hidden; }

::selection { background: var(--accent); color: var(--accent-ink); }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* --- shell ---------------------------------------------------------------- */

#app {
  display: grid;
  grid-template-columns: 316px minmax(0, 1fr) 336px;
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "top   top    top"
    "left  stage  right"
    "bar   bar    bar";
  height: 100%;
}

/* Deliberately slim. The header carries a logo and one reassurance; every
   pixel it takes is a pixel off the 3D view, which is the thing people
   actually came for. The tagline that used to live here is gone — it was
   product copy on a tool the user has already opened. */
#topbar {
  grid-area: top;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 20px;
  padding: 0 16px;
  height: 46px;
  background: var(--bg-1);
  border-bottom: 1px solid var(--line-1);
  /* The privacy panel hangs out of the header's 46px, so this must sit above
     the stage in the stacking order — the stage comes later in the DOM and
     would otherwise paint over it. No `overflow` here, for the same reason. */
  position: relative;
  z-index: 20;
}

/* The masthead link. It must not look like a link — no underline, no link
   colour — but it must still behave like one, so it keeps a focus ring and
   gets a hover state that reads as "this is clickable" without being loud.
   The negative margin plus padding gives it a comfortable hit area without
   pushing the logo off its optical position. */
.brand {
  display: inline-flex; align-items: center; gap: 9px; min-width: 0;
  margin: -4px -8px; padding: 4px 8px;
  border-radius: var(--r-sm);
  text-decoration: none; color: inherit;
  transition: background .12s;
}
.brand:hover { background: var(--bg-2); }
.brand:active { background: var(--bg-3); }

.brand-mark {
  width: 24px; height: 24px; flex: none;
  color: var(--accent);
  transition: transform .12s ease;
}
.brand:hover .brand-mark { transform: scale(1.06); }
/* The wordmark's height is what locks it to the mark; its width follows from
   the viewBox aspect. Nothing here sets a font, because there isn't one.
   13px of cap height against a 24px mark — heavy oblique caps carry at a
   smaller size than the monoline version they replaced. */
.wordmark { margin: 0; display: flex; line-height: 0; }
.wordmark svg { height: 13px; width: auto; display: block; }

/* Available to assistive tech, invisible on screen. */
.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* -- the privacy pill and its panel ---------------------------------------
   Read-once reference — the privacy promise and the maker's mark — does not
   deserve permanent vertical space. One line, expanding on demand into a panel
   that is absolutely positioned, so the header is 46px open or closed. */
.privacy { position: relative; }

.privacy-note {
  appearance: none;
  display: inline-flex; align-items: center; gap: 6px;
  height: var(--h-sm); padding: 0 10px 0 8px;
  font: inherit; font-size: var(--t-xs);
  color: var(--fg-1);
  background: var(--bg-2);
  border: 1px solid var(--line-1);
  border-radius: 100px;
  white-space: nowrap;
  cursor: default;
  transition: border-color .12s, color .12s;
}
.privacy:hover .privacy-note,
.privacy:focus-within .privacy-note { color: var(--fg-0); border-color: var(--line-2); }

.privacy-icon { width: 12px; height: 12px; color: var(--accent); flex: none; }

/* THE BRIDGE lives on the container, never on the panel. An absolutely
   positioned descendant still counts toward an ancestor's scrollHeight, so
   hanging it off the panel invents a scrollbar on content that fits. Here it
   simply spans the gap so the pointer can cross into the panel without the
   hover dropping. */
.privacy::after {
  content: ""; position: absolute;
  top: 100%; right: 0; width: 322px; height: 10px;
  pointer-events: none;
}
.privacy:hover::after { pointer-events: auto; }

.privacy-panel {
  position: absolute; top: calc(100% + 9px); right: 0; z-index: 60;
  /* Wide enough that the credit plate sets on ONE line at the scale's smallest
     size. Narrowing this would either wrap the mark or tempt a sub-11px font,
     and the type scale has no sizes below 11px on purpose. */
  width: 322px; padding: 12px 13px 11px;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  font-size: var(--t-sm); line-height: 1.5; color: var(--fg-1);
  text-align: left;
  opacity: 0; visibility: hidden; transform: translateY(-3px);
  transition: opacity .12s ease, transform .12s ease, visibility .12s;
}
/* `:focus-within` as well as `:hover` — without it the panel is unreachable by
   keyboard, and invisible on touch, where hover does not exist at all. */
.privacy:hover .privacy-panel,
.privacy:focus-within .privacy-panel {
  opacity: 1; visibility: visible; transform: translateY(0);
}
.privacy-panel p { margin: 0; }

/* The maker's mark. Uppercase letterspaced mono, centred under a rule — set
   as a stamped plate, not a sentence. It inherits everything else from the
   panel on purpose: provenance should be findable, not announced. */
.privacy-panel .credit {
  margin: 11px 0 0;
  padding-top: 10px;
  border-top: 1px solid var(--line-1);
  font-family: var(--mono);
  font-size: var(--t-xs); letter-spacing: .055em; line-height: 1.7;
  color: var(--fg-2);
  text-align: center;
  white-space: nowrap;
}
/* THE PLATE IS ONE UNIFORM MARK AT REST — same grey, no underline, no weight
   change, nothing to say "these two words are links". Both an underline and a
   brighter colour were tried and both were wrong for the same reason: they cut
   a single stamp into three pieces and turn it into a sentence with hyperlinks
   in it, which is the opposite of what a maker's mark is.

   The links are still links. They reveal themselves on hover and on keyboard
   focus, and the cursor changes, which is enough — provenance should be
   findable, not announced. */
.credit a {
  color: inherit;
  text-decoration: none;
  transition: color .12s;
}
.credit a:hover,
.credit a:focus-visible { color: var(--accent); }

#panel-left  { grid-area: left;  border-right: 1px solid var(--line-1); }
#panel-right { grid-area: right; border-left:  1px solid var(--line-1); }
#panel-left, #panel-right {
  background: var(--bg-1);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-bottom: 26px;
}

#stage {
  grid-area: stage;
  position: relative;
  background: var(--bg-0);
  display: flex;
  min-height: 0;
}

#statusbar {
  grid-area: bar;
  background: var(--bg-1);
  border-top: 1px solid var(--line-1);
}

/* thin, unobtrusive scrollbars in the panels */
#panel-left::-webkit-scrollbar, #panel-right::-webkit-scrollbar,
.log::-webkit-scrollbar { width: 10px; }
#panel-left::-webkit-scrollbar-thumb, #panel-right::-webkit-scrollbar-thumb,
.log::-webkit-scrollbar-thumb {
  background: var(--line-1); border-radius: 6px; border: 3px solid var(--bg-1);
}
#panel-left::-webkit-scrollbar-thumb:hover,
#panel-right::-webkit-scrollbar-thumb:hover { background: var(--line-2); }

/* --- steps ---------------------------------------------------------------- */

/* Section titles are sentence case and WHITE; the smaller grey uppercase is
   reserved for the collapsible groups inside them. Two levels of heading, two
   visibly different treatments — the previous version made both small grey
   uppercase and the panel read as one undifferentiated list. */
.step { padding: 12px 14px 14px; border-bottom: 1px solid var(--line-1); }
.step:last-child { border-bottom: none; }

.step h2 {
  display: flex; align-items: center; gap: 8px;
  margin: 0 0 10px;
  font-size: var(--t-md); font-weight: 600;
  letter-spacing: -.01em;
  color: var(--fg-0);
}

.step-n {
  display: grid; place-items: center;
  width: 17px; height: 17px; flex: none;
  border-radius: var(--r-sm);
  background: var(--accent-dim); color: var(--accent);
  font-size: 10px; font-weight: 700; letter-spacing: 0;
}

/* --- load panel ----------------------------------------------------------- */

.load-formats {
  margin: 8px 0 0;
  font-family: var(--mono); font-size: 10px;
  letter-spacing: .3px; color: var(--fg-2); text-align: center;
}

.file-name {
  margin: 8px 0 0; padding: 0 8px;
  height: var(--h); display: flex; align-items: center;
  background: var(--bg-2); border: 1px solid var(--line-1); border-radius: var(--r-sm);
  font-family: var(--mono); font-size: var(--t-xs);
  color: var(--fg-1);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}

/* --- buttons -------------------------------------------------------------- */

/* Height comes from --h, never from padding, so a button and a select and an
   input sitting in a row line up exactly. Padding-derived heights were what
   made the old panel look assembled rather than designed. */
.btn {
  appearance: none;
  display: inline-flex; align-items: center; justify-content: center;
  height: var(--h); padding: 0 12px;
  border: 1px solid var(--line-2);      /* control edge: 3.30:1, see header */
  background: var(--bg-2);
  color: var(--fg-0);
  border-radius: var(--r-sm);
  font: inherit; font-size: var(--t-sm); font-weight: 550;
  white-space: nowrap;
  cursor: pointer;
  transition: background .1s, border-color .1s, color .1s;
}
.btn:hover:not(:disabled) { background: var(--bg-3); border-color: var(--fg-2); }
.btn:active:not(:disabled) { background: var(--bg-2); }
.btn:disabled { opacity: .35; cursor: not-allowed; }

.btn-primary {
  background: var(--accent); border-color: var(--accent);
  color: var(--accent-ink); font-weight: 700;
  letter-spacing: .01em;
}
.btn-primary:hover:not(:disabled) { background: var(--accent-hi); border-color: var(--accent-hi); }

.btn-quiet { background: transparent; }
.btn-quiet:hover:not(:disabled) { background: var(--bg-2); }

/* The one place a taller control is right: the button that starts the work. */
.btn-wide  { width: 100%; height: 32px; }
.btn-wide + .btn-wide { margin-top: 6px; }
.btn-tiny  { height: var(--h-sm); padding: 0 9px; font-size: var(--t-xs); }

.row { display: flex; gap: 6px; }
.row .btn { flex: 1; }

/* --- collapsible setting groups ------------------------------------------ */

.group { border-top: 1px solid var(--line-1); }
.group:first-child { border-top: none; }

.group-head {
  display: flex; align-items: center; gap: 7px;
  height: var(--h); padding: 0 2px;
  cursor: pointer;
  list-style: none;
  font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-2);
  user-select: none;
}
.group-head::-webkit-details-marker { display: none; }
.group-head:hover { color: var(--fg-1); }

/* Disclosure caret, drawn rather than an icon font. */
.group-head::before {
  content: ""; flex: none;
  width: 0; height: 0;
  border-left: 4.5px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform .15s ease;
}
.group[open] > .group-head::before { transform: rotate(90deg) translateX(1px); }

.group-count {
  margin-left: auto;
  font-size: var(--t-xs); letter-spacing: 0;
  color: var(--fg-2);
  background: var(--bg-2);
  border-radius: 100px;
  min-width: 18px; text-align: center;
  padding: 1px 6px;
}
.group[open] > .group-head .group-count { opacity: 0; }

.group-body { padding: 0 0 10px; }

/* --- controls ------------------------------------------------------------- */

.ctrl { margin-bottom: 10px; }
.ctrl:last-child { margin-bottom: 0; }

.ctrl-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; min-height: 18px; margin-bottom: 3px;
}
.ctrl-label { font-size: var(--t-sm); font-weight: 500; color: var(--fg-1); }
.ctrl-value {
  font-family: var(--mono); font-size: var(--t-xs);
  color: var(--accent); font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.ctrl-note { margin: 4px 0 0; font-size: var(--t-xs); color: var(--fg-2); line-height: 1.45; }

/* Slider. The track clears 3:1 against the panel so it reads as a control. */
input[type=range] {
  appearance: none; -webkit-appearance: none;
  width: 100%; height: 18px; margin: 0;
  background: transparent; cursor: pointer;
}
input[type=range]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--line-2);
}
input[type=range]::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--line-2);
}
input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px; height: 14px; margin-top: -5px;
  border-radius: 50%; border: none;
  background: var(--accent);
}
input[type=range]::-moz-range-thumb {
  width: 14px; height: 14px; border: none; border-radius: 50%;
  background: var(--accent);
}

select, input[type=number] {
  width: 100%;
  height: var(--h);
  background: var(--bg-2);
  color: var(--fg-0);
  border: 1px solid var(--line-2);      /* control edge: 3.30:1, see header */
  border-radius: var(--r-sm);
  padding: 0 8px;
  font: inherit; font-size: var(--t-sm);
  cursor: pointer;
  transition: border-color .1s, background .1s;
}
select:hover, input[type=number]:hover { border-color: var(--fg-2); background: var(--bg-3); }
select:focus-visible, input[type=number]:focus-visible { border-color: var(--accent); }
select { appearance: none; padding-right: 24px;
  background-image: linear-gradient(45deg, transparent 50%, var(--fg-1) 50%),
                    linear-gradient(135deg, var(--fg-1) 50%, transparent 50%);
  background-position: calc(100% - 13px) 53%, calc(100% - 9px) 53%;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
}

.ctrl-check {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--h-sm); cursor: pointer;
  font-size: var(--t-sm); color: var(--fg-1);
}
.ctrl-check input {
  margin: 0; width: 14px; height: 14px; accent-color: var(--accent); flex: none;
}
.ctrl-check:hover { color: var(--fg-0); }

/* --- tooltips ------------------------------------------------------------- */

.tip {
  position: fixed; z-index: 200;
  max-width: 260px;
  padding: 8px 10px;
  background: var(--bg-3);
  color: var(--fg-0);
  border: 1px solid var(--line-2);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  font-size: var(--t-sm); line-height: 1.5;
  pointer-events: none;
  opacity: 0;
  transition: opacity .1s ease;
}
.tip.is-open { opacity: 1; }

/* --- viewport ------------------------------------------------------------- */

/* The stage reads as a framed object rather than a hole cut in the shell: on
   pure black a --line-1 hairline is invisible (1.28:1), so the frame is
   --line-2 and the corner radius is larger than the controls'. */
#viewport {
  flex: 1;
  min-width: 0;
  background: var(--viewport-bg);
  border: 1px solid var(--line-2);
  margin: 10px;
  border-radius: 10px;
  overflow: hidden;
  position: relative;
}
#viewport canvas { display: block; }

.viewport-bar {
  position: absolute;
  top: 22px; left: 22px; right: 22px;
  z-index: 3;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  pointer-events: none;
}
.viewport-bar > * { pointer-events: auto; }

/* Floating viewport chrome — the HUD and both switches.

   These sit ON the 3D view, so contrast depends on what is behind them. Two
   backdrops matter and they are far apart: the mid-grey viewport, which is
   what you see almost all the time, and a white model under a bright key,
   which is the honest worst case.

   At 62% panel opacity with --fg-0 labels, measured:

       over the mid-grey viewport   10.85:1    <- the normal case
       over blown-out white          5.13:1    <- the worst case

   62% is not a round number and it is not a taste call: it is the lowest
   opacity on the measured curve where the worst case still clears 4.5:1 with
   margin. The curve is steep and worth having in front of you before touching
   it — over white, .55 gives 4.01, .58 gives 4.45, .60 gives 4.77, .62 gives
   5.13, .70 gives 6.90.

   State the trade plainly rather than hiding it: this project holds text to
   7:1 and the worst case here does not meet that. It clears the WCAG AA floor
   of 4.5:1, and it only reaches that state where a white surface sits directly
   behind a label. The alternative was panels dense enough to hide the model
   under them, which defeats the point of floating them over it. The
   text-shadow buys back legibility the contrast formula cannot score, because
   the formula assumes a flat backdrop and this one is never flat. */
.viewport-glass {
  background: rgba(0, 0, 0, .62);
  backdrop-filter: blur(16px) saturate(125%);
  -webkit-backdrop-filter: blur(16px) saturate(125%);
  border: 1px solid rgba(255, 255, 255, .09);
  box-shadow: 0 2px 12px rgba(0, 0, 0, .3);
  border-radius: var(--r);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
}

/* Segmented control, shared by the surface and view switches. */
.seg {
  display: flex; gap: 2px;
  flex: none;
  padding: 3px;
}
.seg-btn {
  appearance: none; border: none; background: transparent;
  color: var(--fg-0);
  height: var(--h-sm); padding: 0 11px; border-radius: 3px;
  font: inherit; font-size: var(--t-xs); font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
  transition: background .1s, color .1s;
}
.seg-btn:hover:not(:disabled) { color: var(--fg-0); background: rgba(255, 255, 255, .10); }
.seg-btn.is-active { background: var(--accent); color: var(--accent-ink); font-weight: 650; }
.seg-btn.is-active:hover { background: var(--accent-hi); }
.seg-btn:disabled { opacity: .35; cursor: not-allowed; }

.viewport-bar .wire-switch { margin-left: auto; }
.viewport-bar .view-switch { margin-left: 0; }

.viewport-hud {
  display: flex; align-items: center; gap: 13px; flex: none;
  height: 28px; padding: 0 11px;
  font-family: var(--mono); font-size: 10.5px; letter-spacing: .02em;
  color: var(--fg-0);
  font-variant-numeric: tabular-nums;
}
.viewport-hud b { color: var(--fg-0); font-weight: 600; }

/* The same glass as the bars, a touch denser: this pill sits low over the
   model itself rather than over empty frame. Measured over white 5.91:1, over
   the mid-grey viewport 11.64:1. */
.viewport-help {
  position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
  z-index: 3;
  padding: 5px 14px;
  background: rgba(0, 0, 0, .66);
  backdrop-filter: blur(16px) saturate(125%);
  -webkit-backdrop-filter: blur(16px) saturate(125%);
  border: 1px solid rgba(255, 255, 255, .09);
  border-radius: 100px;
  font-size: var(--t-sm); color: var(--fg-0);
  text-shadow: 0 1px 2px rgba(0, 0, 0, .55);
  pointer-events: none;
}

/* -- drop anywhere --------------------------------------------------------- */
/* Covers the entire window while a drag is in progress. It must sit above
   everything including the panels, and it must NOT eat pointer events at any
   other time — hence `hidden` rather than opacity, and the [hidden] rule at the
   top of this file is what makes that stick. */
#drop-overlay {
  position: fixed; inset: 0; z-index: 90;
  display: grid; place-content: center;
  background: rgba(0, 0, 0, .78);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  animation: drop-in .12s ease-out;
}
@keyframes drop-in { from { opacity: 0; } to { opacity: 1; } }

.drop-card {
  display: grid; justify-items: center; gap: 4px;
  padding: 36px 58px;
  border: 2px dashed var(--accent);
  border-radius: 16px;
  background: #0d0d0d;
  box-shadow: var(--shadow);
  text-align: center;
}
.drop-icon { width: 38px; height: 38px; color: var(--accent); margin-bottom: 4px; }
.drop-main { margin: 0; font-size: var(--t-lg); font-weight: 700; color: var(--fg-0); }
.drop-sub  { margin: 0; font-size: 10px; color: var(--fg-2); font-family: var(--mono); letter-spacing: .3px; }

.viewport-empty {
  position: absolute; inset: 10px; z-index: 2;
  display: grid; place-content: center; text-align: center;
  background: var(--viewport-bg);
  border-radius: 10px;
  color: #1a1a1a;
}
.viewport-empty p { margin: 0; font-weight: 600; font-size: var(--t-md); }
.viewport-empty .dim { margin-top: 4px; font-weight: 400; color: #3d3d3d; }

/* -- loading ---------------------------------------------------------------
   Covers the stage while the app boots and while any model is being read.
   Black rather than the viewport grey: at this moment there is no model to
   show edges against, and it keeps the first paint continuous with the shell
   instead of flashing a grey panel in. */
.viewport-loading {
  position: absolute; inset: 10px; z-index: 4;
  display: grid; place-content: center;
  background: var(--bg-0);
  border-radius: 10px;
}
.load-card { display: grid; justify-items: center; gap: 14px; }

/* The mark, pulsing. One element, no spinner clichés — and it is the same
   triangle as the header so the wait is branded rather than generic. */
.load-mark {
  width: 42px; height: 42px;
  color: var(--accent);
  animation: load-pulse 1.5s ease-in-out infinite;
}
@keyframes load-pulse {
  0%, 100% { opacity: .3; transform: scale(.94); }
  50%      { opacity: 1;  transform: scale(1); }
}

.load-text {
  margin: 0;
  font-size: var(--t-sm); color: var(--fg-1);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

/* Indeterminate sweep. A fixed-width fill travelling the track reads as
   "working" without claiming to know how far along it is. */
.load-bar {
  width: 168px; height: 2px;
  background: var(--bg-3);
  border-radius: 2px;
  overflow: hidden;
}
.load-bar-fill {
  width: 40%; height: 100%;
  background: var(--accent);
  border-radius: 2px;
  animation: load-sweep 1.15s cubic-bezier(.65, 0, .35, 1) infinite;
}
@keyframes load-sweep {
  0%   { transform: translateX(-105%); }
  100% { transform: translateX(255%); }
}

/* --- report --------------------------------------------------------------- */

.report-empty { padding: 14px; color: var(--fg-1); font-size: var(--t-sm); line-height: 1.55; }
.dim { color: var(--fg-1); }
.panel-note { margin: 9px 0 0; font-size: var(--t-xs); color: var(--fg-2); line-height: 1.5; }

.rep-section { padding: 12px 14px; border-bottom: 1px solid var(--line-1); }
.rep-section h3 {
  margin: 0 0 9px;
  font-size: var(--t-xs); font-weight: 600;
  letter-spacing: .06em; text-transform: uppercase;
  color: var(--fg-2);
}

.stat-grid { display: grid; grid-template-columns: auto auto 1fr; gap: 5px 10px; align-items: baseline; }
.stat-grid dt { color: var(--fg-1); font-size: var(--t-sm); }
.stat-grid dd {
  margin: 0; font-family: var(--mono); font-size: var(--t-xs); text-align: right;
  font-variant-numeric: tabular-nums; color: var(--fg-0);
}
.stat-delta {
  font-family: var(--mono); font-size: var(--t-xs); text-align: right;
  font-variant-numeric: tabular-nums;
}
.stat-delta.good { color: var(--accent); }
.stat-delta.same { color: var(--fg-2); }

.tex-item {
  display: flex; align-items: center; gap: 9px;
  width: 100%;
  padding: 6px; margin: 0 0 1px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--r-sm);
  text-align: left;
  font: inherit; color: inherit;
  cursor: pointer;
  transition: background .12s, border-color .12s;
}
.tex-item:hover { background: var(--bg-2); border-color: var(--line-1); }
.tex-swatch {
  width: 34px; height: 34px; flex: none;
  border-radius: var(--r-sm);
  border: 1px solid var(--line-2);
  background: #6f6f6f center/cover no-repeat;
}
.tex-meta { min-width: 0; flex: 1; display: block; }
.tex-slot { display: block; font-size: var(--t-sm); font-weight: 600; color: var(--fg-0); }
.tex-dims {
  display: block; margin-top: 2px;
  font-family: var(--mono); font-size: 10px; color: var(--fg-2);
}

.finding {
  display: flex; gap: 10px;
  padding: 8px 0;
  border-bottom: 1px solid var(--line-1);
  font-size: var(--t-sm);
}
.finding:last-child { border-bottom: none; }
.finding-icon {
  flex: none; width: 15px; text-align: center;
  font-weight: 700; font-family: var(--mono); font-size: 12px;
  line-height: 1.45;
}
.finding.sev-error .finding-icon { color: var(--error); }
.finding.sev-warn  .finding-icon { color: var(--warn); }
.finding.sev-info  .finding-icon { color: var(--fg-2); }
.finding.sev-fixed .finding-icon { color: var(--accent); }
.finding-body { min-width: 0; }
.finding-title { font-weight: 600; color: var(--fg-0); }
.finding-detail { color: var(--fg-1); margin-top: 3px; line-height: 1.5; }

/* --- status bar ----------------------------------------------------------- */

.status-line { display: flex; align-items: center; gap: 9px; height: 30px; padding: 0 14px; }
.status-dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--line-2); }
.status-dot[data-state=busy]  { background: var(--accent); animation: pulse 1.1s ease-in-out infinite; }
.status-dot[data-state=done]  { background: var(--ok); }
.status-dot[data-state=error] { background: var(--error); }
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } }

.status-text {
  flex: 1; min-width: 0;
  font-size: var(--t-sm); color: var(--fg-1);
  overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
}

.progress { height: 2px; background: var(--line-1); overflow: hidden; }
.progress-fill { height: 100%; width: 0; background: var(--accent); transition: width .18s ease-out; }

.log {
  max-height: 190px; overflow-y: auto;
  padding: 9px 14px 12px;
  border-top: 1px solid var(--line-1);
  font-family: var(--mono); font-size: var(--t-xs); line-height: 1.65;
}
.log-line { display: flex; gap: 10px; }
.log-time { color: var(--fg-2); flex: none; }
.log-msg  { overflow-wrap: anywhere; color: var(--fg-1); }
.log-line.lv-warn  .log-msg { color: var(--warn); }
.log-line.lv-error .log-msg { color: var(--error); }
.log-line.lv-done  .log-msg { color: var(--accent); }

/* --- texture inspector ---------------------------------------------------- */

.inspector {
  position: fixed; inset: 0; z-index: 300;
  background: rgba(0, 0, 0, .78);
  backdrop-filter: blur(3px);
  display: grid; place-items: center;
  padding: 32px;
}
.insp-panel {
  display: flex; flex-direction: column;
  width: min(1180px, 100%); height: min(840px, 100%);
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: 10px;
  box-shadow: var(--shadow);
  overflow: hidden;
}
.insp-bar {
  display: flex; align-items: center; gap: 14px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line-1);
}
.insp-id { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin-right: auto; }
.insp-title { font-size: var(--t-md); font-weight: 600; }
.insp-meta { font-family: var(--mono); font-size: 10px; color: var(--fg-2); }
.insp-channels { background: var(--bg-2); }
.insp-stage { flex: 1; min-height: 0; position: relative; background: var(--viewport-bg); }
.insp-stage canvas { display: block; cursor: grab; }
.insp-stage canvas.is-dragging { cursor: grabbing; }
.insp-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding: 9px 14px;
  border-top: 1px solid var(--line-1);
  font-size: var(--t-sm); color: var(--fg-2);
}
.insp-zoom { font-family: var(--mono); color: var(--fg-1); font-variant-numeric: tabular-nums; }

/* --- narrow screens ------------------------------------------------------- */

@media (max-width: 1180px) {
  #app {
    grid-template-columns: 1fr;
    grid-template-rows: auto 56vh auto auto auto;
    grid-template-areas: "top" "stage" "left" "right" "bar";
    height: auto; min-height: 100%;
  }
  #panel-left, #panel-right { border: none; border-top: 1px solid var(--line-1); }
  .tagline { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
