/* =============================================================================
   Bioreactor part 5: the two 3D viewers (js/bio-viewer.js), 17 Oct 2026.
   A copy of the photometer page's css/explode-viewer.css (W2), so neither page's
   change can move the other; the additions for the loop toggles and the warm
   studio are at the end.
   -----------------------------------------------------------------------------
   Original header: Final designs: the explode viewers and the numbered exploded views (W2, 17 Oct 2026)
   -----------------------------------------------------------------------------
   .xv   the rotatable 3D model with its explode slider (js/explode-viewer.js)
   .xs   the static exploded view: a still of the same model, numbered, with the
         key beside it in two groups (printed, then bought-in)
   .fd-* the layout that puts a viewer beside its specifications table

   Light record only: these sit in part 6, long after the stage has handed over
   to the paper. Colours are the record's own (hwdoc.css): ink #1d1d1f, the Dry
   Lab slate #3f5a66 and the leaf green #23684a.
   ========================================================================== */

.xv, .xs, .xs__key {   /* .xs__key: the parts key also stands alone, in a fold beside its model (17 Oct) */
  --xv-ink: #1d1d1f;
  --xv-ink-2: #424245;
  --xv-ink-3: #6e6e73;
  --xv-line: rgb(29 29 31 / .1);
  --xv-acc: #23684a;
  --xv-slate: #3f5a66;
  --xv-stage: radial-gradient(120% 85% at 50% 36%, #ffffff 0%, #f4f6f8 52%, #e7ebef 100%);
  margin: 0;
}

/* ------------------------------------------------------------ the 3D viewer */
.xv__stage {
  position: relative;
  aspect-ratio: 4 / 5;
  border-radius: 18px;
  overflow: hidden;
  background: var(--xv-stage);
  box-shadow: inset 0 0 0 1px var(--xv-line);
  touch-action: pan-y;               /* a vertical swipe still scrolls the page */
  -webkit-user-select: none;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
  cursor: grab;
  outline: none;
  contain: layout paint;
}
.xv__stage.is-grab { cursor: grabbing; }
.xv__stage:focus-visible { box-shadow: inset 0 0 0 1px var(--xv-line), 0 0 0 3px rgb(35 104 74 / .35); }
.xv--wide .xv__stage { aspect-ratio: 5 / 4; }
/* the Pro comes apart along its whole column, LED mount to base: a taller stage */
.xv--tall .xv__stage { aspect-ratio: 3 / 5; }

.xv__poster,
.xv__gl {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}
.xv__poster { object-fit: cover; z-index: 0; }
.xv__gl { z-index: 1; opacity: 0; transition: opacity .35s ease; }
.xv--live .xv__gl { opacity: 1; }
.xv--live .xv__poster { visibility: hidden; transition: visibility 0s .4s; }

/* the part numbers: each comes in on its own (its opacity and scale are set per frame by js/bio-viewer.js) */
.xv__discs {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  visibility: hidden;
}
.xv__discs.is-on { visibility: visible; }
.xv__leaders {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
.xv__leaders line { stroke: rgb(29 29 31 / .55); stroke-width: 1; }
.xv__leaders circle { fill: var(--xv-ink); stroke: #fff; stroke-width: 1; }
.xv__num {
  position: absolute;
  left: 0;
  top: 0;
  width: 26px;
  height: 26px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  font: 650 12px/26px ui-sans-serif, -apple-system, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-variant-numeric: tabular-nums;
  text-align: center;
  cursor: pointer;
  pointer-events: auto;
  will-change: transform;
  box-shadow: 0 1px 2px rgb(0 0 0 / .18), 0 3px 10px -4px rgb(0 0 0 / .35);
  transition: background-color .15s, color .15s, box-shadow .15s;
}
/* printed parts: ink discs; bought-in parts: white discs (the key's two groups) */
.xv__num--printed { background: var(--xv-ink); color: #fff; }
.xv__num--bought { background: #fff; color: var(--xv-ink); box-shadow: inset 0 0 0 1.5px var(--xv-ink), 0 1px 2px rgb(0 0 0 / .14), 0 3px 10px -4px rgb(0 0 0 / .3); }
.xv__num:hover { background: var(--xv-slate); color: #fff; }
.xv__num.is-sel { background: var(--xv-acc); color: #fff; box-shadow: 0 0 0 3px rgb(35 104 74 / .25), 0 3px 10px -4px rgb(0 0 0 / .35); }
.xv__num:focus-visible { outline: 2px solid var(--xv-acc); outline-offset: 2px; }
/* (each number's opacity is set per frame by js/bio-viewer.js: with a part picked the others step back, and go while
   the camera closes in on it; then they take no taps) */
.xv--focus .xv__num:not(.is-sel) { pointer-events: none; }

/* the first-time hint, and the way back after a turn */
.xv__hint {
  position: absolute;
  left: 12px;
  top: 12px;
  z-index: 3;
  margin: 0;
  padding: 4px 9px 4px 8px;
  border-radius: 999px;
  background: rgb(255 255 255 / .82);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  box-shadow: 0 0 0 1px var(--xv-line);
  font-size: 11px;
  line-height: 1.3;
  color: var(--xv-ink-2);
  pointer-events: none;
  opacity: 0;
  transition: opacity .4s ease;
}
.xv--live .xv__hint { opacity: 1; }
.xv--used .xv__hint { opacity: 0; }
.xv__hint svg { width: 14px; height: 14px; vertical-align: -3px; margin-right: 4px; }
.xv__reset {
  position: absolute;
  right: 10px;
  top: 10px;
  z-index: 3;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 50%;
  background: rgb(255 255 255 / .86);
  box-shadow: 0 0 0 1px var(--xv-line), 0 2px 8px -4px rgb(0 0 0 / .3);
  color: var(--xv-ink-2);
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity .25s, visibility 0s .25s;
}
.xv--turned .xv__reset { opacity: 1; visibility: visible; transition: opacity .25s; }
.xv__reset svg { width: 16px; height: 16px; display: block; margin: auto; }
.xv__reset:focus-visible { outline: 2px solid var(--xv-acc); outline-offset: 2px; }

/* -------------------------------------------------------------- controls */
.xv__bar {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 10px;
  margin-top: 12px;
}
.xv__play {
  width: 36px;
  height: 36px;
  border: 0;
  border-radius: 50%;
  background: var(--xv-ink);
  color: #fff;
  cursor: pointer;
  display: grid;
  place-items: center;
  transition: background-color .15s;
}
.xv__play:hover { background: var(--xv-slate); }
.xv__play:focus-visible { outline: 2px solid var(--xv-acc); outline-offset: 2px; }
.xv__play svg { width: 18px; height: 18px; transition: transform .3s ease; }
.xv__play.is-open svg { transform: rotate(180deg); }
.xv__play:disabled, .xv__range:disabled { opacity: .45; cursor: default; }
.xv__lab, .xv__end {
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--xv-ink-3);
  white-space: nowrap;
}
.xv__range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 28px;
  margin: 0;
  background: transparent;
  cursor: pointer;
  --pct: 0%;
}
.xv__range::-webkit-slider-runnable-track {
  height: 4px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--xv-ink) var(--pct), rgb(29 29 31 / .14) var(--pct));
}
.xv__range::-moz-range-track { height: 4px; border-radius: 2px; background: rgb(29 29 31 / .14); }
.xv__range::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--xv-ink); }
.xv__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 20px;
  height: 20px;
  margin-top: -8px;
  border-radius: 50%;
  background: #fff;
  border: 0;
  box-shadow: 0 0 0 1.5px var(--xv-ink), 0 2px 6px -1px rgb(0 0 0 / .3);
}
.xv__range::-moz-range-thumb {
  width: 20px;
  height: 20px;
  border-radius: 50%;
  background: #fff;
  border: 0;
  box-shadow: 0 0 0 1.5px var(--xv-ink), 0 2px 6px -1px rgb(0 0 0 / .3);
}
.xv__range:focus-visible { outline: none; }
.xv__range:focus-visible::-webkit-slider-thumb { box-shadow: 0 0 0 1.5px var(--xv-ink), 0 0 0 5px rgb(35 104 74 / .3); }
.xv__range:focus-visible::-moz-range-thumb { box-shadow: 0 0 0 1.5px var(--xv-ink), 0 0 0 5px rgb(35 104 74 / .3); }
.xv__sel {
  display: block;
  min-height: 2.9em;
  margin: 8px 0 0;
  font-size: 14px;
  line-height: 1.45;
  color: var(--xv-ink-3);
}
.xv__sel b { color: var(--xv-ink); font-weight: 650; margin-right: .35em; }
.xv__sel.is-part { color: var(--xv-ink-2); }
.xv figcaption { margin-top: 6px; }

/* without WebGL: the still exploded view stands in, the controls go */
.xv--static .xv__bar, .xv--static .xv__sel, .xv--static .xv__hint, .xv--static .xv__poster { display: none; }
.xv--static .xv__stage { cursor: default; aspect-ratio: auto; }
.xv--static .xv__stage .xs__media { border-radius: 0; box-shadow: none; }
/* without WebGL and with no static view on the page: the poster stays, the controls go */
.xv--still .xv__bar, .xv--still .xv__sel, .xv--still .xv__hint, .xv--still .xv__reset { display: none; }
.xv--still .xv__stage { cursor: default; }

/* -------------------------------------------------- the static exploded view */
.xs {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: 18px 28px;
  align-items: start;
}
.xs__media {
  position: relative;
  border-radius: 18px;
  overflow: hidden;
  background: var(--xv-stage);
  box-shadow: inset 0 0 0 1px var(--xv-line);
  container-type: inline-size;
}
.xs__media img { display: block; width: 100%; height: auto; }
.xs__leaders { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.xs__leaders line { stroke: rgb(29 29 31 / .45); stroke-width: 1; vector-effect: non-scaling-stroke; }
.xs__n {
  position: absolute;
  width: clamp(18px, 5.2cqw, 26px);
  height: clamp(18px, 5.2cqw, 26px);
  margin: calc(clamp(18px, 5.2cqw, 26px) / -2) 0 0 calc(clamp(18px, 5.2cqw, 26px) / -2);
  border-radius: 50%;
  display: grid;
  place-items: center;
  font: 650 clamp(10px, 2.5cqw, 12px)/1 ui-sans-serif, -apple-system, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-variant-numeric: tabular-nums;
  box-shadow: 0 1px 2px rgb(0 0 0 / .18), 0 3px 10px -4px rgb(0 0 0 / .35);
  transition: background-color .15s, color .15s, transform .15s;
}
.xs__n--printed { background: var(--xv-ink); color: #fff; }
.xs__n--bought { background: #fff; color: var(--xv-ink); box-shadow: inset 0 0 0 1.5px var(--xv-ink), 0 1px 2px rgb(0 0 0 / .14), 0 3px 10px -4px rgb(0 0 0 / .3); }
.xs__n.is-hot { background: var(--xv-acc); color: #fff; transform: scale(1.15); box-shadow: 0 0 0 3px rgb(35 104 74 / .25); }

/* the key: two short groups, so it never reads as one long column */
.xs__key { display: grid; gap: 18px; align-content: start; }
.xs__nums { position: absolute; inset: 0; pointer-events: none; }
.xs__gh {
  margin: 0 0 6px;
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--xv-slate);
}
.xs__gh span { letter-spacing: .04em; color: var(--xv-ink-3); font-weight: 500; margin-left: .3em; }
.xs__group ol { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.xs__group li {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr);
  gap: 0 10px;
  align-items: baseline;
  padding: 4px 8px;
  border-radius: 10px;
  transition: background-color .15s;
}
.xs__group li:hover, .xs__group li.is-hot { background: rgb(63 90 102 / .07); }
.xs__group li > i {
  grid-row: span 2;
  align-self: start;
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font: 650 11px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  font-style: normal;
  font-variant-numeric: tabular-nums;
}
.xs__group--printed li > i { background: var(--xv-ink); color: #fff; }
.xs__group--bought li > i { background: #fff; color: var(--xv-ink); box-shadow: inset 0 0 0 1.5px var(--xv-ink); }
.xs__group li b { font-weight: 650; color: var(--xv-ink); font-size: 14px; line-height: 1.35; }
.xs__group li span { font-size: 13px; line-height: 1.38; color: var(--xv-ink-3); }

@media (min-width: 760px) {
  .xs { grid-template-columns: minmax(0, 1.12fr) minmax(0, 1fr); }
  /* the key runs longer than the picture: the picture stays in view beside it */
  .xs__media { position: sticky; top: 84px; align-self: start; }
  .xs > figcaption { grid-column: 2; }   /* under the key, so the sticky picture never slides over it (W6 R2, V1) */
}

/* --------------------------------- a viewer beside its specifications table */
.fd-pair { display: grid; grid-template-columns: minmax(0, 1fr); gap: 22px 32px; align-items: start; }
/* a two-column specs table beside the viewer wraps its lines instead of scrolling sideways */
.fd-pair table { min-width: 0 !important; width: 100%; }
.fd-pair .tablewrap { overflow-x: visible; }
@media (min-width: 820px) {
  .fd-pair { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
  .fd-pair > .xv { position: sticky; top: 84px; }
}

@media print {
  .xv__gl, .xv__discs, .xv__bar, .xv__hint, .xv__reset { display: none !important; }
  .xv--live .xv__poster { visibility: visible !important; }
  .fd-pair > .xv { position: static; }
}
@media (prefers-reduced-motion: reduce) {
  .xv__gl, .xv__hint, .xv__num, .xs__n, .xv__play svg { transition: none; }
}

/* W6 R2, V12: the key's group labels at their intended 11 px (style.css's .sec p set "PRINTED PARTS 1-5" at 16.8 px) */
.doc .sec p.xs__gh { margin: 0 0 6px; max-width: none; font-size: 11px; line-height: 1.3; color: var(--xv-slate, #3f5a66); }

/* ======================================= bioreactor additions (17 Oct 2026) */
/* the brochure's warm studio sweep, behind both models */
.xv { --xv-stage: radial-gradient(120% 85% at 50% 34%, #fbf9f5 0%, #f1ede6 55%, #e4ded4 100%); }
.xv__bar--loops { grid-template-columns: repeat(2, minmax(0, auto)) minmax(0, 1fr); }
.xv__loop {
  display: inline-flex; align-items: center; gap: 8px;
  min-height: 36px; padding: 0 14px 0 10px; border: 0; border-radius: 999px;
  background: rgb(29 29 31 / .06); color: var(--xv-ink-2);
  font: 600 13px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Helvetica Neue", Arial, sans-serif;
  cursor: pointer; transition: background-color .15s, color .15s, box-shadow .15s;
}
.xv__loop i { width: 12px; height: 12px; border-radius: 50%; box-shadow: inset 0 0 0 2px var(--lc); }
.xv__loop[data-xv-loop="cul"] { --lc: #e08a22; }
.xv__loop[data-xv-loop="pro"] { --lc: #23864f; }
.xv__loop:hover { background: rgb(29 29 31 / .1); }
.xv__loop[aria-pressed="true"] { background: var(--lc); color: #fff; }
.xv__loop[aria-pressed="true"] i { background: #fff; box-shadow: none; }
.xv__loop:focus-visible { outline: 2px solid var(--xv-acc); outline-offset: 2px; }
.xv__loop:disabled { opacity: .45; cursor: default; }
.xv--still .xv__loop { display: none; }
.xv__bar--loops .xv__end { justify-self: end; text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 12px; }
@media (max-width: 420px) { .xv__bar--loops .xv__end { display: none; } }

/* the numbers. The commercial bioreactor's: white discs, read on its black cabinet and on the pale studio alike, with
   white leaders edged in ink so a leader shows over the cabinet and over the sweep */
.xv__num--printed { background: #fff; color: var(--xv-ink); box-shadow: inset 0 0 0 1.5px var(--xv-ink), 0 1px 2px rgb(0 0 0 / .2), 0 3px 10px -4px rgb(0 0 0 / .4); }
/* the disc's radius follows the stage (--xv-r, set by the script: 13 px, 11.5 on a small stage) */
.xv__num { transform-origin: 50% 50%; width: calc(var(--xv-r, 13px) * 2); height: calc(var(--xv-r, 13px) * 2);
  line-height: calc(var(--xv-r, 13px) * 2); font-size: calc(var(--xv-r, 13px) * .9); }
.xv__leaders line { stroke: rgb(29 29 31 / .7); }
.xv__leaders circle { fill: #fff; stroke: var(--xv-ink); }
.xv--cabinet .xv__leaders { filter: drop-shadow(0 0 .6px rgb(0 0 0 / .9)) drop-shadow(0 0 1.2px rgb(0 0 0 / .35)); }
.xv--cabinet .xv__leaders line { stroke: #fff; stroke-width: 1.25; }
.xv--cabinet .xv__leaders circle { fill: #fff; stroke: var(--xv-ink); stroke-width: 1; }
/* the rack's numbers ride with their loop: the culture loop's amber (1-8), the protectant line's green (9-10), the
   colours of 5.1's key */
.xv--rack .xv__num--cul,
.xv--rack .xv__num--pro { color: #fff; box-shadow: 0 0 0 2px #fff, 0 3px 10px -4px rgb(0 0 0 / .35); }
.xv--rack .xv__num--cul { background: #e08a22; }
.xv--rack .xv__num--pro { background: #23864f; }
.xv--rack .xv__num:hover { background: var(--xv-slate); }
.xv--rack .xv__num.is-sel { background: var(--xv-acc); }

/* the bar: a loop's path leads with the loop's own colour; js/bio-viewer.js keeps the bar at the height of its
   longest message (a part's name and role, or a loop's path), so nothing below it moves */
.xv__sel .xv__lk { font-weight: 650; margin-right: 0; }
.xv__sel .xv__lk--cul { color: #a8620f; }
.xv__sel .xv__lk--pro { color: #23864f; }
.xv__sel.is-loop { color: var(--xv-ink-2); }
