/* =========================================================================
   studiolab.css — THE SPECIMEN LAB studio theme.

   WHAT THIS IS. The donor theme's live skin studio, ported into the shared
   frontend as a THEME the assembler switches on per owner
   (`web.studio.theme: "lab"`). Owner order 2026-08-17: an owner asked for
   the donor theme's chamber 1:1, not merely referenced.

   ★★★★★ READ THIS BEFORE CHANGING ANYTHING — THE FIRST PORT GOT IT WRONG.
   The donor's page loads FIVE sheets over one DOM, and the LAST one wins:
     _components.css -> lab.css -> crt.css -> the LIVING sheet -> mobile.css
   The first attempt at this port read `crt.css` and stopped there, and
   shipped a cyan phosphor terminal. `crt.css` is NOT the look. The LIVING
   sheet loads after it and re-dresses the whole thing: it deletes the
   scanlines, the registration brackets and the `> SPECIMEN LAB_` prompt, drops
   VT323 for Fraunces + Inter, repaints cyan to warm brown / jade / amber, and
   turns the floating chrome into THREE DOCKED COLUMNS. The owner rejected
   the cyan version as nothing like the donor theme.

   So this sheet is the FINAL rendered design in one layer, read off the
   donor theme's own live studio page as it actually paints — not off a
   stylesheet in isolation. If you change it, re-render both pages and compare
   the pictures. A sheet read is not a page seen.

   THE SHAPE, top to bottom:
     · a three-column grid — species list | stage | skin controls
     · a warm painted chamber, lit from above, with film grain and a vignette
     · the specimen's name over it in Fraunces, growth pill above that
     · instruments docked top-LEFT of the stage, sliders bottom-centre
     · an amber tool pill across the foot of the stage
     · the right column has TWO FACES behind tabs: Editor and Library

   FOUR THINGS THAT ARE DELIBERATE — the donor's own notes, and they hold here:
     1. The controls are DOCKED in grid columns, not floating over the stage,
        so the stage loses exactly the two rail widths and studio.mjs's
        ResizeObserver keeps the camera aspect right for free.
     2. Panels are near-OPAQUE, not glass. The chamber colour is player-chosen
        and one preset is white; text over a live render cannot be certified
        for contrast.
     3. Rail chrome is pinned to jade/amber, NEVER --accent. The chamber
        buttons rewrite --accent, so accent-driven chrome would repaint the
        whole tool pink when a player picks the pink backdrop. The stage
        tints; the tool does not.
     4. NO backdrop-filter and NO mix-blend-mode anywhere. Both force the
        compositor to re-sample a LIVE 3D canvas every frame. The surfaces are
        opaque enough not to need it, and the page holds frame rate on weak
        GPUs because of it.

   SEATING MAP — the donor's element -> the shared frontend element wearing it:
     #slot-list / .slot        -> #channel-list / .channel   (the colour rows)
     #species-rail-list        -> #deck-list                 (the species list)
     .preset-row               -> .design-row                (the library)
     .gcard                    -> .glitch-tile               (the glitch shelf)
     the drawer's import block -> #code-row                  (skin codes)
   ========================================================================= */

/* -------------------------------------------------------------------------
   0. TOKENS.

   ★★★★★ 2026-09-07: EVERY ONE OF THESE IS THE SITE'S OWN ROLE TOKEN NOW.
   The names are the donor's and they stay - three thousand lines below read
   them, and the two owner tone blocks at the foot re-point them - but not one
   of them is a colour any more. `--av-ink` IS `--fg`; `--av-jade` IS
   `--accent`; the chamber wall IS `--stage-floor`'s ground. That is the whole
   plug-and-play promise: a server born tomorrow gets this studio in ITS
   palette, on ITS ground, with nobody typing a tone block at all - which is
   what a hand-written donor palette on `body.studiolab` could never do, and
   why two owners had to write one each before this pass.

   THE `-rgb` TRIPLES ARE GONE, and that is the same decision. A triple cannot
   be written from a role token, so every `rgba(var(--av-x-rgb), .n)` in this
   sheet is now `color-mix(in srgb, var(--av-x) n%, transparent)` - one form,
   readable, and it composes with a token the owner sets. The two tone blocks
   still declare their triples (their gates pin them to owner.json) and bridge
   them back onto the plain names with `rgb(var(--av-x-rgb))`, so those two
   owners' studios move not one pixel.
   ------------------------------------------------------------------------- */
body.studiolab {
  /* The wallet joins the real action row. Its status stays visible when the
     phone deck folds ordinary save notes; no fixed height or clipped copy. */
  --livebar-h: 44px;
  /* THE THREE FACES ARE THE SITE'S THREE FACES. The lab used to force
     Fraunces + Inter on every owner; the display face is what a nameplate is
     FOR, and an owner who chose one has already answered this question. */
  --av-serif: var(--font-display);
  --av-sans: var(--font-ui);
  --av-mono: var(--font-mono);

  --av-ink: var(--fg);
  --av-ink-2: var(--fg-dim);
  --av-ink-3: var(--fg-mute);

  --av-page: var(--bg-deep);
  --av-page-deep: var(--bg-deep);
  --av-panel: var(--surface);
  --av-panel-2: var(--surface-2);
  --av-panel-3: var(--surface-3);
  --av-line: var(--border-2);
  --av-line-dim: var(--border);

  /* THE AFFIRMATIVE / SELECTED FAMILY = the site's accent. */
  --av-jade: var(--accent);
  --av-jade-lt: var(--accent-hi);
  --av-jade-hot: var(--accent-hi);
  /* THE INSTRUMENT FAMILY - the tool rail, the pool, the focus ring - is the
     same accent one step down, so the room has ONE colour and spends it once
     (the house rule every other page on the site already keeps). */
  --av-amber: var(--accent);
  --av-amber-lt: var(--accent-hi);
  --av-focus: var(--accent-hi);

  --av-edge: color-mix(in srgb, var(--accent) 22%, transparent);
  --av-edge-lit: color-mix(in srgb, var(--accent) 55%, transparent);

  --av-rail-w: 336px;      /* skin controls, right */
  --av-species-w: 246px;   /* species list, left  */

  /* The chamber accent. studiolab.js rewrites THIS ONE from the chosen
     chamber colour, and only the stage reads it. A page whose script never
     ran opens on the site's own accent rather than on nothing. */
  --av-chamber: var(--accent);

  /* THE SURFACES, each named for the job it does. Every one is a role token
     or a mix of one, so an owner's ground travels the whole room. */
  --av-sink: var(--surface-sink);          /* the deepest inset a field sits in */
  --av-well: var(--surface-sink);          /* the instrument well */
  --av-field: var(--surface-2);            /* the name field's ground */
  --av-rail-hi: var(--surface-2);          /* the tool rail's stack */
  --av-rail-lo: var(--surface-sink);
  --av-sheet-hi: var(--surface);           /* the phone sheet's ground */
  --av-sheet-lo: var(--bg-deep);
  --av-shaft: var(--fg);                   /* the light shaft from above */
  --av-haze: var(--bg-deep);               /* the floor haze under the specimen */
  --av-chamber-top: var(--bg-2);           /* the chamber wall, top and floor */
  --av-chamber-bot: var(--bg-deep);
  --av-deck-top: var(--surface-2);         /* the editor column's own ground */
  --av-deck-bot: var(--bg-deep);
  --av-on-jade: var(--on-primary);         /* ink ON a filled affirmative plate */
  --av-btn-ink: var(--fg-dim);             /* ink on a resting key */
  --av-bad: var(--bad);                    /* the delete ink, and only that */
  --av-bad-lt: color-mix(in srgb, var(--bad) 55%, var(--fg));
  /* the one shadow this room casts, off the page's own deepest ground */
  --av-shadow: color-mix(in srgb, var(--bg-deep) 82%, transparent);

  /* THE CHAMBER PALETTE — the eight swatches the stage-light picker offers,
     and the ONE place in this sheet colour is written down. They are the
     control's CONTENT, not the room's dress: a player picks a light to paint
     the stage with, exactly as a colour picker offers colours, and the names
     and values are mirrored from studiolab.js's CHAMBERS map and
     assemble.py's STUDIO_LAB_CHAMBERS so the swatch and the light can never
     disagree. Nothing else on this page may read them. */
  --av-cham-jade:  #2fbf87;
  --av-cham-ember: #ff6a24;
  --av-cham-blue:  #4da6ff;
  --av-cham-green: #4fe08a;
  --av-cham-amber: #ffb347;
  --av-cham-white: #e8e4dc;
  --av-cham-red:   #ff4444;
  --av-cham-pink:  #ff5fa2;

  /* THE NOTCH. The donor cuts the top-left and bottom-right of every seat; a
     site whose own language cuts a different corner re-points these two and
     the lab stops looking bolted on. */
  --av-notch: 7px;
  --av-notch-key: 8px;
  --av-clip-seat: polygon(var(--av-notch) 0, 100% 0, 100% calc(100% - var(--av-notch)), calc(100% - var(--av-notch)) 100%, 0 100%, 0 var(--av-notch));
  --av-clip-key: polygon(var(--av-notch-key) 0, 100% 0, 100% calc(100% - var(--av-notch-key)), calc(100% - var(--av-notch-key)) 100%, 0 100%, 0 var(--av-notch-key));

  /* ★★★★★ THE RUNTIME PROPERTIES ARE **NOT** DECLARED HERE, AND THAT IS THE
     WHOLE POINT. studiolab.js publishes --lab-dock-h, --lab-bar and
     --lab-bottom; every read below carries its own `var(..., fallback)`, so a
     page whose script never ran still lays out.

     Declaring them in this block looked tidier and was a BUG: this block sits
     on `body.studiolab`, so a value the script set on the ROOT element was
     SHADOWED for every descendant of body - the sheet reservation computed
     376px, the body's own `--lab-dock-h: 0px` won, and the phone's foot strip
     laid out underneath the sheet with the clip keys behind it. A token
     default and a runtime value must never name the same property on an
     element between the writer and the reader. */
}

body.studiolab #studio-gene-wallet {
  flex: 1 1 100%; min-width: 0; width: 100%;
  color: var(--av-ink-2); font-size: 12px; line-height: 1.5;
  overflow-wrap: anywhere; text-transform: none; letter-spacing: normal;
}
body.studiolab #studio-gene-wallet p { margin: 8px 0; }
body.studiolab #studio-gene-wallet .btn { white-space: normal; max-width: 100%; min-height: 44px; }

/* -------------------------------------------------------------------------
   1. THE PAGE SHELL — three columns, no page scroll.
   ------------------------------------------------------------------------- */
html:has(body.studiolab) { height: 100%; overflow: hidden; }
body.studiolab {
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  height: 100%;
  min-height: 100dvh;
  margin: 0;
  padding: 0 !important;
  overflow: hidden;
  background: var(--av-page) !important;
  color: var(--av-ink);
  font-family: var(--av-sans);
}
/* THE LIVEBAR RESERVATION SURVIVES THE LAB (a sibling owner 2026-08-18, his
   screenshot: the site's fixed status rail sat OVER the swatch strip and the
   rail's own save / wear / skin-code foot - the fixed player bar covered the
   bottom of the rail). The shell's `padding: 0
   !important` above rightly strips the page chrome, but it also stripped
   site.css's `body.has-livebar { padding-bottom: var(--livebar-h) }`
   reservation, and every 100%-height column then ran under the fixed bar
   (z-livebar 90). Restore exactly that one clearance, at higher specificity.
   body itself is NOT covered by the descendant box-sizing rule below, so say
   border-box here or the padding would grow past height:100%. */
body.studiolab { box-sizing: border-box; }
/* ★★★ THE SHELL DEFENDS ITSELF AGAINST A SITE THEME (2026-09-01, Dino
   Queen's Lair). A site theme sheet (`web.site.theme`) is seated AFTER this
   one and addresses <body> as `html[data-site-theme="x"] body` - specificity
   (0,1,2) against `body.studiolab`'s (0,1,1) - so ONE `display: flex` in a
   theme's footer-to-the-floor rule flattened the three columns into a stack
   on the live page. The lab IS the viewport: nothing about a site look may
   reshape its shell. (0,2,2) here beats every `html[attr] body` a theme can
   write, whatever order the sheets load in. Repeated for the phone shell in
   section 12 - the phone layout is a different grid, not a different
   display. The theme that did it is fixed at source too (lair.css). */
html[data-site-theme] body.studiolab { display: grid; }
body.studiolab.has-livebar {
  /* +12px: the bar FLOATS ~10px off the viewport foot, so it occupies
     ~livebar-h + 10 of screen; the bare token left the deck list 7.5px under
     the bar's top edge (measured in the /validate render, cosmetic only). */
  padding-bottom: calc(var(--livebar-h, 44px) + 12px) !important;
}
body.studiolab *, body.studiolab *::before, body.studiolab *::after {
  box-sizing: border-box;
}
body.studiolab > header,
body.studiolab > .site-header,
body.studiolab > #sky-topbar { grid-row: 1; grid-column: 1 / -1; z-index: 32; }
body.studiolab > .skip-link, body.studiolab > .skip { position: absolute; }

#workspace { grid-row: 2; grid-column: 1; position: relative; min-width: 0; min-height: 0; overflow: hidden; }
.lab-main { position: relative; display: flex; flex-direction: column; height: 100%; min-height: 0; margin: 0; padding: 0; }
#workspace, #panel, .viewport-wrap { min-width: 0; }
#panel, #panel * { max-width: 100%; }

/* the page's own heading, for a screen reader. The stage carries no title -
   the specimen's name IS the heading, exactly as on the donor. */
.lab-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }

/* -------------------------------------------------------------------------
   2. THE CHAMBER — a PAINTED STUDIO, never a photograph.
   A graded wall, a warm pool where the specimen stands, one light shaft,
   grain, vignette. Plain gradients: behind a lit 3D model a photograph
   competes with the subject at the same depth.
   ------------------------------------------------------------------------- */
.viewport-wrap {
  position: relative; width: 100%; height: 100%;
  min-width: 0; min-height: 0; overflow: clip; overscroll-behavior: contain;
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 14%, transparent);
  border-top: 0; border-bottom: 0;
  background:
    radial-gradient(92% 72% at 50% 2%, color-mix(in srgb, var(--av-chamber) 13%, transparent), transparent 62%),
    radial-gradient(66% 56% at 14% 100%, color-mix(in srgb, var(--av-amber) 13%, transparent), transparent 60%),
    radial-gradient(52% 26% at 50% 92%, color-mix(in srgb, var(--av-amber-lt) 9%, transparent), transparent 70%),
    linear-gradient(var(--av-chamber-top), var(--av-chamber-bot));
}
#stage-canvas {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; z-index: 2; cursor: grab; touch-action: none;
}
#stage-canvas:active { cursor: grabbing; }

/* ── THE ATMOSPHERE ────────────────────────────────────────────────────
   A warm floor haze with three slow puffs, and fine motes rising through the
   light shaft. Both sit BEHIND the specimen (z-index 1) so nothing ever reads
   as dirt on the lens in front of it, and both animate TRANSFORM and OPACITY
   only - no layout, no paint of a live 3D canvas, no compositor read-back.
   studiolab.js builds the motes; the Effects key takes the whole layer away. */
/* the haze spans the CHAMBER, not just the floor - the donor's bokeh sit at
   every height, and a layer pinned low reads as fog on the ground instead of
   light in the room. */
#lab-cloud {
  position: absolute; left: 0; right: 0; top: 18%; bottom: 0;
  z-index: 1; pointer-events: none;
}
#lab-cloud .base {
  position: absolute; left: 50%; bottom: 4%; transform: translateX(-50%);
  width: 56%; height: 66px; border-radius: 50%; filter: blur(18px);
  background: radial-gradient(ellipse, color-mix(in srgb, var(--av-haze) 72%, transparent), transparent 72%);
}
#lab-cloud .contact {
  position: absolute; left: 50%; bottom: 16%; transform: translateX(-50%);
  width: 30%; height: 62px; border-radius: 50%; filter: blur(14px);
  background: radial-gradient(ellipse, color-mix(in srgb, var(--av-amber) 24%, transparent), transparent 72%);
}
#lab-cloud .puff {
  position: absolute; border-radius: 50%; filter: blur(26px);
  animation: lab-drift var(--ds, 15s) ease-in-out infinite alternate;
}
#lab-cloud .puff.p1 { left: 6%;  top: 16%; width: 26%; height: 40%; --ds: 15s; --dx: 18px;
  background: radial-gradient(circle, color-mix(in srgb, var(--av-amber-lt) 22%, transparent), transparent 68%); }
#lab-cloud .puff.p2 { right: 8%; top: 8%;  width: 22%; height: 34%; --ds: 19s; --dx: -15px;
  background: radial-gradient(circle, color-mix(in srgb, var(--av-jade-lt) 15%, transparent), transparent 68%); animation-delay: -6s; }
#lab-cloud .puff.p3 { left: 34%; top: 46%; width: 30%; height: 40%; --ds: 23s; --dx: 11px;
  background: radial-gradient(circle, color-mix(in srgb, var(--av-amber) 18%, transparent), transparent 70%); animation-delay: -12s; }
@keyframes lab-drift {
  from { transform: translate(0, 0) scale(1); }
  to   { transform: translate(var(--dx, 12px), -10px) scale(1.06); }
}

#lab-parts { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
#lab-parts .mote {
  position: absolute; border-radius: 50%;
  background: var(--mote, var(--av-amber-lt));
  box-shadow: 0 0 var(--glow, 8px) calc(var(--glow, 8px) / 2) color-mix(in srgb, var(--av-amber-lt) 16%, transparent);
  filter: var(--soft, none);
  opacity: 0;
  animation: lab-rise var(--dur, 17s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes lab-rise {
  0%   { opacity: 0;   transform: translate3d(0, 0, 0) scale(.7); }
  12%  { opacity: var(--peak, .5); }
  80%  { opacity: var(--peak, .5); }
  100% { opacity: 0;   transform: translate3d(var(--dx, 14px), calc(-1 * var(--rise, 260px)), 0) scale(1.05); }
}

/* film grain, blended by OPACITY only — mix-blend-mode over a live canvas
   costs a full compositor read-back every frame */
#lab-grain {
  position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: .045;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='2'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.7'/%3E%3C/svg%3E");
  background-size: 140px 140px;
}
/* a soft daylight vignette, not a CRT tunnel */
#lab-vig {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  box-shadow: inset 0 0 210px 58px color-mix(in srgb, var(--av-page-deep) 62%, transparent), inset 0 0 80px 10px color-mix(in srgb, var(--av-page-deep) 28%, transparent);
}
/* the light shaft from above. The falloff is IN the gradient, so it needs no
   filter:blur() — one less full-layer filter on every repaint. */
#lab-vig::after {
  content: ""; position: absolute; left: -8%; top: -30%; width: 64%; height: 120%;
  transform: rotate(17deg); pointer-events: none;
  background: linear-gradient(90deg,
    color-mix(in srgb, var(--av-shaft) 7.5%, transparent) 0%, color-mix(in srgb, var(--av-shaft) 4.5%, transparent) 26%,
    color-mix(in srgb, var(--av-shaft) 1.5%, transparent) 48%, transparent 68%);
}
.vp-overlay { position: absolute; inset: 0; z-index: 4; pointer-events: none; }

/* -------------------------------------------------------------------------
   3. THE SPECIMEN CAPTION — top middle.
   growth pill, then the saved-look cycler, then the species name with its two
   arrows, as one centred stack above the animal.
   ------------------------------------------------------------------------- */
.stage-growth {
  position: absolute; left: 50%; top: 26px; transform: translateX(-50%);
  z-index: 7; max-width: calc(100% - 32px); pointer-events: auto;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 7px 14px; border-radius: 999px;
  background: color-mix(in srgb, var(--av-well) 82%, transparent); border: 1px solid color-mix(in srgb, var(--av-ink) 12%, transparent);
}
.stage-growth .gr-label, .stage-growth .gr-readout {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--av-ink-3); white-space: nowrap;
}
.stage-growth .gr-readout { min-width: 92px; text-align: left; }
.stage-growth #growth-pct {
  color: var(--av-jade-lt); font-variant-numeric: tabular-nums;
  display: inline-block; min-width: 34px; text-align: right;
}

.vp-center-stack {
  position: absolute; left: 50%; top: 74px; transform: translateX(-50%);
  z-index: 6; display: flex; flex-direction: column; align-items: center;
  gap: 10px; max-width: calc(100% - 32px); pointer-events: none;
}
.skin-cycler {
  order: 2; display: none; align-items: center; gap: 2px; pointer-events: auto;
  padding: 4px 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--av-well) 82%, transparent); border: 1px solid color-mix(in srgb, var(--av-ink) 14%, transparent);
  box-shadow: 0 10px 26px -16px var(--av-shadow);
}
.skin-cycler.has-options { display: inline-flex; }
.skin-cycler button {
  width: 26px; height: 26px; border-radius: 50%; border: 1px solid transparent;
  background: transparent; color: var(--av-ink-3);
  font-size: 15px; line-height: 1; cursor: pointer; transition: .15s;
}
.skin-cycler button:hover { color: var(--av-ink); background: color-mix(in srgb, var(--av-jade) 18%, transparent); }
.skin-cycler button[hidden] { display: none; }
#skin-cycler-name {
  font-family: var(--av-sans); font-size: 11px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--av-ink-2); padding: 0 10px;
}

.vp-specnav {
  order: 3; display: inline-flex; align-items: center; gap: 10px;
  background: none; border: 0; box-shadow: none; padding: 0; pointer-events: auto;
}
.vp-specnav .spec-block { display: flex; flex-direction: column; align-items: center; gap: 5px; min-width: 0; }
/* ★ THE NAMEPLATE IS THE PAGE'S HEADING, so it speaks in the site's own
   heading voice: the display face, TRACKED WIDE, in caps. It used to be a
   300-weight serif at -.01em - a magazine masthead on a page whose every
   other word is a tracked capital, and a fourth typeface on a site with
   three. `word-spacing` gives the tracking back at the space so a two-word
   species does not read as one long word. */
.vp-specnav .spec-name {
  display: inline-flex; align-items: center; gap: 12px;
  font-family: var(--av-serif); font-weight: 400;
  font-size: clamp(24px, 2.6vw, 38px); line-height: 1;
  letter-spacing: .16em; word-spacing: .1em;
  text-transform: uppercase; color: var(--av-ink);
  text-shadow: 0 2px 22px color-mix(in srgb, var(--av-page-deep) 60%, transparent);
}
/* the trailing letter-space is real width: give it back so the name reads
   optically centred between its two arrows */
.vp-specnav .spec-name > span:last-child { margin-right: -.16em; }
.vp-specnav .spec-name .dot {
  width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto;
  background: var(--av-jade-lt); box-shadow: 0 0 12px color-mix(in srgb, var(--av-jade-lt) 70%, transparent);
}
.vp-specnav .spec-status {
  font-family: var(--av-sans); font-size: 10.5px; letter-spacing: .26em;
  text-transform: uppercase; color: var(--av-ink-3);
}
.vp-specnav .nav-arrow {
  width: 44px; height: 44px; border-radius: 50%; font: inherit; font-size: 18px;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid color-mix(in srgb, var(--av-ink) 16%, transparent); background: color-mix(in srgb, var(--av-well) 82%, transparent);
  color: var(--av-ink-2); cursor: pointer; transition: .15s;
}
.vp-specnav .nav-arrow:hover {
  color: var(--av-ink); background: color-mix(in srgb, var(--av-jade) 22%, transparent); border-color: var(--av-edge-lit);
}
.vp-specnav .nav-arrow:disabled { opacity: .35; cursor: not-allowed; }

/* -------------------------------------------------------------------------
   4. INSTRUMENTS ON THE STAGE — docked top-LEFT.
   ------------------------------------------------------------------------- */
.vp-mesh-status, .vp-loading {
  position: absolute; z-index: 7;
  font-family: var(--av-sans); font-size: 10.5px; letter-spacing: .18em;
  text-transform: uppercase; padding: 8px 11px; border-radius: 9px;
  border: 1px solid color-mix(in srgb, var(--av-ink) 14%, transparent); background: color-mix(in srgb, var(--av-well) 88%, transparent);
  color: var(--av-ink-2);
}
.vp-mesh-status { left: 18px; top: 18px; width: 206px; }
.vp-mesh-status.online { color: var(--av-jade-lt); border-color: var(--av-edge-lit); }
.vp-mesh-status.offline { color: var(--av-amber-lt); border-color: color-mix(in srgb, var(--av-amber-lt) 45%, transparent); }
.vp-loading { left: 50%; top: 50%; transform: translate(-50%, -50%); transition: opacity .3s ease; }
.vp-loading.hide { opacity: 0; pointer-events: none; visibility: hidden; }

/* ★ ONE READING ON SCREEN (2026-09-07). The well carried SPECIES, DIET and
   RENDER; the nameplate two inches to the right already says the first two,
   and two places that say one thing are two places that can disagree. The
   quiet pair keep their elements and keep being written - a screen reader
   still gets the whole well in one place - and only the reading the nameplate
   does NOT carry is drawn. */
.vp-readout {
  position: absolute; left: 18px; top: 58px; z-index: 6; width: auto;
  display: inline-flex; flex-direction: column; gap: 4px; padding: 7px 12px;
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--av-ink-3);
  border-radius: 999px; border: 1px solid color-mix(in srgb, var(--av-ink) 12%, transparent);
  background: color-mix(in srgb, var(--av-well) 82%, transparent); font-variant-numeric: tabular-nums;
}
.vp-readout .vp-readout-quiet {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.vp-readout > span { display: flex; justify-content: space-between; gap: 10px; }
.vp-readout .k { color: color-mix(in srgb, var(--av-jade-lt) 72%, transparent); }
.vp-readout .v { color: var(--av-ink-2); }

.vp-autorotate {
  position: absolute; left: 18px; top: 100px; z-index: 6; width: auto;
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  font-family: var(--av-sans); font-size: 11px; letter-spacing: .18em;
  text-transform: uppercase; padding: 9px 14px; border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--av-ink) 14%, transparent); background: color-mix(in srgb, var(--av-well) 82%, transparent);
  color: var(--av-ink-2); cursor: pointer; pointer-events: auto; transition: .15s;
}
.vp-autorotate:hover { color: var(--av-ink); border-color: var(--av-edge-lit); }
.vp-autorotate[aria-pressed="true"] {
  color: var(--av-on-jade); background: linear-gradient(var(--av-jade-lt), var(--av-jade));
  border-color: var(--av-jade-lt);
}
.vp-autorotate[aria-pressed="true"] .ar-ico { color: var(--av-on-jade); }
.vp-autorotate:disabled { opacity: .4; cursor: not-allowed; }

.vp-rot, .showcase-hint {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--av-ink-3);
}
/* the hint sits on the tool rail's own line, at the right edge of the stage,
   exactly where the reference puts it - not stacked above the clip keys */
.vp-rot {
  position: absolute; right: 18px; bottom: calc(var(--lab-bottom, 18px) + 14px);
  z-index: 6; opacity: .8;
}
.showcase-hint {
  position: absolute; left: 50%; bottom: 40px; transform: translateX(-50%);
  z-index: 6; pointer-events: none; display: inline-flex; align-items: center; gap: 9px;
  padding: 9px 16px; border-radius: 999px;
  background: color-mix(in srgb, var(--av-well) 82%, transparent); border: 1px solid color-mix(in srgb, var(--av-ink) 14%, transparent);
  opacity: 0; visibility: hidden; transition: opacity .38s, visibility 0s linear .38s;
}
.showcase-hint .sh-dot {
  width: 6px; height: 6px; border-radius: 50%;
  background: var(--av-jade-lt); box-shadow: 0 0 9px color-mix(in srgb, var(--av-jade-lt) 70%, transparent);
}
body.showcase .showcase-hint { opacity: 1; visibility: visible; transition: opacity .38s, visibility 0s linear 0s; }

/* the loading spinner: amber + jade rings */
.crt-spinner {
  position: absolute; left: 50%; top: calc(50% + 52px); transform: translate(-50%, -50%);
  z-index: 4; width: 46px; height: 46px; pointer-events: none;
}
.vp-loading.hide + .crt-spinner { display: none; }
.crt-spinner .ring {
  position: absolute; inset: 0; border: 2px solid transparent; border-radius: 50%;
  border-top-color: var(--av-amber-lt);
  animation: lab-spin 1.1s cubic-bezier(.55, .15, .45, .85) infinite;
}
.crt-spinner .ring.r2 {
  inset: 8px; border-top-color: transparent; border-bottom-color: var(--av-jade-lt);
  animation: lab-spin-rev 1.6s linear infinite;
}
.crt-spinner .core {
  position: absolute; left: 50%; top: 50%; width: 6px; height: 6px;
  margin: -3px 0 0 -3px; border-radius: 50%;
  background: var(--av-jade-lt); box-shadow: 0 0 14px color-mix(in srgb, var(--av-jade-lt) 60%, transparent);
  animation: lab-pulse 1.1s ease-in-out infinite;
}
@keyframes lab-spin { to { transform: rotate(360deg); } }
@keyframes lab-spin-rev { to { transform: rotate(-360deg); } }
@keyframes lab-pulse { 0%, 100% { opacity: .55; transform: scale(.6); } 50% { opacity: 1; transform: scale(1); } }

/* the stage's own note, and the animation keys, bottom-left */
.stage-note {
  position: absolute; left: 0; right: 0; bottom: 122px; z-index: 6;
  text-align: center; margin: 0; padding: 0 18px;
  font-family: var(--av-sans); font-size: 12px; color: var(--av-ink-2);
}
.stage-note.is-error { color: var(--av-amber-lt); }
.stage-anim {
  /* ★ THE STAGE FOOT IS TWO ROWS NOW (2026-09-07). It was three - clip keys
     LEFT, view sliders RIGHT, orbit hint above - because the sliders needed a
     seat that could not land on the clip keys at 1440. The sliders moved INTO
     the tool rail, so the row is free: the clip keys take it CENTRED, above
     the rail, exactly as the reference stacks them, and the orbit hint drops
     onto the rail's own line at the right edge. Nothing at the foot shares a
     row with anything it can overlap; there is simply one row less to keep
     apart. */
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: calc(var(--lab-bottom, 18px) + 58px); z-index: 6;
  display: inline-flex; align-items: center; justify-content: center;
  gap: 6px; flex-wrap: wrap;
  max-width: min(620px, calc(100% - 48px)); pointer-events: auto;
}
.stage-anim[hidden] { display: none; }
.stage-anim-k {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--av-ink-3);
}

/* the signed-out gate */
.stage-gate {
  position: absolute; inset: 0; z-index: 9;
  display: grid; place-content: center; justify-items: center;
  gap: 14px; text-align: center; padding: 32px;
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-panel) 94%, transparent), color-mix(in srgb, var(--av-sink) 98%, transparent));
}
.stage-gate[hidden] { display: none; }
.stage-gate h3 {
  font-family: var(--av-serif); font-weight: 400; font-size: 26px;
  letter-spacing: .01em; color: var(--av-ink); margin: 0;
}
.stage-gate p { max-width: 40ch; color: var(--av-ink-2); font-size: 13.5px; line-height: 1.6; margin: 0; }

/* -------------------------------------------------------------------------
   5. THE BOTTOM BAND — view sliders, then the amber tool pill.
   ------------------------------------------------------------------------- */
/* ★ THE SLIDERS SIT IN THE RAIL (2026-09-07), so they are two more controls
   on the one bar rather than a second floating cluster - and they survive the
   phone, where the rail folds into a reachable sheet and this pair used to be
   `display: none`. Static, because the rail lays them out. */
.vp-tools {
  position: static; display: flex; flex-direction: row; align-items: center;
  gap: 8px; pointer-events: auto;
}
.vp-rotate, .vp-exposure {
  display: inline-flex; align-items: center; gap: 8px; pointer-events: auto;
  padding: 3px 4px; border-radius: 999px;
  background: none; border: 0; box-shadow: none;
}
.vp-rotate .rot-ico, .vp-exposure .exp-ico { font-size: 13px; color: var(--av-amber-lt); }
.vp-rotate input[type="range"], .vp-exposure input[type="range"],
.stage-growth input[type="range"] {
  -webkit-appearance: none; appearance: none; height: 3px; border-radius: 3px;
  background: color-mix(in srgb, var(--av-ink) 18%, transparent); outline: none; cursor: pointer;
}
.vp-rotate input[type="range"], .vp-exposure input[type="range"] { width: 74px; }
.stage-growth input[type="range"] { width: 150px; }
.vp-rotate input[type="range"]::-webkit-slider-thumb,
.vp-exposure input[type="range"]::-webkit-slider-thumb,
.stage-growth input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; width: 14px; height: 14px; border-radius: 50%;
  background: var(--av-amber-lt); border: 2px solid var(--av-page-deep); cursor: pointer;
}
.vp-rotate input[type="range"]::-moz-range-thumb,
.vp-exposure input[type="range"]::-moz-range-thumb,
.stage-growth input[type="range"]::-moz-range-thumb {
  width: 12px; height: 12px; border-radius: 50%;
  background: var(--av-amber-lt); border: 2px solid var(--av-page-deep); cursor: pointer;
}
.vp-rotate input:disabled, .vp-exposure input:disabled { opacity: .4; cursor: not-allowed; }

/* THE AMBER TOOL RAIL — a pill across the foot of the STAGE.
   #lab-rail is a grid-level element, so it is spanned across the stage
   column and its contents centred; `left:50%` would centre it on the WINDOW
   and slide the pill under a docked column. */
/* --lab-bottom is the height of whatever FIXED bar the owner's site pins to
   the foot of the window (this owner's live strip is one). studiolab.js
   measures it; 18px is the donor's own number for a site with none. */
#lab-rail {
  position: absolute; left: 0; right: 0; bottom: var(--lab-bottom, 18px); z-index: 8;
  display: flex; flex-direction: row; align-items: center; justify-content: center;
  padding: 0 16px; pointer-events: none;
}
.rail-fab { display: none; }
.lab-rail-stack {
  pointer-events: auto; max-width: 100%; flex-wrap: nowrap;
  display: flex; flex-direction: row; align-items: center; justify-content: center;
  gap: 12px; padding: 9px 15px; border-radius: 999px;
  background: linear-gradient(color-mix(in srgb, var(--av-rail-hi) 94%, transparent), color-mix(in srgb, var(--av-rail-lo) 96%, transparent));
  border: 1px solid color-mix(in srgb, var(--av-amber) 34%, transparent);
  box-shadow: 0 24px 52px -26px var(--av-shadow), inset 0 1px 0 color-mix(in srgb, var(--av-ink) 9%, transparent);
}
.rail-colors { display: flex; gap: 7px; }
.rail-colors button, .rail-custom {
  width: 22px; height: 22px; border-radius: 6px; padding: 0; cursor: pointer;
  position: relative; transition: .15s;
  border: 1px solid color-mix(in srgb, var(--av-ink) 22%, transparent);
  box-shadow: inset 0 1px 3px color-mix(in srgb, var(--av-page-deep) 50%, transparent);
}
.rail-colors button:hover, .rail-custom:hover { transform: scale(1.12); }
.rail-colors button.active {
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--av-rail-lo) 95%, transparent), 0 0 0 3px var(--av-jade-lt);
}
/* ONE BEAD RECIPE, EIGHT SEATS. Each button names its chamber and the bead is
   derived - a highlight toward the ink, the colour itself, a shadow toward the
   page's ground - so eight hand-written three-stop gradients became one rule
   that cannot drift between swatches. */
.rail-colors button[data-bg="jade"]  { --cham: var(--av-cham-jade); }
.rail-colors button[data-bg="ember"] { --cham: var(--av-cham-ember); }
.rail-colors button[data-bg="blue"]  { --cham: var(--av-cham-blue); }
.rail-colors button[data-bg="green"] { --cham: var(--av-cham-green); }
.rail-colors button[data-bg="amber"] { --cham: var(--av-cham-amber); }
.rail-colors button[data-bg="white"] { --cham: var(--av-cham-white); }
.rail-colors button[data-bg="red"]   { --cham: var(--av-cham-red); }
.rail-colors button[data-bg="pink"]  { --cham: var(--av-cham-pink); }
.rail-colors button[data-bg] {
  background: radial-gradient(circle at 34% 30%,
    color-mix(in srgb, var(--cham) 58%, var(--av-ink)),
    var(--cham) 60%,
    color-mix(in srgb, var(--cham) 46%, var(--av-page-deep)));
}
/* the custom key wears the whole palette, built from the same eight. */
.rail-custom { overflow: hidden; background: conic-gradient(var(--av-cham-red), var(--av-cham-amber), var(--av-cham-green), var(--av-cham-blue), var(--av-cham-pink), var(--av-cham-red)); }
.rail-custom input[type="color"] {
  position: absolute; inset: -6px; width: calc(100% + 12px); height: calc(100% + 12px);
  opacity: 0; cursor: pointer; border: 0; padding: 0;
}
.rail-divider { width: 1px; height: 24px; align-self: center; background: color-mix(in srgb, var(--av-amber) 30%, transparent); }
.rail-action {
  font: inherit; font-family: var(--av-sans); font-size: 12px; font-weight: 400;
  letter-spacing: .02em; padding: 7px 13px; cursor: pointer; text-align: center;
  border-radius: 0;
  clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 34%, transparent); background: color-mix(in srgb, var(--av-jade-lt) 7%, transparent);
  color: var(--av-btn-ink); white-space: nowrap; transition: .15s;
}
.rail-action:hover { background: color-mix(in srgb, var(--av-jade-lt) 16%, transparent); border-color: var(--av-edge-lit); }
.rail-action[aria-pressed="true"] { background: color-mix(in srgb, var(--av-jade-lt) 20%, transparent); border-color: var(--av-edge-lit); }
.rail-action:disabled { opacity: .4; cursor: not-allowed; }
/* Ultra ON is a STATE, not a press - it gets the jade fill the auto-rotate key
   uses for the same reason. */
#quality-toggle[aria-pressed="true"] {
  background: var(--av-jade-lt); border-color: var(--av-jade-lt); color: var(--av-on-jade);
}

/* -------------------------------------------------------------------------
   6. THE LEFT COLUMN — the species list.
   ------------------------------------------------------------------------- */
#species-rail {
  grid-row: 2; grid-column: 1; position: relative;
  display: flex; flex-direction: column; height: 100%; min-width: 0;
  overflow: hidden; z-index: 24;
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-panel-2) 98%, transparent), var(--av-page));
  border-right: 1px solid var(--av-line);
}
#species-rail[hidden] { display: flex; }
.ctx-collapse { display: none; }
.ctx-title {
  display: flex; align-items: center; height: 52px; margin: 0; padding: 0 16px;
  flex: 0 0 auto;
  font-family: var(--av-serif); font-weight: 500; font-size: 16px;
  letter-spacing: .01em; text-transform: none; color: var(--av-ink);
  background: linear-gradient(color-mix(in srgb, var(--av-panel-3) 94%, transparent), color-mix(in srgb, var(--av-panel) 94%, transparent));
  border-bottom: 1px solid var(--av-line-dim);
}
.ctx-body { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
.deck-search { flex: 0 0 auto; padding: 10px 12px 0; }
#deck-filter {
  width: 100%; font: inherit; font-family: var(--av-sans); font-size: 12px;
  padding: 8px 10px; border-radius: 0;
  background: color-mix(in srgb, var(--av-sink) 85%, transparent); border: 1px solid color-mix(in srgb, var(--av-jade-lt) 20%, transparent);
  color: var(--av-ink); outline: 0;
}
#deck-filter:focus { border-color: var(--av-jade); box-shadow: 0 0 0 1px color-mix(in srgb, var(--av-jade) 40%, transparent); }
.deck-list {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 10px 12px 16px; display: flex; flex-direction: column; gap: 4px;
}
.deck-row {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  text-align: left; cursor: pointer; border-radius: 0;
  clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
  border: 1px solid transparent; background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  font-family: var(--av-sans); font-size: 12px; letter-spacing: .02em;
  color: var(--av-ink); padding: 8px 11px; transition: .14s;
}
.deck-row b { font: inherit; font-weight: 400; color: inherit; }
.deck-row span {
  font-family: var(--av-sans); font-size: 9.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--av-ink-3);
}
.deck-row:hover { border-color: var(--av-line); background: color-mix(in srgb, var(--av-jade) 9%, transparent); }
.deck-row.on { background: color-mix(in srgb, var(--av-jade) 14%, transparent); border-color: var(--av-edge-lit); }
.deck-row:focus-visible { outline: 2px solid var(--av-focus); outline-offset: -2px; }
.deck-empty { font-size: 12px; color: var(--av-ink-3); padding: 8px 4px; }

/* -------------------------------------------------------------------------
   7. THE RIGHT COLUMN — the skin controls, and its two faces.
   ------------------------------------------------------------------------- */
/* ★★★★★ THE COLUMN. Two rows: the doors, then whichever face is open.
   Before 2026-08-25 these were three siblings in one grid cell, each pushed
   clear of the tab row by a hardcoded 52px - so the row could never be
   anything but 52px, and never more than one line. See the fragment's note
   at #lab-col. */
#lab-col {
  grid-row: 2; grid-column: 3; min-width: 0; min-height: 0;
  display: grid; grid-template-rows: auto minmax(0, 1fr);
}
#lab-col > #lab-tabs { grid-row: 1; }
#lab-col > #lab-dock-panel, #lab-col > #panel { grid-row: 2; }

#lab-tabs {
  align-self: start;
  position: relative; z-index: 41; display: flex; align-items: stretch;
  /* ★ THE ROW WRAPS AND THE LABELS STAY WHOLE (an owner 2026-08-25,
     from his own screenshot: "YOUR L..." - and it was ellipsised at 1440 as
     well as on a phone, so it was never a width problem). `flex: 1 1 0`
     gives four tracks the SAME width whatever they say, which cuts the
     longest label every time; `flex: 1 1 auto` starts each track at its own
     words and shares only the SPARE. A row that still cannot fit takes a
     second line rather than clipping - the clipped-nav law: a clipped label
     is unreachable by any gesture and nothing errors. */
  flex-wrap: wrap; row-gap: 0;
  min-height: 52px; width: 100%;
  background: linear-gradient(color-mix(in srgb, var(--av-panel-3) 97%, transparent), color-mix(in srgb, var(--av-panel) 97%, transparent));
  border-bottom: 1px solid var(--av-line-dim);
  border-left: 1px solid var(--av-line);
}
#lab-tabs .lab-tab {
  /* 2026-08-20: the row holds FOUR tabs now (Editor/Custom/Glitch/Saved) -
     at the old 15px/10px "Custom" ellipsised in the 1440 rail. */
  flex: 1 1 auto; min-width: 0; padding: 14px 8px 12px; border: 0; background: none; cursor: pointer;
  font-family: var(--av-serif); font-weight: 500; font-size: 14px; letter-spacing: .01em;
  color: var(--av-ink-3); border-bottom: 2px solid transparent;
  transition: color .15s, border-color .15s;
  white-space: nowrap; overflow: visible; text-overflow: clip;
}
#lab-tabs .lab-tab::before, #lab-tabs .lab-tab::after { content: none; }
#lab-tabs .lab-tab:hover { color: var(--av-ink-2); }
#lab-tabs .lab-tab.active { color: var(--av-ink); border-bottom-color: var(--av-jade-lt); }
#lab-tabs .lab-tab:focus-visible { outline: 2px solid var(--av-focus); outline-offset: -2px; }

#lab-dock-panel {
  position: relative;
  display: flex; flex-direction: column; height: 100%; min-width: 0;
  overflow: hidden; z-index: 24;
}
.dock-connector, .dock-handle { display: none; }
.deck {
  /* ★★★★★ RESERVE THE OWNER'S FIXED BAR. The column is `height: 100%` of its
     grid area and that area runs UNDER whatever bar the site pins to the
     bottom of the window - so its last row was drawn behind the live bar and
     cut in half. Owner, 2026-08-17: the import controls sat under the fixed
     bottom bar until a code key was pressed - the press
     scrolled them into view, which is how a control can be present, correct,
     reachable and still, honestly, missing.

     `--lab-bar` is that bar measured at runtime (studiolab.js), so this
     follows an owner whose bar is a different height and costs nothing on one
     who has no bar at all. */
  height: 100%; max-height: none; overflow: hidden;
  display: flex; flex-direction: column; gap: 0;
  /* ★ AFTER the shorthand, never before it. The first cut of this put
     `padding-bottom` above `padding: 0` in the same rule, so the shorthand
     reset it to nothing and the reservation measured as applied while doing
     absolutely nothing - the block still ended exactly at the window's foot,
     under the bar. Same rule, later declaration wins. */
  padding: 0 0 var(--lab-bar, 0px);
  box-sizing: border-box;
  border-left: 1px solid var(--av-line);
  background: linear-gradient(180deg, var(--av-deck-top) 0%, var(--av-panel) 46%, var(--av-deck-bot) 100%);
}
.deck-row-l { display: flex; align-items: center; gap: 10px; }
.deck > .deck-row-l {
  border-top: 1px solid var(--av-line-dim); padding: 13px 16px; flex-wrap: wrap;
}
.deck > .deck-row-l:first-child { border-top: 0; }
/* ★ nowrap IS LOAD-BEARING. `.deck > .deck-row-l` wraps, and a COLUMN flex
   that also wraps puts its overflow in a SECOND COLUMN - which threw the
   pattern-size note 200px clear of the panel's right edge. */
.deck > .deck-row-l.deck-head { flex-wrap: nowrap; }
.deck-head { flex-direction: column; align-items: stretch; }
.deck-field { display: flex; flex-direction: column; gap: 5px; flex: 1 1 auto; min-width: 0; }
.micro-label, .stage-foot-k {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--av-ink-3); white-space: nowrap;
}
#design-name {
  width: 100%; height: 32px; font: inherit; font-family: var(--av-serif);
  font-size: 14.5px; font-weight: 500; padding: 5px 11px; border-radius: 0;
  background: color-mix(in srgb, var(--av-field) 90%, transparent); border: 1px solid var(--av-line);
  color: var(--av-ink); outline: 0;
}
#design-name::placeholder { color: var(--av-ink-3); }
#design-name:focus { border-color: var(--av-jade); box-shadow: 0 0 0 1px color-mix(in srgb, var(--av-jade) 40%, transparent); }

.stage-foot { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.chips { display: inline-flex; gap: 5px; flex-wrap: wrap; }
.chip {
  font: inherit; font-family: var(--av-sans); font-size: 11.5px;
  min-width: 32px; padding: 6px 9px; border-radius: 0; cursor: pointer;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 4%, transparent);
  color: var(--av-ink); transition: .15s;
}
.chip:hover:not(:disabled) { border-color: var(--av-edge-lit); }
.chip.on { color: var(--av-on-jade); background: var(--av-jade-lt); border-color: var(--av-jade-lt); }
.chip:disabled { opacity: .4; cursor: not-allowed; }
.studio-size-note {
  flex-basis: 100%; margin: 2px 0 0;
  font-family: var(--av-sans); font-size: 10.5px; line-height: 1.45; color: var(--av-ink-3);
}

/* ★★★★★ EVERY COLOUR MUST BE ON SCREEN AT ONCE, WITHOUT SCROLLING.
   Owner ruling 2026-08-17: the colour rows must not need scrolling to
   reach. Seven channels is the whole product of this page - a player picks
   them against each other, and a list you have to scroll to see the end of is
   a list you cannot compare. So the head and the foot of this column are
   budgeted to leave the rows room, in this order:

     · the pattern row is ONE line, not four stacked ones (label, chips,
       label, chips read across; they were 126px stacked, 44px across);
     · the size EXPLAINER leaves the page and becomes the row's tooltip -
       studio.mjs already sets it as `title`, and explainer copy is removed
       fleet-wide by owner order anyway;
     · the action row is compact, and its note is one small line.

   Measured at 1600x950: head 260 -> 106, palette 116 -> 46, actions 156 ->
   112, which buys the seven rows 159px more than they need. */
/* the head is budgeted for a 768-tall laptop, which is where the seven rows
   ran out of room first: name on one line, pattern and size across the next. */
.deck-row-l.deck-head { padding-top: 10px; padding-bottom: 10px; gap: 8px; }
.deck-name .micro-label { font-size: 9.5px; letter-spacing: .14em; }
.deck-name { gap: 4px; }
/* ★★★★★ EVERY CHIP GROUP CARRIES ITS NAME, ON ITS OWN LINE, IN A COLUMN
   (2026-08-25, an owner: three unlabelled chip rows stacked -
   "A B C D", "SMALL MEDIUM LARGE", "DEFAULT SECONDARY" - and no way for a
   player to tell a pattern letter from a body size from a theme).

   IT WAS A FLEX ROW AND THAT IS WHY THE NAMES WENT. studio.mjs appends the
   size and theme controls as flat key-then-chips SIBLINGS of the pattern
   pair, so a wrapping row could break between a caption and the chips it
   names; the cure was `flex-basis: 100%` on each caption, which cost a whole
   line per group - and the short-screen tier then bought that height back by
   DELETING the captions (`display: none`), which is how a 1440x900 monitor
   ended up showing three anonymous chip rows.

   A TWO-TRACK GRID pairs them by construction: auto-flow puts caption,
   chips, caption, chips into column 1, column 2, next row, and nothing can
   strand a name from its group however many groups studio.mjs appends. It
   also costs THREE lines where the flex row cost six, so the captions can
   stay on at every height - the height was the whole reason they were cut. */
.deck-pattern.deck-field {
  display: grid; grid-template-columns: auto minmax(0, 1fr);
  align-items: center; gap: 7px 12px;
}
.deck-pattern .chips { flex: 0 1 auto; }
.deck-pattern .stage-foot-k,
.deck-pattern #size-chips-k,
.deck-pattern .skin-theme-label { grid-column: 1; justify-self: start; }
.deck-pattern .chips { grid-column: 2; }
/* the note is the row's TOOLTIP, not a paragraph in the panel */
.studio-size-note { display: none; }

/* THE COLOUR ROWS. A 336px column cannot carry seven 84px tiles without
   truncating every label, so the wells are a LIST: swatch, name, value. */
.deck-wells { flex-direction: column; align-items: stretch; }
.deck-row-l.deck-wells {
  /* ★★★★★ THE WELLS CANNOT COLLAPSE — BUT NOT BECAUSE OF THIS RULE (the
     2026-08-30 adversarial pass, measured in a real browser at 8 viewports).
     The desktop guarantor is the ONE-SCROLL-REGION block near the sheet's
     foot: `body.studiolab .deck-row-l.deck-wells { flex: 0 0 auto;
     min-height: auto; overflow: visible }` + `body.studiolab .deck
     { overflow-y: auto }` — natural row height always, the DECK scrolls.
     A min-height floor written HERE is shadowed by that block's higher
     specificity and never applies (a first cut of the 2026-08-30 wave
     shipped exactly that inert floor and its equally inert short-height
     tier; both were measured dead and removed). The rows' guarantee is
     pinned where it really lives: tests/frontend/test_studio_wells_floor.py
     asserts the FOOT block's declarations, with a red control.
     The owner's "i cant even pick colors" screenshot was the sub-901px
     PHONE band, where the sheet legitimately folds rows behind its dock —
     the colour-lab popover is the access cure there, not a floor. */
  flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain;
  align-items: flex-start; justify-content: flex-start; flex-wrap: nowrap;
}
#channel-list { display: flex; flex-direction: column; gap: 4px; width: 100%; }
.channel {
  display: grid; grid-template-columns: auto minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px; width: 100%; flex: 0 0 auto; padding: 5px 10px; cursor: pointer;
  border-radius: 0;
  clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
  border: 1px solid transparent; background: color-mix(in srgb, var(--av-ink) 3%, transparent); transition: .15s;
}
.channel:hover, .channel:focus-within { border-color: var(--av-line); background: color-mix(in srgb, var(--av-jade) 9%, transparent); }
/* yields its own box so the picker and the hex become row cells */
.channel-io { display: contents; }
.channel input[type="color"] {
  order: 1; display: block; width: 34px; height: 24px; padding: 0; border-radius: 0;
  background: transparent; cursor: pointer;
  border: 1px solid color-mix(in srgb, var(--av-ink) 22%, transparent);
}
.channel input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
.channel input[type="color"]::-webkit-color-swatch { border: 0; border-radius: 0; }
.channel input[type="color"]::-moz-color-swatch { border: 0; border-radius: 0; }
.channel:hover input[type="color"] { box-shadow: 0 0 0 1px var(--av-edge-lit); }
.channel label {
  order: 2; min-width: 0; text-align: left; cursor: pointer;
  font-family: var(--av-sans); font-size: 11px; font-weight: 400; letter-spacing: .06em;
  color: var(--av-ink-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.channel:hover label { color: var(--av-ink); }
/* The donor's rows carry no hex box; the shared frontend's do, and a control
   that exists must stay reachable - so it reads as the row's VALUE, right-aligned. */
.channel .hex {
  order: 3; width: 9.5ch; padding: 3px 0; margin: 0; border: 0; border-radius: 0;
  background: transparent; color: var(--av-ink-3); text-align: right;
  font-family: var(--av-sans); font-size: 10.5px; letter-spacing: .04em;
  text-transform: lowercase; outline: 0; font-variant-numeric: tabular-nums;
}
.channel .hex:focus { background: color-mix(in srgb, var(--av-sink) 85%, transparent); color: var(--av-ink); box-shadow: inset 0 0 0 1px var(--av-jade); }
/* ★★★★★ AN ADVANCED SLOT WITH NO PREVIEW MAP SAYS SO, ON ITS OWN LINE —
   AND THIS REGISTER IS A GRID, SO THE SHOP THEME'S FLEX FIX DOES NOT CARRY
   (2026-08-24, MEASURED on a sibling owner's live studio the moment the v2
   editor first rendered). teeth/mouth/claws are editable wherever the
   island's manifest lists them, but the 3D specimen can only paint them
   where that species also declares a `preview_channel_mapping`; studio.mjs
   then prints an honest caption. studio.css (the shop register) gave it
   `order: 3; flex: 1 1 100%` — correct there, inert here, because
   `.channel` in THIS sheet is `display: grid` with three tracks and
   explicit `order` on the dial (1), the label (2) and the hex (3). An
   unordered fourth child defaults to `order: 0`, so the caption seized the
   FIRST cell, pushed the label into the second and wrapped the dial and the
   hex onto an implicit second row: the two advanced rows on Omniraptor read
   as broken furniture beside seven clean ones. It is a caption about the
   row, so it takes the row's full width UNDER the controls it explains and
   every channel keeps the one shape. */
.channel-preview-note {
  order: 4; grid-column: 1 / -1; margin: 2px 0 0;
  font-family: var(--av-sans); font-size: 10px; font-weight: 400;
  line-height: 1.35; letter-spacing: .1em; text-transform: uppercase;
  color: var(--av-ink-3);
}

/* the actions, pinned to the foot of the column */
.deck-row-l.deck-actions {
  flex: 0 0 auto; margin-top: 0; padding-top: 10px; padding-bottom: 10px;
  border-top: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-sink) 55%, transparent);
  display: flex; flex-direction: row; flex-wrap: wrap; align-items: stretch; gap: 6px;
}
.deck-gap { display: none; }
.save-row { display: contents; }
.btn {
  font: inherit; font-family: var(--av-sans); font-size: 11.5px; font-weight: 400;
  letter-spacing: .02em; text-transform: none; cursor: pointer; text-align: center;
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  min-height: 36px; padding: 8px 10px; border-radius: 0; text-decoration: none;
  clip-path: polygon(8px 0, 100% 0, 100% calc(100% - 8px), calc(100% - 8px) 100%, 0 100%, 0 8px);
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 34%, transparent); background: color-mix(in srgb, var(--av-jade-lt) 7%, transparent);
  color: var(--av-btn-ink); white-space: nowrap; transition: .15s;
}
.btn:hover { background: color-mix(in srgb, var(--av-jade-lt) 16%, transparent); border-color: var(--av-edge-lit); }
.btn:disabled { opacity: .4; cursor: not-allowed; }
.btn::before { content: none; }
.btn:focus-visible, .rail-action:focus-visible { outline: 2px solid var(--av-focus); outline-offset: 2px; }
.btn-primary, .btn.primary {
  background: var(--av-jade-lt); border-color: var(--av-jade-lt);
  color: var(--av-on-jade); font-weight: 600;
}
.btn-primary:hover, .btn.primary:hover { background: var(--av-jade-hot); border-color: var(--av-jade-hot); }
.btn.save { border-color: color-mix(in srgb, var(--av-amber-lt) 55%, transparent); color: var(--av-amber-lt); }
.btn-ghost { background: color-mix(in srgb, var(--av-ink) 4%, transparent); border-color: var(--av-line); }
.btn-sm { min-height: 32px; padding: 6px 9px; font-size: 11px; }
.deck-actions .btn { flex: 1 1 calc(50% - 3px); width: auto; min-width: 0; min-height: 32px; padding: 6px 10px; }
.deck-actions .deck-apply { flex: 1 1 100%; min-height: 38px; padding: 9px 16px; font-size: 12.5px; }
.save-note { flex: 1 1 100%; margin: 0; font-size: 9.5px; color: var(--av-ink-3); line-height: 1.35; }

/* -------------------------------------------------------------------------
   8. THE LIBRARY FACE — docked into the same column, never sliding over.
   ------------------------------------------------------------------------- */
#lab-drawer-scrim { display: none; }
#panel {
  position: relative;
  width: 100%; height: 100%; min-width: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 16px 16px 30px; z-index: 26;
  border-left: 1px solid var(--av-line);
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-panel-2) 98%, transparent), var(--av-page));
  display: none;
}
body.drawer-open #panel { display: block; }
body.drawer-open #lab-dock-panel { visibility: hidden; }
.drawer-head { display: none; }

body.studiolab #panel h2, body.studiolab #panel h3 {
  display: flex; align-items: center; gap: 8px; margin: 0 0 11px;
  font-family: var(--av-sans); font-size: 10.5px; font-weight: 500;
  letter-spacing: .2em; text-transform: uppercase; color: var(--av-ink-2);
}
body.studiolab #panel h2::before, body.studiolab #panel h3::before {
  content: ""; width: 5px; height: 5px; border-radius: 50%; flex: 0 0 auto;
  background: var(--av-jade-lt); box-shadow: 0 0 8px color-mix(in srgb, var(--av-jade-lt) 60%, transparent);
}
body.studiolab #panel .right { margin-left: auto; font-size: 10px; color: var(--av-ink-3); }
body.studiolab #panel p { font-family: var(--av-sans); font-size: 12px; line-height: 1.6; color: var(--av-ink-2); }
body.studiolab #panel .dim { color: var(--av-ink-3); }
.lab-section { margin-bottom: 20px; }
.lab-section + .lab-section { padding-top: 18px; border-top: 1px solid var(--av-line-dim); }
.lab-lede { margin: 0 0 16px; font-size: 12.5px; line-height: 1.6; color: var(--av-ink-2); }

body.studiolab .studio-tabs {
  display: flex; gap: 4px; padding: 4px; margin-bottom: 16px; border-radius: 0;
  background: color-mix(in srgb, var(--av-sink) 55%, transparent); border: 1px solid var(--av-line-dim);
}
body.studiolab .studio-tabs [role="tab"] {
  flex: 1; font: inherit; font-family: var(--av-sans); font-size: 12px;
  padding: 8px 10px; border: 0; border-radius: 0; min-height: 36px;
  background: transparent; color: var(--av-ink-3); cursor: pointer; transition: .15s;
}
body.studiolab .studio-tabs [role="tab"]:hover { color: var(--av-ink-2); }
body.studiolab .studio-tabs [role="tab"][aria-selected="true"] {
  color: var(--av-on-jade); background: var(--av-jade-lt);
}

body.studiolab .studio-panel-label {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--av-ink-3); margin: 0 0 8px;
}
.presets { display: flex; flex-wrap: wrap; gap: 5px; }
.preset {
  font: inherit; font-family: var(--av-sans); font-size: 11.5px;
  padding: 7px 10px; border-radius: 0; cursor: pointer;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 4%, transparent);
  color: var(--av-ink-2); transition: .15s;
}
.preset:hover { color: var(--av-ink); border-color: var(--av-edge-lit); }

.code-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 4px; }
body.studiolab #code-input, body.studiolab #code-name, body.studiolab .skin-pattern,
body.studiolab #slotdeck-name, body.studiolab .slotdeck-target {
  font: inherit; font-family: var(--av-sans); font-size: 12px;
  padding: 8px 10px; border-radius: 0;
  background: color-mix(in srgb, var(--av-sink) 85%, transparent); border: 1px solid color-mix(in srgb, var(--av-jade-lt) 20%, transparent);
  color: var(--av-ink); outline: 0;
}
body.studiolab #code-input { flex: 1 1 170px; min-width: 0; }
body.studiolab #code-name { flex: 0 1 130px; min-width: 0; }
body.studiolab #code-input:focus, body.studiolab #code-name:focus, body.studiolab .skin-pattern:focus,
body.studiolab #slotdeck-name:focus, body.studiolab .slotdeck-target:focus {
  border-color: var(--av-jade); box-shadow: 0 0 0 1px color-mix(in srgb, var(--av-jade) 40%, transparent);
}
#code-note { flex-basis: 100%; margin: 0; font-size: 11px; }
.skin-pattern { flex: 0 0 auto; max-width: 44%; }

.designs { border-top: 1px solid var(--av-line-dim); padding-top: 12px; margin-top: 4px; }
.design-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 5px; }
.design-load {
  flex: 1 1 auto; min-width: 9em;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 7px 10px; text-align: left; cursor: pointer; border-radius: 0;
  clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
  background: color-mix(in srgb, var(--av-ink) 3%, transparent); border: 1px solid transparent;
  color: var(--av-ink-2); transition: .15s;
}
.design-load:hover { border-color: var(--av-line); background: color-mix(in srgb, var(--av-jade) 9%, transparent); }
.design-load b { font: inherit; font-size: 12.5px; font-weight: 500; color: var(--av-ink); overflow-wrap: anywhere; }
.design-load span { font-family: var(--av-sans); font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--av-ink-3); }
.design-row.editing .design-load { border-color: var(--av-edge-lit); background: color-mix(in srgb, var(--av-jade) 14%, transparent); }
.design-row .btn, .design-row .design-del { flex: 0 0 auto; white-space: nowrap; }
.design-row .btn { min-height: 30px; padding: 5px 9px; font-size: 10.5px; }
.design-row .skin-pattern { max-width: 9em; }
.design-del {
  width: 30px; height: 30px; flex: none; border-radius: 0;
  border: 1px solid color-mix(in srgb, var(--av-bad) 35%, transparent); background: transparent;
  color: var(--av-bad); font: 14px/1 var(--av-sans); cursor: pointer;
}
.design-del:hover { border-color: var(--av-bad); background: color-mix(in srgb, var(--av-bad) 12%, transparent); }
.designs-empty, .preset-empty { font-family: var(--av-sans); font-size: 11.5px; color: var(--av-ink-3); padding: 8px 4px; }
.skin-row .skin-name { cursor: default; }
.skin-row .skin-name:hover { border-color: transparent; background: color-mix(in srgb, var(--av-ink) 3%, transparent); }
.skin-row .skin-name span { color: var(--av-jade-lt); }

/* -------------------------------------------------------------------------
   8b. THE SAVED-SKIN SLOTS — the BOT's five, drawn beside the site's library.

   ★ THE PREFIX IS THE POINT. Every class here is `slotdeck-*` and none of
   them is `.slot`, `.item` or any other word the house look already owns.
   terminal.css section 6b dresses `.slot` as a shared-frontend RECORD - a hairline,
   the house radius, the house ink - and this page is a page THEME whose whole
   contract is that it replaces that language rather than wearing it. A class
   named for the thing instead of for this chrome would have quietly inherited
   the wrong plate on the one page that must not have it.

   Everything below reuses the library's own geometry (`.design-row` and
   `.design-load` two blocks up) so the two lists read as one column, and every
   colour is one of this theme's own `--av-*` tokens.
   ------------------------------------------------------------------------- */
.slotdeck-save { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
body.studiolab #slotdeck-name { flex: 1 1 150px; min-width: 0; }
/* The box is shut whenever the key is (studiolab.js, slotPaint), so it has to
   READ shut - the same .4 the buttons use. A field that looks live and takes
   no typing is the dead control this whole panel refuses to ship. */
body.studiolab #slotdeck-name:disabled { opacity: .45; cursor: not-allowed; }
.slotdeck-save .btn { flex: 0 0 auto; }
#slotdeck-note { margin: 0 0 8px; font-size: 11px; }
/* The refusal reads as a refusal. One tone for "the bot said no" and for the
   inline cap message, because they are the same fact to a player. */
#slotdeck-note.is-bad { color: var(--av-amber-lt); }

.slotdeck-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-bottom: 5px; }
.slotdeck-name {
  flex: 1 1 auto; min-width: 9em;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  padding: 7px 10px; border-radius: 0;
  clip-path: polygon(7px 0, 100% 0, 100% calc(100% - 7px), calc(100% - 7px) 100%, 0 100%, 0 7px);
  background: color-mix(in srgb, var(--av-ink) 3%, transparent); border: 1px solid transparent;
  color: var(--av-ink-2);
}
.slotdeck-name b { font: inherit; font-size: 12.5px; font-weight: 500; color: var(--av-ink); overflow-wrap: anywhere; }
.slotdeck-name span { font-family: var(--av-sans); font-size: 9.5px; letter-spacing: .12em; text-transform: uppercase; color: var(--av-ink-3); }
.slotdeck-row .btn { flex: 0 0 auto; min-height: 30px; padding: 5px 9px; font-size: 10.5px; white-space: nowrap; }
/* ARMED, not a dialog. The first press asks and the second commits, so a
   delete needs two deliberate taps and no modal has to be dismissed on a
   phone where the panel is already a sheet. */
.slotdeck-del {
  width: 30px; height: 30px; flex: none; border-radius: 0;
  border: 1px solid color-mix(in srgb, var(--av-bad) 35%, transparent); background: transparent;
  color: var(--av-bad); font: 14px/1 var(--av-sans); cursor: pointer;
}
.slotdeck-del:hover { border-color: var(--av-bad); background: color-mix(in srgb, var(--av-bad) 12%, transparent); }
.slotdeck-del.is-armed { width: auto; padding: 0 9px; border-color: var(--av-bad); background: color-mix(in srgb, var(--av-bad) 12%, transparent); font-size: 10.5px; }

/* the apply picker, opened inside the row it belongs to so the answer can
   never be about a different slot than the one that was pressed */
.slotdeck-pick { flex: 1 1 100%; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 0 0 4px 10px; }
body.studiolab .slotdeck-target { flex: 1 1 150px; min-width: 0; }
.slotdeck-pick .btn { flex: 0 0 auto; }
.slotdeck-pick .designs-empty { flex: 1 1 100%; padding: 0 0 4px; }

/* the glitch shelf and the typed composer */
.glitch-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.glitch-tile {
  position: relative; overflow: hidden; display: flex; flex-direction: column; gap: 8px;
  padding: 10px; border-radius: 0;
  clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
  background: color-mix(in srgb, var(--av-ink) 3%, transparent); border: 1px solid var(--av-line-dim); transition: .15s;
}
.glitch-tile:hover { border-color: var(--av-line); background: color-mix(in srgb, var(--av-jade) 7%, transparent); }
.glitch-tile.is-owned { border-color: var(--av-edge-lit); }
body.studiolab .glitch-tile h3 { font-family: var(--av-serif); font-weight: 400; font-size: 14px; letter-spacing: 0; color: var(--av-ink); margin: 0; text-transform: none; }
body.studiolab .glitch-tile h3::before { display: none; }
body.studiolab .glitch-tile p { margin: 0; font-size: 11.5px; color: var(--av-ink-2); }
.glitch-prox { display: flex; width: 100%; aspect-ratio: 5 / 4; overflow: hidden; margin: 0; border-radius: 0; }
.glitch-prox i { flex: 1 1 0; min-width: 0; display: block; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--av-page-deep) 35%, transparent); }
.glitch-cmd { font-family: var(--av-sans); font-size: 10.5px; color: var(--av-ink-3); }
.glitch-cmd code { font-family: var(--av-sans); color: var(--av-jade-lt); background: color-mix(in srgb, var(--av-sink) 60%, transparent); padding: 2px 5px; }
.glitch-buy { display: flex; gap: 6px; }
.glitch-buy .btn { flex: 1; justify-content: center; }
.glitch-buy .btn.is-armed { border-color: var(--av-amber-lt); color: var(--av-amber-lt); }
.glitch-note { font-family: var(--av-sans); font-size: 10.5px; margin: 6px 0 0; color: var(--av-ink-2); }
.glitch-note:empty { display: none; }
.glitch-shimmer { display: none; }

.gc-lab { display: flex; flex-wrap: wrap; gap: 10px 14px; align-items: flex-end; margin-top: 10px; }
.gc-lab-field { display: grid; gap: 5px; }
.gc-lab-acts { display: flex; gap: 6px; }
.gc-lab-label, .gc-slot-name, .gc-chan-k {
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--av-ink-3);
}
.gc-chip-row { display: flex; flex-wrap: wrap; gap: 4px; }
/* .gc-chan-row was in the line above until 2026-08-17. It is a GRID now (see
   below) and leaving it here left two rules governing one row, with only
   source order deciding - the kind of pair a later reorder turns back into
   the wrapped alpha column the owner reported. */
.gc-chip {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 32px; padding: 6px 9px; cursor: pointer; border-radius: 0;
  font-family: var(--av-sans); font-size: 11.5px;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 4%, transparent); color: var(--av-ink-2);
}
.gc-chip:hover { border-color: var(--av-edge-lit); color: var(--av-ink); }
.gc-chip.is-on { background: var(--av-jade-lt); border-color: var(--av-jade-lt); color: var(--av-on-jade); }
.gc-slots { display: grid; gap: 8px; margin-top: 14px; }
.gc-slot-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px; align-items: center; }
.gc-num, body.studiolab #gc-name {
  width: 8ch; padding: 6px 7px; border-radius: 0;
  font-family: var(--av-sans); font-size: 11.5px; text-align: right;
  font-variant-numeric: tabular-nums;
  background: color-mix(in srgb, var(--av-sink) 85%, transparent); border: 1px solid color-mix(in srgb, var(--av-jade-lt) 20%, transparent);
  color: var(--av-ink); outline: 0;
}
body.studiolab #gc-name { width: auto; flex: 1 1 130px; text-align: left; }
.gc-num:focus, body.studiolab #gc-name:focus { border-color: var(--av-jade); box-shadow: 0 0 0 1px color-mix(in srgb, var(--av-jade) 40%, transparent); }
.gc-num.is-bad { border-color: var(--av-bad); }
/* ★ THE R G B A HEADER HAD NOTHING UNDER ITS LAST COLUMN.
   The header spans were 8ch each with no padding, while `.gc-num` is 8ch PLUS
   14px of padding and a border - so four values were wider than four headers,
   the flex row wrapped, and the alpha field dropped onto its own line under a
   header that stayed put. Owner, 2026-08-17: the R G B A header read misaligned.

   A header and its values cannot be laid out by two different rules and be
   expected to line up. One grid, four equal tracks, shared by both rows, and
   the inputs fill their track instead of setting their own width - so they
   align at every panel width and can never wrap. */
.gc-chan-row {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-column: 1 / -1;
  gap: 4px;
  align-items: center;
}
.gc-chan-row .gc-num { width: 100%; min-width: 0; }
.gc-chan-k { width: auto; min-width: 0; text-align: center; }
.gc-chan-head { margin-bottom: 2px; }
.gc-save-row { margin-top: 14px; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
.gc-price-note { flex-basis: 100%; }
.gc-try { white-space: nowrap; }
.gc-preview-head {
  display: grid; grid-template-columns: minmax(0, 1fr) auto;
  align-items: center; gap: 8px 12px; padding: 10px;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-sink) 62%, transparent);
}
.gc-preview-title { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.gc-preview-contract {
  padding: 3px 6px; border: 1px solid var(--av-jade); color: var(--av-jade-lt);
  font: 600 9px/1 var(--av-sans); letter-spacing: .08em;
}
.gc-preview-contract[data-state="fault"] { border-color: var(--av-bad); color: var(--av-bad); }
.gc-preview-head #gc-preview-status { grid-column: 1 / -1; margin: 0; }
.gc-history-tools, .gc-share-actions, .gc-seed-row {
  display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
}
.gc-history-count, .gc-recipe-id { color: var(--av-ink-3); font: 10px/1 var(--av-sans); }
.gc-random-advanced { display: grid; gap: 8px; margin-top: 12px; }
.gc-authentic-note { margin: 3px 0 0; max-width: 72ch; line-height: 1.45; }
#gc-seed, #gc-import-text {
  min-width: 0; padding: 7px 8px;
  border-color: color-mix(in srgb, var(--av-jade-lt) 20%, transparent); background: color-mix(in srgb, var(--av-sink) 85%, transparent);
  color: var(--av-ink); border-style: solid; border-width: 1px;
  border-radius: 0; font: 12px var(--av-sans);
}
#gc-seed { flex: 1 1 160px; }
#gc-import-text { width: 100%; resize: vertical; box-sizing: border-box; }
#gc-seed:focus, #gc-import-text:focus { border-color: var(--av-jade); }
.gc-lock {
  min-width: 58px; padding: 6px; cursor: pointer; background: transparent;
  border: 1px solid var(--av-line); color: var(--av-ink-3); border-radius: 0;
  font: 600 9px/1 var(--av-sans); letter-spacing: .06em;
}
.gc-lock.is-on { border-color: var(--av-jade); color: var(--av-jade-lt); }
.gc-share {
  margin-top: 14px; padding: 10px; border: 1px solid var(--av-line);
  background: color-mix(in srgb, var(--av-sink) 62%, transparent);
}
.gc-share.is-drag { border-color: var(--av-jade); }
.gc-share summary { cursor: pointer; color: var(--av-ink); font-family: var(--av-sans); font-weight: 600; }
.gc-share > * + * { margin-top: 9px; }
.gc-file-label { position: relative; overflow: hidden; }
.gc-file-label input { position: absolute; width: 1px; height: 1px; opacity: 0; }
.gc-editing { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* The composer pane's own 3D stage (glitchcompose.mjs; proven live on
   Jurassic Land 2026-08-29). The design stage is hidden on this tab and
   measures 0x0, so the composer owns a canvas inside the pane the player is
   looking at. Emitted only where the custom lane is on. */
.gc-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}
.gc-stage { display: flex; flex-direction: column; gap: 10px; position: sticky; top: 14px; }
.gc-stage-wrap { position: relative; height: 420px; }
#gc-stage-canvas { position: absolute; inset: 0; width: 100%; height: 100%; touch-action: none; }
.gc-stage-status { margin: 0; font-size: 12px; line-height: 1.45; }

@media (max-width: 1080px) {
  .gc-preview-head { grid-template-columns: 1fr; }
  .gc-layout { grid-template-columns: minmax(0, 1fr); }
  .gc-stage { position: static; }
  .gc-stage-wrap { height: 340px; }
}

/* the tier gate card studio.mjs draws into #main */
#studio-tier-gate {
  position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  z-index: 26; width: min(520px, calc(100vw - 40px));
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-panel-2) 98%, transparent), var(--av-page));
  border: 1px solid var(--av-line);
}
#studio-tier-gate h3 { font-family: var(--av-serif); font-weight: 400; font-size: 22px; color: var(--av-ink); margin: 0; }
#studio-tier-gate p { font-family: var(--av-sans); font-size: 13px; color: var(--av-ink-2); }
body.lab-gated #lab-dock-panel, body.lab-gated #lab-rail, body.lab-gated #species-rail,
body.lab-gated .vp-overlay, body.lab-gated #panel, body.lab-gated #lab-tabs,
/* ★ THE WRAPPER GOES WITH ITS CHILDREN (2026-08-25). Hiding only the three
   faces left #lab-col standing as an empty 336px grid track beside the gate
   card - the room was gone and its wall was not. */
body.lab-gated #lab-col { display: none !important; }

/* EFFECTS OFF — the rail's own switch. It strips the ROOM (grain, vignette,
   light shaft) and leaves the specimen and every control where they were. A
   player checking a colour against a plain ground should not have to leave
   the studio to do it. */
body.lab-fx-off #lab-grain,
body.lab-fx-off #lab-cloud,
body.lab-fx-off #lab-parts,
body.lab-fx-off #lab-vig::after { display: none; }
/* the vignette itself STAYS - it is the room's edge, not an effect, and
   without it the chamber ends in a hard rectangle. */

/* signed out: the name box goes with the save row it belongs to */
body.lab-readonly .deck-name { display: none; }

.toast-host {
  position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
  z-index: 80; display: flex; flex-direction: column; gap: 6px; align-items: center; pointer-events: none;
}
.toast {
  padding: 10px 16px; border-radius: 0;
  clip-path: polygon(12px 0, 100% 0, 100% calc(100% - 12px), calc(100% - 12px) 100%, 0 100%, 0 12px);
  font-family: var(--av-sans); font-size: 12.5px;
  background: color-mix(in srgb, var(--av-panel) 96%, transparent); border: 1px solid var(--av-edge);
  color: var(--av-ink); box-shadow: 0 18px 50px color-mix(in srgb, var(--av-page-deep) 50%, transparent);
  min-width: 220px; text-align: center;
}
.toast.ok { border-color: var(--av-edge-lit); }
.toast.err { border-color: var(--av-bad); color: var(--av-bad-lt); }
.toast.warn { border-color: var(--av-amber-lt); color: var(--av-amber-lt); }

/* -------------------------------------------------------------------------
   9. SHOWCASE — the chrome-free stage.
   ------------------------------------------------------------------------- */
#lab-rail, .vp-specnav, .skin-cycler, .vp-tools, .vp-readout, .vp-autorotate,
.vp-rot, .vp-mesh-status, .stage-growth, .stage-anim, .vp-viewkeys {
  transition: opacity .38s, visibility 0s linear 0s;
}
body.showcase #lab-rail, body.showcase .vp-specnav, body.showcase .skin-cycler,
body.showcase .vp-tools, body.showcase .vp-readout, body.showcase .vp-autorotate,
body.showcase .vp-rot, body.showcase .vp-mesh-status,
body.showcase .stage-growth, body.showcase .stage-anim, body.showcase .vp-viewkeys {
  opacity: 0; visibility: hidden; pointer-events: none;
  transition: opacity .38s, visibility 0s linear .38s;
}

/* -------------------------------------------------------------------------
   10. THE COLUMNS — created here, at desktop widths only.
   ------------------------------------------------------------------------- */
@media (min-width: 901px) {
  body.studiolab { grid-template-columns: var(--av-species-w) minmax(0, 1fr) var(--av-rail-w); }
  #workspace { grid-column: 2; }
  /* while orbiting, nothing stands between the eye and the animal */
  body.lab-orbiting .vp-center-stack, body.lab-orbiting .stage-growth { opacity: .12; pointer-events: none; }
}

/* ══ SHORT SCREENS ══ <=900px tall: the seven colours STILL fit whole.
   A 1366x768 laptop is the width this list ran out of room on first, and the
   owner's ruling is that scrolling for the last two colours is the defect -
   so on a short screen the ROOM gets tighter rather than the list getting
   longer. What goes is chrome the chips already say: the "PATTERN" and
   "PATTERN SIZE" captions (A/B/C and SMALL/MEDIUM/LARGE name themselves, and
   the tooltip still carries the sentence), and a few pixels off every seat.
   Nothing that can be pressed is removed. */
@media (min-width: 901px) and (max-height: 900px) {
  .deck-row-l.deck-head { padding-top: 8px; padding-bottom: 8px; gap: 6px; }
  /* ★ THE CAPTIONS NO LONGER PAY THE HEIGHT (2026-08-25). Two lines used to
     be cut here - "PATTERN" and "PATTERN SIZE" - on the reasoning that
     A/B/C and SMALL/MEDIUM/LARGE name themselves. They do not: a third
     group (THEME: DEFAULT / SECONDARY) arrived, and an owner's
     1440x900 studio then showed three anonymous chip rows in a column with
     no words in it at all. In the two-track grid above a caption costs no
     line of its own, so there is nothing left to buy back. */
  .deck-pattern.deck-field { gap: 5px 10px; }
  .deck-pattern .stage-foot-k { font-size: 9px; letter-spacing: .12em; }
  #design-name { height: 30px; font-size: 14px; }
  .deck > .deck-row-l { padding-top: 9px; padding-bottom: 9px; }
  .channel { padding: 4px 10px; gap: 10px; }
  .channel input[type="color"] { height: 22px; }
  #channel-list { gap: 3px; }
  .deck-actions .btn { min-height: 30px; }
  .deck-actions .deck-apply { min-height: 36px; }
  /* ★★★★★ THE GLITCH CREATOR'S OWN STATUS LINES ARE NOT THE DECK'S CHROME
     (2026-08-26, measured at 1440x900 - `#gcr-note` computed `display: none`).
     This block budgets the height of the STUDIO DECK, and `.save-note` is the
     deck's resting explainer, which is fair to drop on a short screen. But
     `glitchcreator.js` gives its three `role="status"` lines the same class
     (`#gcr-note`, `#gcr-code-note`, `#gcr-shelf-note`) and those live in the
     CREATOR PANE, not in the deck - so a rule buying deck pixels was silently
     deleting the only place that pane says "saved", "that paste has no body
     colour", or names the press. A refusal nobody can read is the same bug as
     no refusal at all, and exempting them costs this block nothing because
     they were never taking up any of the height it is reclaiming.
     1440x900 is a max-height: 900px match, so this fired on the commonest
     laptop the fleet sees, on every owner using this theme. */
  .save-note:not(.gcr-note) { display: none; }
}

/* ...and one tier tighter again at 720p, the shortest desktop this fleet
   sees. Same rule, same reason: the list stays whole. */
@media (min-width: 901px) and (max-height: 780px) {
  .deck > .deck-row-l { padding-top: 7px; padding-bottom: 7px; }
  .channel { padding: 3px 10px; }
  .channel input[type="color"] { height: 20px; }
  .channel label { font-size: 10.5px; }
  #channel-list { gap: 2px; }
  /* ★ THE CODE BLOCK'S OWN 720p RULES LEFT THIS TIER (2026-08-18). The old
     one-line five-track grid here NEVER APPLIED - the base .deck-codes block
     sits LATER in this sheet, same selectors, and later declaration wins
     (headless-measured: the lead sat on its own line at 1366x760 all along,
     and the two fields were 61px slivers). The block's short-screen
     tightening now lives AFTER the base rules, at the sheet's foot, where it
     actually wins; the wrapping row itself takes the fewest lines the
     owner's own labels physically fit at every height. */
  .deck-actions .btn { min-height: 27px; font-size: 11px; }
}
/* ★ 720p AND NARROWER: the code block's controls wrap onto an extra line at
   this width, and the last of the pixels come out of the swatches - the same
   rule the tier above states. Measured after: every one of the seven colour
   rows fully visible, the swatch strip no longer scrolling, and the block
   still clear of the owner's live bar. */
@media (min-width: 901px) and (max-height: 820px) {
  /* .deck-codes' own tightening for this tier lives at the sheet's foot -
     declared here it was shadowed by the later base block (same selector,
     later declaration wins). */
  .deck-actions .deck-apply { min-height: 33px; }
  .deck > .deck-row-l { padding-top: 6px; padding-bottom: 6px; }
  #design-name { height: 28px; }
  #channel-list { gap: 1px; }
}

/* ══ TABLET ══ 1024-1279: the same three columns, tighter */
@media (min-width: 1024px) and (max-width: 1279px) {
  body.studiolab { --av-species-w: 206px; --av-rail-w: 300px; }
  .vp-specnav .spec-name { font-size: 26px; }
}

/* ══ SMALL LAPTOP ══ 901-1023: drop the species column; the list moves into
   the right column's own face, so nothing becomes unreachable. */
@media (min-width: 901px) and (max-width: 1023px) {
  body.studiolab { --av-species-w: 0px; --av-rail-w: 288px;
    grid-template-columns: minmax(0, 1fr) var(--av-rail-w); }
  #workspace { grid-column: 1; }
  #lab-col { grid-column: 2; }
  #species-rail, #species-rail[hidden] { display: none; }
  .vp-specnav .spec-name { font-size: 24px; }
  .vp-mesh-status, .vp-readout, .vp-autorotate { width: 180px; }
}

/* -------------------------------------------------------------------------
   11. PHONE / TABLET PORTRAIT — one column, the controls become a bottom
   sheet, and NOTHING text-bearing sits over the model band.
   ------------------------------------------------------------------------- */
@media (max-width: 900px) {
  body.studiolab { grid-template-columns: minmax(0, 1fr); }
  /* ★ THE WRAPPER STANDS DOWN ON A PHONE. `display: contents` takes #lab-col
     out of layout entirely, so the sheet, the drawer and the scrim behave as
     the direct grid/viewport children they were before it existed - and
     studiolab.js's `tabsHome` (the row's parentNode) still resolves to it, so
     the tab row goes home correctly when the window widens again. */
  #lab-col { display: contents; }
  #species-rail, #species-rail[hidden], #lab-tabs { display: none; }
  #workspace { grid-column: 1; }
  .lab-main {
    display: flex; flex-direction: column; min-height: 0;
    padding-bottom: var(--lab-dock-h, 0px);
  }
  .viewport-wrap { flex: 1 1 auto; min-height: 0; height: auto; border: 0; }
  .vp-readout, .vp-rot, .showcase-hint { display: none; }
  .vp-mesh-status { left: 12px; top: 12px; width: auto; max-width: calc(100% - 24px); }
  .stage-growth { top: 14px; }
  .vp-center-stack { top: 56px; }
  .vp-specnav .spec-name { font-size: clamp(19px, 5.4vw, 26px); }
  .vp-specnav .spec-status { font-size: 9px; letter-spacing: .2em; }

  /* the stage's own controls move UNDER the band */
  #lab-headstrip { flex: none; position: relative; z-index: 6; padding: 6px 8px 4px; display: flex; justify-content: center; }
  #lab-headstrip[hidden] { display: none; }
  #lab-headstrip .vp-center-stack {
    position: static; transform: none; top: auto; left: auto;
    display: flex; flex-direction: column; align-items: center; gap: 6px; max-width: 100%;
  }
  #lab-footstrip { flex: none; position: relative; z-index: 6; min-width: 0; padding: 2px 8px 4px; }
  #lab-footstrip .fs-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
  #lab-footstrip .stage-anim {
    position: static; max-width: none; flex: 1 1 auto; min-width: 0;
    overflow-x: auto; flex-wrap: nowrap; padding: 3px 0;
  }
  #lab-footstrip .stage-anim .btn, #lab-footstrip .stage-anim .chip { min-height: 38px; flex: none; }
  #lab-footstrip .vp-autorotate {
    position: static; transform: none; flex: none; width: auto;
    padding: 9px 12px; min-height: 38px;
  }
  body.deck-expanded #lab-footstrip { display: none; }
  body.deck-expanded .skin-cycler { display: none; }
  .stage-note { bottom: 12px; }

  /* the sheet */
  #lab-tabs { display: none; }
  /* ★ THE DOORS ON A PHONE (2026-08-22). The desktop column's tab row is
     hidden at this width and nothing else opened the panel - a phone had no
     Custom, no Glitch and no Saved at all. studiolab.js moves #lab-tabs into
     the sheet above the grab handle; this rule is what lets it show there. */
  #lab-dock-panel #lab-tabs {
    display: flex; position: static; min-height: 42px; height: auto; width: 100%; z-index: 1;
    background: transparent; border-left: 0; border-bottom: 1px solid var(--av-line-dim);
  }
  #lab-dock-panel #lab-tabs .lab-tab { font-size: 12.5px; padding: 11px 6px 9px; }
  /* the sheet sits ON the owner's fixed foot bar, never under it - the Apply
     key is the last thing that may end up behind a live strip. */
  #lab-dock-panel {
    position: absolute; left: 0; right: 0; bottom: var(--lab-bar, 0px); top: auto;
    grid-column: 1; height: auto; padding-top: 0; z-index: 33;
  }
  /* ★ THE SHEET IS CAPPED IN BOTH STATES. An uncapped column of colour rows
     is seven rows tall on a phone and leaves the animal a letterbox - the one
     thing this page must never do. Collapsed shows a few rows and scrolls;
     expanded gets more. The band keeps the rest either way. */
  .deck {
    /* collapsed is the state you LOOK at the animal in, so it takes the
       smaller share; the grab handle buys the rest when you are editing. */
    height: auto; max-height: 34dvh; overflow: hidden;
    border-left: 0; border-top: 1px solid var(--av-edge);
    background: linear-gradient(180deg, color-mix(in srgb, var(--av-sheet-hi) 98%, transparent), var(--av-sheet-lo));
    box-shadow: 0 -18px 40px -24px var(--av-shadow);
    padding-bottom: env(safe-area-inset-bottom);
  }
  #lab-dock-panel.expanded .deck { max-height: 62dvh; }
  .deck-row-l.deck-wells { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .dock-handle {
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 3px; width: 100%; height: 28px; padding: 0; border: 0; background: transparent; cursor: pointer;
  }
  .dock-handle span { width: 56px; height: 5px; border-radius: 3px; background: var(--av-ink-3); }
  .dock-handle::after {
    content: ""; width: 8px; height: 8px;
    border-left: 2px solid var(--av-ink-3); border-top: 2px solid var(--av-ink-3);
    transform: rotate(45deg) translateY(1px);
  }
  #lab-dock-panel.expanded .dock-handle::after { transform: rotate(225deg) translateY(1px); }
  /* thumb-first: the wells you paint with come FIRST */
  .deck { display: flex; flex-direction: column; }
  /* ★ THE WELLS ARE THE ONLY THING THAT SCROLLS. The sheet is capped, so if
     this row does not shrink it pushes the Apply key clean off the bottom of
     the sheet - measured at 492x932: actions at y=892 inside a deck ending at
     811. flex:1 1 auto + min-height:0 is what lets a flex child be smaller
     than its content. */
  .deck-row-l.deck-wells { order: 1; flex: 1 1 auto; min-height: 0; overflow-y: auto; }
  .deck-head { order: 2; }
  .deck-row-l.deck-actions { order: 3; }
  /* the skin-code block used to default to order 0 and sat ABOVE the colour
     wells in the collapsed sheet, leaving one colour row visible */
  .deck-row-l.deck-codes { order: 4; }
  #lab-dock-panel:not(.expanded) .deck-codes { display: none; }
  #lab-dock-panel .stage-growth {
    position: static; transform: none; order: 0; width: 100%; max-width: none;
    margin: 0; padding: 8px 16px 0; justify-content: center;
    background: transparent; border: 0;
  }
  #lab-dock-panel .stage-growth input[type="range"] { width: auto; flex: 1 1 auto; min-width: 0; }
  #lab-dock-panel:not(.expanded) .deck-head { display: none; }
  /* Same exemption as the short-screen block above, same reason: the collapsed
     dock hides the DECK's chrome, and the creator pane's status lines are not
     in the dock at all. */
  #lab-dock-panel:not(.expanded) .save-note:not(.gcr-note) { display: none; }
  #lab-dock-panel:not(.expanded) .deck-actions .dock-secondary { display: none; }
  .channel { padding: 8px 10px; }

  /* the tool rail becomes a sheet key over the deck */
  #lab-rail { left: auto; right: 10px; bottom: auto; top: 12px; justify-content: flex-end; padding: 0; }
  #lab-rail:not(.expanded) .lab-rail-stack { display: none; }
  .rail-fab {
    display: flex; align-items: center; justify-content: center;
    width: 44px; height: 44px; border-radius: 0;
    clip-path: polygon(9px 0, 100% 0, 100% calc(100% - 9px), calc(100% - 9px) 100%, 0 100%, 0 9px);
    border: 1px solid var(--av-edge); background: color-mix(in srgb, var(--av-panel) 94%, transparent);
    color: var(--av-ink); font-size: 16px; cursor: pointer; pointer-events: auto;
  }
  #lab-rail.expanded { z-index: 41; }
  #lab-rail.expanded .lab-rail-stack {
    position: fixed; left: 8px; right: 8px; width: auto;
    top: auto; bottom: calc(var(--lab-bar, 0px) + 8px + env(safe-area-inset-bottom));
    max-height: 60dvh; overflow-y: auto; flex-wrap: wrap; border-radius: 0;
    clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
    z-index: 41;
  }
  /* ★ THE SLIDERS ARE NO LONGER DELETED HERE (2026-09-07). They used to float
     at the foot of the stage, where a phone has no room, so this tier hid
     them - and a control that disappears at a width is a control nobody on a
     phone has. They live in the rail now, and the rail is already a sheet at
     this width, so they simply come with it; the two of them take the sheet's
     own full-width line rather than a 74px sliver of it. */
  /* they ride the foot strip with the clip keys; studiolab.js moves them */
  .vp-tools { display: flex; position: static; flex: 1 1 auto; min-width: 0; gap: 8px; }

  /* the library becomes its own bottom sheet, with a header and a close key */
  #lab-drawer-scrim {
    display: block; position: fixed; inset: 0; z-index: 32;
    background: color-mix(in srgb, var(--av-page-deep) 62%, transparent); opacity: 0; pointer-events: none; transition: opacity .24s;
  }
  #lab-drawer-scrim.open { opacity: 1; pointer-events: auto; }
  #panel {
    position: fixed; top: auto; bottom: var(--lab-bar, 0px); left: 0; right: 0;
    width: 100%; max-width: 100%; height: min(78vh, 600px); max-height: 78vh;
    transform: translateY(100%); display: block; z-index: 34;
    border-left: 0; border-top: 1px solid var(--av-edge);
    padding: 12px 16px calc(24px + env(safe-area-inset-bottom));
    transition: transform .24s cubic-bezier(.2, .9, .25, 1);
    box-shadow: 0 -18px 40px -24px var(--av-shadow);
  }
  body.drawer-open #panel { transform: translateY(0); }
  body.drawer-open #lab-dock-panel { visibility: visible; }
  .drawer-head {
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    position: sticky; top: 0; z-index: 2; margin: 0 -16px 10px; padding: 4px 16px 10px;
    background: linear-gradient(color-mix(in srgb, var(--av-panel-2) 99%, transparent), color-mix(in srgb, var(--av-panel) 97%, transparent));
    border-bottom: 1px solid var(--av-line-dim);
  }
  .drawer-head strong {
    display: block; font-family: var(--av-serif); font-weight: 400; font-size: 20px; color: var(--av-ink);
  }
  #drawer-close {
    min-width: 44px; min-height: 44px; border-radius: 0;
    border: 1px solid var(--av-edge); background: color-mix(in srgb, var(--av-jade-lt) 7%, transparent);
    color: var(--av-ink); font: 22px/1 var(--av-sans); cursor: pointer;
  }
  .glitch-grid { grid-template-columns: 1fr 1fr; }
}

@media (max-width: 640px) {
  .stage-growth { left: 12px; right: 12px; transform: none; max-width: none; justify-content: center; }
  .stage-growth .gr-label { display: none; }
  #lab-dock-panel .stage-growth .gr-label { display: inline; }
  .glitch-grid { grid-template-columns: 1fr; }
}

/* ══ TOUCH ══ real thumb targets, whatever the width */
@media (pointer: coarse) {
  .channel { min-height: 44px; }
  .deck-actions .btn, .chip, .rail-action, body.studiolab #panel button, .preset { min-height: 44px; }
  .vp-specnav .nav-arrow { width: 44px; height: 44px; }
  /* a 3px range track is a 3px target: pad the control, not the track */
  .stage-growth input[type="range"], .vp-rotate input[type="range"], .vp-exposure input[type="range"] {
    height: 44px; background: transparent;
    background-image: linear-gradient(color-mix(in srgb, var(--av-ink) 18%, transparent), color-mix(in srgb, var(--av-ink) 18%, transparent));
    background-size: 100% 3px; background-position: 0 50%; background-repeat: no-repeat;
  }
  .stage-growth { padding-top: 0; padding-bottom: 0; }
}

/* -------------------------------------------------------------------------
   12. REDUCED MOTION
   ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  body.studiolab *, body.studiolab *::before, body.studiolab *::after {
    animation-duration: .001ms !important; animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }
  .vp-loading { transition: none; }
  /* the motes exist to DRIFT; frozen they are specks on the glass, so the
     reader who asked for no motion gets the still chamber instead. */
  #lab-parts { display: none; }
}


/* ---- SKIN CODES, seated in the editor column (2026-08-17; reflowed
   2026-08-18) ----
   It sits under Random / Save / Apply: the presses and the fields share a
   line where the words leave room, and the note speaks underneath. The whole
   row is DISPLAY:NONE when the owner switched codes off, because the assembler
   removes the markup outright - there is no heading left behind to orphan.

   ★ THE CONTROLS ARE A WRAPPING ROW NOW, NOT GRID TRACKS. The old
   `auto minmax(0,1fr) minmax(0,1fr) auto` grid sized its two auto columns to
   the BUTTON LABELS - and the labels are the owner's own words
   (labels.studio_code*), not ours. On one owner the two code-button labels
   ate ~300px of the rail's 336, so both minmax(0,1fr) fields
   rendered as slivers a few pixels wide ON A 1920px BROWSER: the crush lives
   in the COLUMN, and the 720px phone tier that was meant to catch it keys on
   the VIEWPORT. A wrapping flex row is sized by the container itself -
   whatever fits a line shares it, whatever does not takes the next line
   whole - so the code box, the row's whole purpose, never drops below its
   floor at any label length in any language, and no media query has to
   guess the panel's width again. */
body.studiolab .deck-codes {
  display: block;
  margin-top: 5px;
  /* ★ AND IT NEVER SHRINKS. `.deck` is a flex COLUMN with overflow hidden,
     so a full panel squeezed this row - the last child, with no shrink guard -
     down toward zero height and then clipped what was left. That is why it
     read as half-buried rather than merely low. */
  flex: 0 0 auto;
  /* ★ AND IT LOOKS LIKE SOMETHING. The owner asked for it to read as loudly
     as it does. A strip of controls at the foot of a long column reads as chrome
     even when it is fully on screen, so the block gets its own lit edge, its
     own ground and its own name. */
  padding: 8px 10px 9px;
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 34%, transparent);
  border-left: 3px solid var(--av-jade);
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-jade) 7%, transparent), color-mix(in srgb, var(--av-sink) 55%, transparent));
}

body.studiolab .deck-codes .code-row {
  display: flex; flex-wrap: wrap;
  gap: 4px 8px; align-items: center;
  /* the wrapper's padding is the spacing here; .code-row's own-seat rule
     above would add 4px more on top of it */
  margin-top: 0;
}
body.studiolab .deck-codes #code-input,
body.studiolab .deck-codes #code-name {
  min-width: 0;
  padding: 5px 8px; border-radius: 0;
  font-family: var(--av-sans); font-size: 11.5px;
  background: color-mix(in srgb, var(--av-sink) 85%, transparent);
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 20%, transparent);
  color: var(--av-ink); outline: 0;
}
/* The code box is the row's purpose - a player pastes a code INTO it - so it
   holds a real floor (~a code's first 20 characters) and takes first claim on
   a line's spare width. Wrapping means the floor is honoured by taking a
   whole line, never by crushing a neighbour. The name is one short word and
   gives way first. */
body.studiolab .deck-codes #code-input { flex: 2 1 170px; min-width: min(170px, 100%); }
body.studiolab .deck-codes #code-name { flex: 1 1 130px; }
body.studiolab .deck-codes #code-input:focus,
body.studiolab .deck-codes #code-name:focus {
  border-color: var(--av-jade);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--av-jade) 40%, transparent);
}
body.studiolab .deck-codes #code-note {
  flex: 0 0 100%; margin: 0;
}
/* No phone tier any more: the wrap above is container-driven, so a narrow
   deck - phone, tablet rail, or a future column width - lays the same row
   out on more lines by itself. */

/* ---- the skin-code block's own name, and its two presses ---- */
body.studiolab .deck-codes .code-lead {
  flex: 0 0 100%;
  font-family: var(--av-sans);
  font-size: 10.5px; font-weight: 700;
  letter-spacing: .16em; text-transform: uppercase;
  color: var(--av-jade);
  margin: 0; line-height: 1.15;
}
/* Copy and Import are the two things a player comes here to press, so they
   read as presses rather than as ghosts among the fields. */
body.studiolab .deck-codes #copy-code,
body.studiolab .deck-codes #import-code {
  border-color: var(--av-jade);
  color: var(--av-ink);
  background: color-mix(in srgb, var(--av-jade) 16%, transparent);
  font-weight: 600;
  white-space: nowrap;
}
body.studiolab .deck-codes #copy-code:hover,
body.studiolab .deck-codes #import-code:hover {
  background: color-mix(in srgb, var(--av-jade) 28%, transparent);
}
/* An owner who switched codes off has the row REMOVED by the assembler; without
   this the wrapper would keep drawing its border and its ground around
   nothing. */
body.studiolab .deck-codes:empty { display: none; }
/* ...and the OTHER way the row goes: signed out, studio.mjs hides #code-row
   (hidden + display:none, same both-ways idiom as the staff toggle). Same
   promise as the :empty guard - no lit edge around nothing. Scoped with
   :has() like site.css's nav rules; a browser without it simply keeps
   today's empty strip. */
body.studiolab .deck-codes:has(> #code-row[hidden]) { display: none; }

/* Short desktops (the 720p family): the block gives a few pixels back -
   the same trade the swatch tiers above make. Declared HERE, after the base
   rules, because the base block sits this late in the sheet and same-selector
   precedence is file order: written inside the height tiers above (where the
   old grid's one-line variant also sat) these were silently shadowed and
   never applied - "measured as applied while doing absolutely nothing", the
   exact trap the .deck padding comment names. */
@media (min-width: 901px) and (max-height: 820px) {
  body.studiolab .deck-codes { padding: 5px 8px 6px; }
  body.studiolab .deck-codes .code-row { gap: 3px 6px; }
}

/* 2026-08-20 r2 (this owner): the top rail (#lab-tabs) is the one
   switcher; this inner twin duplicated it visually. It stays in the DOM
   (the rail clicks it and studio.mjs reads it by id) and hides only
   where the rail exists - below 901px the rail itself is hidden. */
@media (min-width: 901px) { body.studiolab #studio-tabs { display: none; } }

/* =========================================================================
   2026-08-25 BREATHING-ROOM PASS (an owner reported the studio too compact,
   every control too close to the next). Appended
   overrides only - `body.studiolab`-scoped so every rule outranks its base
   by specificity, no !important (the dropped-declaration law). Desktop and
   up; the phone sheet below 901px keeps the economics the 08-23 mobile
   pass settled.
   ========================================================================= */
@media (min-width: 901px) {
  /* the colour rows: taller rows, real air between them, a bigger dial */
  body.studiolab #channel-list { gap: 7px; }
  body.studiolab .channel { padding: 8px 12px; gap: 14px; }
  body.studiolab .channel input[type="color"] { width: 40px; height: 28px; }
  body.studiolab .channel label { font-size: 12px; letter-spacing: .09em; }
  body.studiolab .channel .hex { font-size: 11px; }
  body.studiolab .channel-preview-note { margin: 5px 0 0; }
  /* the deck's sections: wider seats, clearer separation */
  body.studiolab .deck > .deck-row-l { padding: 15px 18px; }
  body.studiolab .deck-row-l { gap: 12px; }
  body.studiolab .deck-field { gap: 8px; }
  body.studiolab .deck-row-l.deck-head { padding-top: 13px; padding-bottom: 11px; gap: 10px; }
  body.studiolab #design-name { height: 36px; font-size: 15px; padding: 6px 12px; }
  body.studiolab .chips { gap: 8px; }
  body.studiolab .chip { padding: 7px 12px; font-size: 12px; min-width: 40px; }
  body.studiolab .micro-label { font-size: 10.5px; }
  /* the library face */
  body.studiolab .deck-search { padding: 12px 14px 0; }
  body.studiolab .deck-list { padding: 12px 14px 18px; gap: 7px; }
  body.studiolab .deck-row { padding: 11px 13px; }
  body.studiolab .deck-row span { font-size: 10px; }
  /* the actions foot: the apply is the page's one commitment - let it read
     like one, and give the row pair room to be two buttons, not one blur */
  body.studiolab .deck-row-l.deck-actions { padding-top: 12px; padding-bottom: 12px; gap: 8px; }
  /* the staff switch is a SENTENCE, not a half-slot: its own full row -
     at half width it clipped mid-word the moment the gap widened */
  body.studiolab .deck-actions #apply-admin-toggle { flex: 1 1 100%; }
  body.studiolab .deck-actions .btn { flex-basis: calc(50% - 4px); min-height: 38px; padding: 8px 12px; font-size: 12px; }
  body.studiolab .deck-actions .deck-apply { min-height: 44px; padding: 11px 16px; font-size: 13.5px; letter-spacing: .05em; }
  body.studiolab .save-note { font-size: 10.5px; line-height: 1.5; }
}

/* the SHORT-DESKTOP tier of the same pass: a 900-class window must still show
   a working stack of colour rows, so the air here is measured, not maximal -
   same shape as the sheet's older height tiers. (The sticky swatch strip this
   tier was originally measured against was struck 2026-08-25; the rows own
   that height now.) */
@media (min-width: 901px) and (max-height: 960px) {
  body.studiolab #channel-list { gap: 5px; }
  body.studiolab .channel { padding: 6px 11px; }
  body.studiolab .channel input[type="color"] { width: 38px; height: 26px; }
  body.studiolab .deck > .deck-row-l { padding: 12px 16px; }
  body.studiolab .deck-row-l.deck-head { padding-top: 11px; padding-bottom: 9px; }
  body.studiolab .deck-row-l.deck-actions { padding-top: 10px; padding-bottom: 10px; }
  body.studiolab .deck-actions .deck-apply { min-height: 40px; padding: 9px 14px; }
  body.studiolab #design-name { height: 33px; }
}

/* =========================================================================
   2026-08-25 ONE SCROLL REGION (an owner, his own screenshot of the
   rail: a HALF-CLIPPED "BODY" row with a scrollbar thumb jammed against the
   button tower - the colour rows, the point of the page, showing ONE of
   seven).

   THE DEFECT WAS TWO NESTED SCROLLERS SHARING ONE BUDGET. The wells had
   `flex: 1 1 auto; overflow-y: auto` inside a `.deck` that also scrolled, so
   on any column shorter than its content the wells was the only child
   allowed to give and it gave all the way to a single row - a scrollbar
   inside a scrollbar, and the page's whole subject behind it. The 08-25
   vertical-budget pass answered that with a 258px floor on the wells, which
   was right on a desktop and WRONG everywhere else: it carried no width
   guard, so a 617px phone sheet inherited a 258px child and the sheet became
   a third scroller.

   THE BUDGET, RESTATED ONCE AND FOR ALL:
     · the DECK is the ONE scroller, at every width;
     · the wells is content-height (`flex: 0 1 auto`) with NO inner scroll,
       so seven rows are seven rows and ten are ten - never one;
     · the head sticks to the top and the actions stick to the foot, so the
       name you are editing and the key that commits it are both in reach
       from anywhere in that one scroll;
     · nothing is starved: when the column runs out of room the LAST thing
       (the skin-code block) goes below the fold, which is what a fold is for.
   ========================================================================= */
body.studiolab .deck {
  overflow-y: auto;
  /* content-first: the column ends where its controls end, and the ground
     below it is the panel's own. The old `flex: 1 1 auto` wells absorbed
     that ground instead, which put a ~300px HOLE between the last colour
     and the action bar on a signed-out 1440 rail. */
  justify-content: flex-start;
}
/* ★ `flex: 0 0 auto`, AND THE `min-height: 0` GOES. Those two together were
   the whole crush: `min-height: 0` is what lets a flex child be SMALLER than
   its own content, and shrink:1 is what made it the only child that paid.
   A flex item at its default `min-height: auto` cannot be crushed below its
   content, so seven rows measure seven rows and the DECK overflows instead -
   which is the one scroller this column is allowed. */
body.studiolab .deck-row-l.deck-wells {
  flex: 0 0 auto; min-height: auto; overflow: visible;
}
body.studiolab .deck > .deck-row-l { flex: 0 0 auto; }
body.studiolab .deck-row-l.deck-head {
  position: sticky; top: 0; z-index: 3;
  background: linear-gradient(180deg, var(--av-deck-top) 0%, color-mix(in srgb, var(--av-panel) 97%, transparent) 100%);
}
body.studiolab .deck-row-l.deck-actions {
  position: sticky; bottom: 0; z-index: 3;
  background: linear-gradient(180deg, color-mix(in srgb, var(--av-sink) 92%, transparent), color-mix(in srgb, var(--av-sink) 99%, transparent));
  -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
}
body.studiolab .deck {
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--av-jade-lt) 35%, transparent) transparent;
}
body.studiolab .deck::-webkit-scrollbar { width: 7px; }
body.studiolab .deck::-webkit-scrollbar-track { background: transparent; }
body.studiolab .deck::-webkit-scrollbar-thumb {
  background: color-mix(in srgb, var(--av-jade-lt) 32%, transparent); border-radius: 4px;
}
body.studiolab .deck::-webkit-scrollbar-thumb:hover {
  background: color-mix(in srgb, var(--av-jade-lt) 50%, transparent);
}

/* =========================================================================
   2026-08-25 THE ACTION FOOT READS AS A HIERARCHY (same screenshot: four
   full-width slabs of near-identical weight - RANDOMISE | SAVE DESIGN |
   APPLY IN GAME | "IN-GAME APPLY: ON - SWITCH" - four different jobs and no
   way to tell which one changes the animal).

   APPLY IN GAME is the only control here that reaches the island. It is the
   only FILLED key, it is the only full-width one, and it is the tallest. The
   three that only touch the page recede to a compact row. The staff switch
   is a SETTING, not an action, so it is a quiet line with a state pip rather
   than a fourth slab - and it says what it is on its own line, in words.
   (Brand-vs-danger by SHAPE, FILL and WORDS: on an owner whose accent IS red
   no pair of reds is more than ~1.5:1 apart, so hue can never carry it.)
   ========================================================================= */
@media (min-width: 901px) {
  body.studiolab .deck-row-l.deck-actions { gap: 7px; }
  /* the three page-side keys share one compact row */
  body.studiolab .deck-actions .dock-secondary {
    flex: 1 1 calc(33.333% - 5px); min-height: 34px; padding: 7px 6px;
    font-size: 11px; letter-spacing: .03em;
  }
  /* ...and the one that commits owns a line, a fill and the height */
  body.studiolab .deck-actions .deck-apply {
    flex: 1 1 100%; order: 9; min-height: 46px; padding: 12px 16px;
    font-size: 13.5px; letter-spacing: .06em; font-weight: 600;
  }
  /* the staff switch: a setting, last, quiet, and never mistaken for Apply */
  body.studiolab .deck-actions #apply-admin-toggle {
    flex: 1 1 100%; order: 10; min-height: 28px; padding: 5px 9px;
    font-size: 10px; letter-spacing: .1em;
    background: transparent; border-color: var(--av-line-dim);
    color: var(--av-ink-3);
  }
  body.studiolab .deck-actions #apply-admin-toggle:hover {
    color: var(--av-ink-2); border-color: var(--av-line); background: transparent;
  }
  body.studiolab .save-note { order: 11; }
}

/* =========================================================================
   2026-08-25 SIGNED OUT, THE ROOM IS QUIET.

   `body.lab-readonly` is already published by studiolab.js (mirrored off
   #save-row[hidden], which studio.mjs sets for exactly one reason: nobody is
   signed in). Only ONE rule read it. Everything else in the chamber stayed
   lit BEHIND the sign-in card - and on a phone the specimen navigator and
   the clip strip are hoisted OUT of the stage by studiolab.js, so the gate
   could not cover them at all: his 390px capture shows a live "< Pick a
   species >" carousel above the card and a lone AUTO-ROTATE pill floating on
   a black band below it. An instrument that cannot do anything yet is
   clutter, so the room goes dark and the card stands alone.
   ========================================================================= */
body.lab-readonly #lab-headstrip,
body.lab-readonly #lab-footstrip,
body.lab-readonly .vp-readout,
body.lab-readonly .vp-autorotate,
body.lab-readonly .vp-tools,
body.lab-readonly .vp-viewkeys,
body.lab-readonly .vp-rot,
body.lab-readonly .skin-cycler,
body.lab-readonly .stage-growth,
body.lab-readonly #lab-rail,
body.lab-readonly .showcase-hint { display: none; }
/* the nameplate stays - it is the page's visible heading - but it steps back
   behind the card rather than competing with it */
body.lab-readonly .vp-center-stack { opacity: .35; pointer-events: none; }
/* and the card itself gets a real ground, not just a blur: a 5px blur over a
   lit chamber leaves every edge underneath legible */
body.studiolab .stage-gate {
  background:
    radial-gradient(120% 90% at 50% 40%, color-mix(in srgb, var(--av-page-deep) 82%, transparent), color-mix(in srgb, var(--av-page-deep) 95%, transparent) 70%),
    linear-gradient(color-mix(in srgb, var(--av-page-deep) 90%, transparent), color-mix(in srgb, var(--av-page-deep) 96%, transparent));
  -webkit-backdrop-filter: blur(14px) saturate(85%);
  backdrop-filter: blur(14px) saturate(85%);
}

/* ★★★★★ UNDER A SCENE THE GATE IS A SCRIM, NOT A LID (2026-09-07, live).
   The rule above paints 82-96% of the page ground across the WHOLE stage so a
   5px blur cannot leave the lit chamber legible underneath. That was right when
   the ground behind it was a rendered room. It is wrong the moment the owner
   types `theme.art_scenes.studio`: the stage is then the owner's PHOTOGRAPH,
   and a signed-out visitor - which is every first visitor - got a black
   rectangle where the background was meant to be. Measured on the live site
   the day the redesign shipped: --lab-scene resolved, the .webp answered 200,
   ::after was at opacity 1, and #stage-gate sat over all 766x780 of it at z=9.

   So under a scene the full-bleed ground becomes the site's own scrim - clear
   at the top, deep at the foot, the same register every other scene page uses -
   and the WORDS get their own plate instead, sized to the copy rather than to
   the stage. Contrast is carried where the text is, not by hiding the picture.
   With no scene typed nothing here applies and the lid above still rules. */
body.studiolab.lab-scene .stage-gate {
  background: linear-gradient(180deg,
    color-mix(in srgb, var(--av-page-deep) 10%, transparent) 0%,
    color-mix(in srgb, var(--av-page-deep) 34%, transparent) 38%,
    color-mix(in srgb, var(--av-page-deep) 62%, transparent) 72%,
    color-mix(in srgb, var(--av-page-deep) 82%, transparent) 100%);
  -webkit-backdrop-filter: blur(2px) saturate(96%);
  backdrop-filter: blur(2px) saturate(96%);
}
/* the plate the copy sits on: a soft radial that fades out well inside the
   stage, so the photograph is never squared off by a visible box edge */
body.studiolab.lab-scene .stage-gate::before {
  content: ""; position: absolute; z-index: 0;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(52ch, 88%); height: min(340px, 66%);
  pointer-events: none;
  background: radial-gradient(115% 100% at 50% 48%,
    color-mix(in srgb, var(--av-page-deep) 88%, transparent) 0%,
    color-mix(in srgb, var(--av-page-deep) 64%, transparent) 46%,
    transparent 74%);
}
body.studiolab.lab-scene .stage-gate > * { position: relative; z-index: 1; }
/* the nameplate behind the card stays out of the picture's way */
body.studiolab.lab-scene.lab-readonly .vp-center-stack { opacity: .22; }

/* =========================================================================
   2026-08-25 ONE RADIUS LANGUAGE, ONE NUMERAL FACE.

   His list: the name field, the chip rows and the channel list each carried
   a different corner radius, border and inset colour - three
   hand-written notch polygons and three ground colours. Every seat now cuts
   its corner from the SAME token pair, and every field sits on the same
   ground; an owner re-points the pair once. And the numerals - the hex
   value, the skin code, the typed RGBA channels - take the mono face
   instead of the prose one, which is what tells 2 from Z and 5 from S.
   ========================================================================= */
body.studiolab .deck-row,
body.studiolab .channel { clip-path: var(--av-clip-seat); }
body.studiolab .btn,
body.studiolab .rail-fab { clip-path: var(--av-clip-key); }
body.studiolab #design-name,
body.studiolab #deck-filter,
body.studiolab .deck-codes #code-input,
body.studiolab .deck-codes #code-name,
body.studiolab .gc-num,
body.studiolab #gc-seed,
body.studiolab #gc-import-text { background: color-mix(in srgb, var(--av-sink) 85%, transparent); }
body.studiolab .channel .hex,
body.studiolab .deck-codes #code-input,
body.studiolab .gc-num,
body.studiolab #gc-seed,
body.studiolab #gc-import-text,
body.studiolab #growth-pct,
body.studiolab .gr-readout,
body.studiolab .vp-readout .v { font-family: var(--av-mono); }

/* =========================================================================
   2026-08-25 THE SKIN-CODE BLOCK HOLDS ITS OWN FIELD.

   His screenshot: "Paste a skin code or desig" cut off at the card's right
   edge, with COPY CODE and IMPORT as outlined boxes that line up with
   nothing. The block was one wrapping row of six controls sharing the rail's
   312px, so the box a player actually pastes into was whatever was left
   after two owner-worded buttons.

   It is two halves now, and the halves say which way the code is going:
   OUT on the first line (Copy code / Copy JSON), IN on the second (the paste
   field, full width, with Import beside it). Still ONE flat row - the assembler's
   `strip_studio_code_row` opt-out matches this block up to its FIRST close
   tag, so a nested div would leave half the row on the page.
   ========================================================================= */
/* ★ `min-width: auto`, NOT 0 - these two carry the OWNER's words
   (one owner's code-button label runs long), and a half-track sized in percent
   shaved the first one. Same cure as the action foot: the content minimum
   is the label, and a key that will not fit takes the next line. */
body.studiolab .deck-codes #copy-code,
body.studiolab .deck-codes #copy-json { flex: 1 1 auto; min-width: auto; }
body.studiolab .deck-codes #code-input {
  flex: 1 1 100%; min-width: 0; width: 100%;
}
body.studiolab .deck-codes #code-name { flex: 1 1 96px; min-width: 0; }
body.studiolab .deck-codes #import-code { flex: 0 0 auto; }

/* =========================================================================
   ★★★★★ THE OWNER TONE BLOCK — how a site theme claims the lab.

   THE DEFAULT DOES NOT MOVE. The Specimen Lab is the donor theme's chamber,
   1:1, on an owner's 2026-08-17 order for that chamber reproduced exactly
   rather than referenced, and every owner who says nothing still gets
   exactly that - jade, amber, Fraunces, the lot. test_house_look.py's
   PAGE_THEME_SHEETS note records that ruling and it stands.

   WHAT MOVED IS THAT AN OWNER MAY NOW SAY OTHERWISE. Another owner,
   2026-08-25, asked for his studio to stop looking wrong on his own site. His
   site is `rift` - ember red on near-black, Anton and JetBrains Mono - and
   his studio was a warm jade-and-amber donor lab bolted inside it, four
   typefaces and two accent families on one screen. That is not a polish
   problem; the room was somebody else's.

   HOW TO JOIN: one block, tokens only, keyed on your site theme's own
   attribute (it is on <html>, see test_site_theme.py). Nothing else in this
   sheet changes and no sibling can be reached from here. The values are
   LITERALS, not `var(--accent)`, because test_studio_theme.py refuses a
   property this sheet reads and does not define - and a literal that could
   drift from owner.json is worth nothing, so the literals are pinned to that
   file by the lab tone test.
   ========================================================================= */
html[data-site-theme="rift"] body.studiolab {
  /* ONE DISPLAY FACE. The site's own, so the specimen's nameplate, the
     doors and the name field speak in the voice the logo already set -
     Fraunces was a fourth typeface on a page that had three. */
  --av-serif: 'Anton', 'Arial Narrow', 'Franklin Gothic Medium', Impact, sans-serif;
  /* ...AND A REAL MONO FOR THE NUMBERS. The hex value, the skin code and the
     typed RGBA channels are read character by character. */
  --av-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

  --av-ink: #f5edec;
  --av-ink-rgb: 245, 237, 236;

  --av-page: #050203;
  --av-page-rgb: 11, 4, 6;
  --av-page-deep-rgb: 5, 2, 3;
  --av-panel: #191214;
  --av-panel-rgb: 25, 18, 20;
  --av-panel-2: #221b1d;
  --av-panel-2-rgb: 34, 27, 29;
  --av-panel-3-rgb: 44, 37, 38;
  --av-sink-rgb: 8, 3, 5;
  --av-well-rgb: 8, 3, 5;
  --av-field-rgb: 25, 18, 20;
  --av-rail-hi-rgb: 34, 27, 29;
  --av-rail-lo-rgb: 8, 3, 5;
  --av-sheet-hi-rgb: 25, 18, 20;
  --av-sheet-lo-rgb: 5, 2, 3;
  --av-haze-rgb: 11, 4, 6;
  --av-chamber-top: #140d0f;
  --av-chamber-bot: #050203;
  --av-deck-top: #221b1d;
  --av-deck-bot: #0b0406;
  --av-shaft-rgb: 255, 237, 236;

  /* ★ THE BRIDGE (2026-09-07). This sheet reads the PLAIN names now - every
     `rgba(var(--av-x-rgb), .n)` became `color-mix(in srgb, var(--av-x) n%,
     transparent)` when the base block was re-pointed at the site's own role
     tokens. The eleven surfaces below never had a plain name to re-point, so
     each is bridged off the triple this block already pins to owner.json:
     same colour, same gate, one form for the sheet to read. */
  --av-page-deep: rgb(var(--av-page-deep-rgb));
  --av-panel-3: rgb(var(--av-panel-3-rgb));
  --av-sink: rgb(var(--av-sink-rgb));
  --av-well: rgb(var(--av-well-rgb));
  --av-field: rgb(var(--av-field-rgb));
  --av-rail-hi: rgb(var(--av-rail-hi-rgb));
  --av-rail-lo: rgb(var(--av-rail-lo-rgb));
  --av-sheet-hi: rgb(var(--av-sheet-hi-rgb));
  --av-sheet-lo: rgb(var(--av-sheet-lo-rgb));
  --av-haze: rgb(var(--av-haze-rgb));
  --av-shaft: rgb(var(--av-shaft-rgb));
  --av-shadow: color-mix(in srgb, rgb(var(--av-page-deep-rgb)) 82%, transparent);

  --av-line: #484142;
  --av-line-dim: #352e2f;

  /* ★★★★★ THE AFFIRMATIVE FAMILY IS NOT A SECOND RED, AND THAT IS ARITHMETIC.
     This owner's brand IS red (#e4222c / #ff6552). Any two reds that both
     clear the 3:1 status floor on near-black land within ~1.5:1 of each
     other, so a "selected" red beside a "commit" red is one colour to the
     eye and no colour at all to a red-green reader. The jade family
     therefore becomes the site's BONE: a selected chip is a filled bone
     plate with near-black ink, which separates by LIGHTNESS (about 14x the
     ground) and survives any colour vision. Red is spent on exactly one
     thing - the key that reaches the island. */
  --av-jade: #9c9495;
  --av-jade-rgb: 156, 148, 149;
  --av-jade-lt: #f5edec;
  --av-jade-lt-rgb: 245, 237, 236;
  --av-jade-hot: #ffffff;
  --av-on-jade: #050203;
  --av-btn-ink: #c2abac;

  /* the amber family is this site's EMBER: the tool rail, the focus ring */
  --av-amber: #e4222c;
  --av-amber-rgb: 228, 34, 44;
  --av-amber-lt: #ff6552;
  --av-amber-lt-rgb: 255, 101, 82;
  --av-focus: #ff6552;

  --av-bad: #ff9c86;
  --av-bad-rgb: 255, 156, 134;
  --av-bad-lt: #ffc9bb;

  /* THE NOTCH TURNS THE OTHER WAY. rift cuts the TOP-RIGHT of every seat
     (rift.css sections 11-12); the donor cuts top-left and bottom-right. Two
     chamfer languages on one screen is what made the Apply key look like it
     was poking out of the panel. */
  --av-clip-seat: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
  --av-clip-key: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
}
/* THE ONE RULE THE TOKENS CANNOT CARRY. The donor tints its Save key amber to
   mark it as the write; here amber IS the brand ember, so an ember-edged
   Save reads as a second commit beside Apply. Quiet it, and let the fill do
   the telling. */
html[data-site-theme="rift"] body.studiolab .btn.save {
  border-color: var(--av-line); color: var(--av-btn-ink);
}

/* =========================================================================
   2026-08-25 THE CHIP COLUMN FITS ITS CHIPS.

   Measured on a sibling owner at 1440x900 with the two-track grid in: the
   chips column is 220px and SMALL/MEDIUM/LARGE wanted 226 - so LARGE took a
   second line and the head grew 44px, which is what pushed the seventh
   colour off a 900-tall window. terminal.css uppercases every `.chip`, so
   the words are wider than they read in the sheet. The caption column is
   pinned narrow (and may wrap - two short lines of 10px still measure less
   than one chip) and the chips give back the padding they did not need.
   ========================================================================= */
body.studiolab .deck-pattern.deck-field {
  /* ★ display AND gap restated here, not only in the in-place rule above.
     A SPLICE onto a box whose sheet predates that rule takes the appended
     blocks only, and `grid-template-columns` on a FLEX container is inert:
     measured on the box's own bytes, every caption and every chip row then
     took a line of its own (five lines where three were designed) and the
     column grew 40px. Same declarations, no behaviour of its own. */
  display: grid; align-items: center; gap: 7px 12px;
  grid-template-columns: minmax(0, 72px) minmax(0, 1fr);
}
body.studiolab .deck-pattern .stage-foot-k {
  white-space: normal; line-height: 1.25;
  font-size: 9.5px; letter-spacing: .1em;
}
body.studiolab .deck-pattern .chips { gap: 6px; }
body.studiolab .deck-pattern .chip { padding: 7px 9px; min-width: 34px; }

/* =========================================================================
   2026-08-25 THE PHONE SHEET NO LONGER RESERVES THE OWNER'S BAR TWICE.

   MEASURED, not reasoned (390x844, a sibling owner signed in): the sheet
   runs 456..815, `.lab-main` ends at 788 and reserved 388px - leaving a
   56px band of nothing between the specimen and the sheet, which reads as
   the stage having quit early. `--lab-dock-h` is published as
   `dock.height + --lab-bar` (studiolab.js::publish) and `body.has-livebar`
   ALREADY holds `--livebar-h + 12` off the bottom of the grid, so the bar is
   counted in both. The exact reservation is the published height MINUS
   whatever the body already holds back; with no live bar the body holds back
   nothing and the published value is right as it stands, which is why this
   is one scoped rule and not a change to the script's arithmetic.
   ========================================================================= */
@media (max-width: 900px) {
  body.studiolab.has-livebar .lab-main {
    padding-bottom: calc(var(--lab-dock-h, 0px) - var(--livebar-h, 44px) - 12px);
  }
}

/* THE SECOND RULE THE TOKENS CANNOT CARRY (see THE OWNER TONE BLOCK above).
   This owner's display face is Anton - right for a nameplate, a door and a
   panel title, wrong inside a text field, where a 15px condensed poster face
   reads as a heading somebody typed into. The field takes the UI face; every
   other seat of --av-serif is a heading and keeps it. */
html[data-site-theme="rift"] body.studiolab #design-name {
  font-family: var(--av-sans); font-weight: 500; letter-spacing: .01em;
}

/* =========================================================================
   2026-08-25 THE PHONE CLIP ROW WRAPS INSTEAD OF CLIPPING.

   Measured at 346x617: IDLE / WALK / RUN and a 6px SLIVER of ATTACK. The
   strip was `flex-wrap: nowrap; overflow-x: auto` - a clipped scroll
   container, which is the one shape that fails with no error and no hint:
   a thumb can swipe it, a mouse cannot reach the far half by any gesture,
   and a WIDER window shows LESS whenever something above it grows. The
   desktop seat of this same row already wraps (`.stage-anim`, section 3);
   the phone seat now does too, and the band above yields the line.
   ========================================================================= */
@media (max-width: 900px) {
  #lab-footstrip .stage-anim {
    flex-wrap: wrap; overflow-x: visible; row-gap: 6px;
  }
  #lab-footstrip .fs-row { flex-wrap: wrap; row-gap: 6px; }
  /* ...and the sheet gets a little more of a SHORT screen. At 617px tall the
     34dvh cap left the colour rows two of seven behind a growth pill; the
     animal still keeps the larger share and the grab handle still buys the
     rest. Capped in px as well as dvh so a tall phone does not hand the
     sheet half the window. */
  .deck { max-height: min(38dvh, 320px); }
}

/* =========================================================================
   2026-08-25 A SECONDARY KEY NEVER LOSES LETTERS.

   Caught on a SIBLING while proving this wave harmless: another owner's own
   words for the same three keys run longer than ours, and a fixed one-third
   track cut the first of them mid-word.
   Same defect as the tab row, one row lower, and introduced by the
   fix above - the labels are the OWNER's (labels.studio_*), never ours, so
   any track sized in percent is sized against words we have not seen.

   `min-width: auto` is the whole cure: on a flex item that resolves to the
   content minimum, and `.btn` is `white-space: nowrap`, so the content
   minimum IS the label. A key that will not fit beside its neighbours takes
   the next line whole instead of being shaved - the rule `.design-row` in
   the classic sheet already learned in 2026-08 ("a control that loses
   letters is worse than one that moves"). The base `.deck-actions .btn`
   sets `min-width: 0`, which is what let it shave.
   ========================================================================= */
body.studiolab .deck-actions .btn { min-width: auto; }
@media (min-width: 901px) {
  body.studiolab .deck-actions .dock-secondary {
    flex: 1 1 auto; min-width: auto;
  }
}

/* =========================================================================
   2026-08-25 THE DRAWER'S INNER TAB ROW WRAPS TOO.

   `#studio-tabs` is hidden on a desktop (the column's own row is the one
   switcher) and SHOWN on a phone, inside the library sheet - and it carried
   the identical defect the top row did: `flex: 1` is `flex-basis: 0`, four
   equal tracks, longest label cut. Measured at 390: two-word tab labels on
   one owner and two more on another owner, the longest cut on both.
   Same cure, same reason: content basis, and the row wraps
   rather than shaving a word nobody can scroll to.
   ========================================================================= */
body.studiolab .studio-tabs { flex-wrap: wrap; }
body.studiolab .studio-tabs [role="tab"] {
  flex: 1 1 auto; min-width: auto; white-space: nowrap;
}

/* =========================================================================
   2026-08-25 A CAPTION WITH NOTHING UNDER IT IS A MISSING CONTROL.

   Signed out there is no species, so studio.mjs leaves `#pattern-chips`
   empty - and now that every group carries its name, the word PATTERN sat in
   the rail pointing at a blank cell. A label over nothing reads as a control
   that failed to load, which is worse than no label at all.
   `:has(+ .chips:empty)` takes the pair away together; a browser without
   `:has()` simply keeps today's lone caption, which is where this started.
   ========================================================================= */
body.studiolab .deck-pattern .chips:empty { display: none; }
body.studiolab .deck-pattern .stage-foot-k:has(+ .chips:empty) { display: none; }

/* =========================================================================
   2026-08-25 THE CAPTION/CHIPS PAIRING, RESTATED AT THE SHEET'S FOOT.

   The two-track grid above is declared in place, on `.deck-pattern.deck-field`
   — which is right for this tree and WRONG for a splice: a box whose sheet
   predates that rule takes the appended blocks from here to the end of the
   file and would get the grid without its column assignments, so auto-flow put
   every caption and every chip row on a line of its own (five lines where
   three were measured). Restated here, the pairing travels with the rest of
   the wave. Same declarations, higher specificity, no behaviour of its own.
   ========================================================================= */
body.studiolab .deck-pattern .stage-foot-k,
body.studiolab .deck-pattern #size-chips-k,
body.studiolab .deck-pattern .skin-theme-label { grid-column: 1; justify-self: start; }
body.studiolab .deck-pattern .chips { grid-column: 2; }

/* =========================================================================
   ★★★★★ THIS OWNER — `vanguard` CLAIMS THE LAB (2026-08-25).

   He rejected his own studio's look and asked for the chamber another owner
   on this theme already had.

   WHAT WAS ACTUALLY WRONG, MEASURED ON HIS OWN LIVE BYTES (rendered at
   1440x900 against his own origin, studiolab.css sha 0aa8993e6600):

     1. THE ROOM WAS HALF-REPAINTED. vanguard.css section 12 re-points
        SIXTEEN --av-* tokens and none of the -rgb TRIPLES, so every surface
        written as rgba(var(--av-*-rgb), .x) still painted the donor's warm
        brown: the name field's ground measured rgba(36, 26, 17, .9), the
        action foot rgba(10, 8, 6, .55), the code sink rgba(10, 8, 6, .85) —
        brown furniture in a near-black steel room, with the donor's jade
        47, 191, 135 still washing every #copy-code while its BORDER had gone
        lime. Two green families and two brown/red families on one screen.
        That is what he rejected, and no amount of layout fixes it.
     2. THE SELECTED CHIP AND THE COMMIT KEY WERE ONE COLOUR. Measured:
        #apply-ingame rgb(95, 197, 62) (vanguard.css section 8 paints it from
        --vg-live) against .chip.on #86cf6c (--av-jade-lt, which section 12
        sets to color-mix(primary 72%, fg)) — 1.17:1 apart. Below the ~1.5:1
        floor at which two colours stop being two colours. Every chip he had
        selected shouted as loudly as the one key that reaches the island.
     3. FOUR TYPEFACES. --av-serif was still Fraunces, so the name field
        measured Fraunces, Georgia, "Times New Roman", serif on a site whose
        display face is Russo One — and the hex values measured Inter, the
        PROSE face, on an owner who declares JetBrains Mono and never used it.

   THE FIX IS THE PALETTE, NOT A SECOND LAYOUT. Every structural defect he
   photographed — the crushed colour wells, the three anonymous chip rows, the
   four equal slabs — is already cured for every owner earlier in this sheet;
   This owner's box simply had not received those bytes yet.

   THE SELECTOR CARRIES .lab AND MUST KEEP IT.
   vanguard.css is linked AFTER this sheet on the studio page (its own
   section 12 comment says so, and the assembled studio.html confirms the
   order). Its block is html[data-site-theme="vanguard"] body.studiolab —
   specificity (0,2,2), IDENTICAL to the shape the rift block uses. Written
   that way here it would lose every shared token to file order and this whole
   block would be INERT while measuring as present: the exact "correct,
   shipped and completely inert" failure. body carries class="studiolab lab"
   (the assembler hardcodes it for the lab studio), so .lab is a free, honest
   third class and takes this block to (0,3,2). It WINS, without one
   !important, and it still cannot reach a sibling because it still begins
   with this owner's own site-theme attribute.
   The studio tone test GATES that raise: it fails
   the moment vanguard.css declares a token this block does not, or this block
   stops out-ranking it.

   WHERE EACH COLOUR IS SPENT — one job each, and the loudest spent once:
     · LIME is the commit, and ONLY the commit. #apply-ingame keeps the fill
       vanguard.css section 8 already gives it; nothing else in this room is
       allowed to be green. That is the whole of defect 2 above.
     · STEEL BONE is STATE. A selected chip is a filled bone plate with
       near-black ink — 14.49:1 against the panel, and NEUTRAL, so the one
       saturated fill on the screen is the key that reaches the island.
       (a sibling owner's own move, and the reason is the same: separate by
       lightness and chroma, never by hue.)
     · RED is the INSTRUMENT — the tool rail, the ember pool, the focus ring.
       It is this site's second voice (vanguard.css's twin edge ignites red at
       one terminus and green at the other); it never fills a control.
     · --av-bad stays the delete INK and only that. On this owner brand red
       and danger red measure 1.14:1 apart, which no hue can fix, so the split
       is carried by shape, fill and the word — never by colour.
   ========================================================================= */
html[data-site-theme="vanguard"] body.studiolab.lab {
  /* ONE DISPLAY FACE — the site's own, so the nameplate, the doors and the
     panel titles speak in the voice the logo already set. */
  --av-serif: 'Russo One', 'Bahnschrift', 'Arial Black', 'Segoe UI', sans-serif;
  /* ...AND A REAL MONO FOR THE NUMBERS. The hex value, the skin code and the
     typed RGBA channels are read character by character; this owner declares
     JetBrains Mono and the lab had never asked for it. */
  --av-mono: 'JetBrains Mono', ui-monospace, 'Cascadia Mono', Consolas, monospace;

  --av-ink: #eae8e3;
  --av-ink-rgb: 234, 232, 227;
  /* the ink ladder is this owner's own dim/mute, not an alpha wash of white -
     a wash over a warm ground reads warm, which is how the brown got in */
  --av-ink-2: #c4c1ba;
  --av-ink-3: #9b978e;

  --av-page: #060607;
  --av-page-rgb: 11, 11, 12;
  --av-page-deep-rgb: 6, 6, 7;
  --av-panel: #181819;
  --av-panel-rgb: 24, 24, 25;
  --av-panel-2: #212122;
  --av-panel-2-rgb: 33, 33, 34;
  --av-panel-3-rgb: 42, 42, 42;
  --av-sink-rgb: 8, 8, 9;
  --av-well-rgb: 8, 8, 9;
  --av-field-rgb: 24, 24, 25;
  --av-rail-hi-rgb: 33, 33, 34;
  --av-rail-lo-rgb: 8, 8, 9;
  --av-sheet-hi-rgb: 24, 24, 25;
  --av-sheet-lo-rgb: 6, 6, 7;
  --av-haze-rgb: 11, 11, 12;
  --av-chamber-top: #141415;
  --av-chamber-bot: #060607;
  --av-deck-top: #212122;
  --av-deck-bot: #0b0b0c;
  --av-shaft-rgb: 234, 232, 227;

  /* ★ THE BRIDGE (2026-09-07). This sheet reads the PLAIN names now - every
     `rgba(var(--av-x-rgb), .n)` became `color-mix(in srgb, var(--av-x) n%,
     transparent)` when the base block was re-pointed at the site's own role
     tokens. The eleven surfaces below never had a plain name to re-point, so
     each is bridged off the triple this block already pins to owner.json:
     same colour, same gate, one form for the sheet to read. */
  --av-page-deep: rgb(var(--av-page-deep-rgb));
  --av-panel-3: rgb(var(--av-panel-3-rgb));
  --av-sink: rgb(var(--av-sink-rgb));
  --av-well: rgb(var(--av-well-rgb));
  --av-field: rgb(var(--av-field-rgb));
  --av-rail-hi: rgb(var(--av-rail-hi-rgb));
  --av-rail-lo: rgb(var(--av-rail-lo-rgb));
  --av-sheet-hi: rgb(var(--av-sheet-hi-rgb));
  --av-sheet-lo: rgb(var(--av-sheet-lo-rgb));
  --av-haze: rgb(var(--av-haze-rgb));
  --av-shaft: rgb(var(--av-shaft-rgb));
  --av-shadow: color-mix(in srgb, rgb(var(--av-page-deep-rgb)) 82%, transparent);

  --av-line: #454444;
  --av-line-dim: #333333;

  /* THE AFFIRMATIVE FAMILY IS BONE, AND THAT IS THE HIERARCHY.
     Lime is this site's LIVE colour and it belongs to one control. Anything
     else filled lime is a second commit key, which is what the studio was
     shipping. The bone plate separates from the panel by LIGHTNESS (14.49:1)
     and from the lime key by CHROMA - it is neutral - so the split survives
     any colour vision, and shape does the rest: a 34px chip is not a 46px
     full-width slab. */
  --av-jade: #97948c;
  --av-jade-rgb: 151, 148, 140;
  --av-jade-lt: #eae8e3;
  --av-jade-lt-rgb: 234, 232, 227;
  --av-jade-hot: #ffffff;
  --av-on-jade: #050505;
  --av-btn-ink: #c4c1ba;
  /* restated from the base so this block, not vanguard.css section 12, owns
     them - an inherited edge is how a lime wash survives a repaint */
  --av-edge: color-mix(in srgb, var(--av-jade-lt) 17%, transparent);
  --av-edge-lit: color-mix(in srgb, var(--av-jade-lt) 55%, transparent);

  /* THE AMBER FAMILY IS THIS SITE'S RED - the rail, the pool, the focus ring */
  --av-amber: #e14b3c;
  --av-amber-rgb: 225, 75, 60;
  --av-amber-lt: #ff7161;
  --av-amber-lt-rgb: 255, 113, 97;
  --av-focus: #ff7161;

  --av-bad: #ff3b5c;
  --av-bad-rgb: 255, 59, 92;
  --av-bad-lt: #f68999;

  /* THE MILLED CORNER. vanguard.css's own signature is a 45-degree cut off
     the TOP-RIGHT of every plate (--vg-cut: 11px); the donor cuts top-left AND
     bottom-right. Two chamfer languages on one screen is what made the Apply
     key look bolted on. The key takes the site's own 11px so a lab key IS a
     site plate; a seat takes 8px because it is smaller furniture. */
  --av-clip-seat: polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 0 100%);
  --av-clip-key: polygon(0 0, calc(100% - 11px) 0, 100% 11px, 100% 100%, 0 100%);
}

/* THE FIRST RULE THE TOKENS CANNOT CARRY. Russo One is right for a nameplate,
   a door and a panel title, and wrong inside a text field, where a 15px poster
   face reads as a heading somebody typed into. The field takes the UI face;
   every other seat of --av-serif is a heading and keeps it. (Same exception,
   same reason, as the rift block above.) */
html[data-site-theme="vanguard"] body.studiolab.lab #design-name {
  font-family: var(--av-sans); font-weight: 500; letter-spacing: .01em;
}

/* THE SECOND RULE THE TOKENS CANNOT CARRY — and the last of his seven.
   His screenshot: the three action keys in sentence
   case, sitting under three rows of chips that terminal.css uppercases. Two
   casing languages in one 335px column. vanguard.css's own written contract
   is "labels are 10.5px caps in the UI face", so the column joins the site it
   lives in rather than the donor it came from. The labels are the OWNER's
   words (labels.studio_*), never ours, and .deck-actions .btn is already
   min-width: auto - so a key that no longer fits beside its neighbours takes
   the next line WHOLE instead of losing letters. */
html[data-site-theme="vanguard"] body.studiolab.lab .deck .btn {
  text-transform: uppercase; letter-spacing: .06em;
}


/* =========================================================================
   2026-08-28 THE EDITOR COLUMN, MEASURED AT 1280x700
   (moved ABOVE section 14 on 2026-08-30: it had been appended at the sheet's
   foot, INSIDE section 14's slice, where the section's own contract gates
   refuse generic deck rules - section 14 is the saved face and only
   `body.studiolab #lab-col/#panel` selectors may live in it.)
   ========================================================================= */

/* ★ THE BAR WAS RESERVED TWICE, AND A COLOUR ROW DREW IN THE SECOND ONE.
   `body.studiolab.has-livebar` already holds the owner's fixed bar off the
   whole grid, so the deck's own `padding-bottom: var(--lab-bar)` reserved it
   again INSIDE the scroller. A sticky foot pins to the content box, so that
   second reservation stayed live below the pinned Apply key and the next row
   painted into it - measured at 1280x700: deck bottom 644, foot bottom 602,
   `eyes` drawn at 601.3-641.5, and the body's own clearance (bar top 658.5)
   already ending the deck 14.5px above the bar. One reservation, on the body,
   where it works. */
body.studiolab.has-livebar .deck { padding-bottom: 0; }
@media (max-width: 900px) {
  /* the phone sheet reserves the notch instead, and still must */
  body.studiolab.has-livebar .deck { padding-bottom: env(safe-area-inset-bottom, 0px); }
}

/* The commit key wraps WHOLE instead of losing letters: `min-width: auto` is
   the content minimum, the lab-tone law (and the vanguard note above says the
   same). The 08-28 cut of this block wrote `0` here and re-armed the exact
   shaving it was fixing - Jurassic Den's "Ruffle the colours" lost its tail
   to a percent-sized track. */
body.studiolab .deck-actions .btn { min-width: auto; }
body.studiolab .deck-actions .deck-apply { white-space: normal; }

/* The wrap is not what changes. In a 196px track the three size chips wrap
   2+1 both before and after; at `flex: 0 0 auto` the wrapped chip kept its
   content width and left the line short. Each line now fills its track. */
body.studiolab .deck-pattern .chips > .chip { flex: 1 1 auto; }

/* the composer stage's animation chips - clip names are the game's own
   export words (data); the row hides while a species ships none.
   ★ THE LAB'S OWN TOKENS, NOTHING ELSE (2026-08-30). This block arrived
   reading the CLASSIC studio's names (--surface, --fg, --accent-hi...),
   which THIS page never defines - on every lab owner the chips drew
   currentColor borders on a transparent ground, the exact shape
   test_studio_theme's defines-what-it-reads gate refuses (and did). A
   fallback chain does not cure it: a read is a read. The chips wear the
   chamber family the rest of this sheet's chips already wear. */
.gc-clip-row { display: flex; flex-wrap: wrap; gap: 5px; }
.gc-clip {
  padding: 6px 11px;
  border: 1px solid var(--av-line);
  background: color-mix(in srgb, var(--av-ink) 4%, transparent);
  color: var(--av-ink-2); font: 600 10.5px/1 var(--av-sans);
  letter-spacing: .06em; text-transform: uppercase; cursor: pointer;
}
.gc-clip:hover { color: var(--av-ink); border-color: var(--av-edge-lit); }
.gc-clip.is-on { color: var(--av-jade-hot); border-color: var(--av-jade); }

/* THE LAB'S SEATS FOR THE COLOUR LAB (js/colorlab.mjs + css/colorlab.css).
   The popover sheet itself is theme-blind core vocabulary; the two things
   only this page knows live here: the chamber buttons rewrite --accent, so
   the lab pins the popover's accent to jade like every other control (law 3
   at the head of this sheet), and the channel row grid grows a fourth track
   for the row's opener key. */
body.studiolab .clab-pop { --clab-accent: var(--av-jade); }
/* the lab's own ground on BOTH shapes - the first cut excluded the phone
   sheet (`:not(.clab-sheet)`) and a phone player got terminal.css's neutral
   grey slab on the lab's brown deck (measured 2026-08-30) */
body.studiolab .clab-pop {
  background: linear-gradient(180deg, var(--av-panel-2), var(--av-panel));
  border-color: var(--av-line);
}
body.studiolab .channel { grid-template-columns: auto minmax(0, 1fr) auto auto; }
body.studiolab .channel .clab-open { order: 4; }
body.studiolab .channel .channel-preview-note { order: 5; }


/* -------------------------------------------------------------------------
   15. DONOR PARITY, 2026-09-02 - the donor's studio as it paints TODAY.
   Dino Queen's Lair, the owner: "design looks hurrendous still. make it like
   arkadia 1:1". Measured against the donor's live page and its living sheet
   (sections 7c "the right column, un-starved" and 7d "the wells column
   below 1280", 2026-08-28/29 - both landed on the donor AFTER this port was
   read, so the port had kept the OLDER 336px column and one-per-line rows):
     - the right column is clamp(400px, 26vw, 480px) from 1280 up and 388px
       below it - a 336px rail could not carry the wells two-up;
     - the colour wells are a two-up GRID of 56px cards - swatch, then name,
       no hex box (the donor's rows never had one; the colour lab's own hex
       field keeps typed hex reachable);
     - chips are 40px, actions 44px, the apply key 50px, rows 16/18px;
     - the sex choice is one full-width segmented switch;
     - the clip keys sit in a rounded panel of pills.
   AND THE THEME LEAKS (the same class of defect as the shell): a site theme
   addresses `html[data-site-theme] .btn-primary` / `body` at (0,2,1) /
   (0,1,2) and repaints the lab's apply key in the SITE accent and its ink in
   the site's grey. The lab's palette is pinned here at (0,2,2)+ - the donor's
   law 3: rail chrome is jade/amber, never the site's accent.
   Seated AFTER the one-scroll-region block and BEFORE section 14 (the saved
   face, which carries its own sheet contract): same-specificity rules above
   are overridden by ORDER, and this block is the last word on every seat it
   names in the editor face.
   ------------------------------------------------------------------------- */
html[data-site-theme] body.studiolab { color: var(--av-ink); font-family: var(--av-sans); }
html[data-site-theme] body.studiolab .btn {
  font-family: var(--av-sans); text-transform: none; border-radius: 0; box-shadow: none;
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 34%, transparent); background: color-mix(in srgb, var(--av-jade-lt) 7%, transparent);
  color: var(--av-btn-ink);
}
html[data-site-theme] body.studiolab .btn:hover { background: color-mix(in srgb, var(--av-jade-lt) 16%, transparent); border-color: var(--av-edge-lit); }
html[data-site-theme] body.studiolab .btn-primary, html[data-site-theme] body.studiolab .btn.primary {
  background: var(--av-jade-lt); border-color: var(--av-jade-lt); color: var(--av-on-jade); font-weight: 600;
}
html[data-site-theme] body.studiolab .btn-primary:hover, html[data-site-theme] body.studiolab .btn.primary:hover {
  background: var(--av-jade-hot); border-color: var(--av-jade-hot);
}
html[data-site-theme] body.studiolab .btn.save { border-color: color-mix(in srgb, var(--av-amber-lt) 55%, transparent); color: var(--av-amber-lt); }
html[data-site-theme] body.studiolab .btn-ghost { background: color-mix(in srgb, var(--av-ink) 4%, transparent); border-color: var(--av-line); color: var(--av-btn-ink); }
html[data-site-theme] body.studiolab .btn:disabled { opacity: .4; }

/* the sex-on-apply choice: studio.mjs mounts it before the apply key with its
   own injected style (id-selector rules); this out-ranks those by class. */
body.studiolab #sex-choice {
  flex: 1 1 100%; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 0; margin: 0; border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  font-size: 12px;
}
body.studiolab #sex-choice .sex-choice-label {
  grid-column: 1 / -1; padding: 7px 10px 4px; opacity: 1;
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--av-ink-3);
}
body.studiolab #sex-choice .sex-choice-btn {
  font: inherit; font-family: var(--av-sans); font-size: 12.5px; letter-spacing: .04em; text-transform: uppercase;
  min-height: 40px; padding: 0 14px; border: 0; border-top: 1px solid var(--av-line-dim); border-radius: 0;
  opacity: 1; color: var(--av-ink-2); background: transparent; cursor: pointer; transition: .15s;
}
body.studiolab #sex-choice .sex-choice-btn + .sex-choice-btn { border-left: 1px solid var(--av-line-dim); }
body.studiolab #sex-choice .sex-choice-btn:hover { color: var(--av-ink); background: color-mix(in srgb, var(--av-jade) 9%, transparent); }
body.studiolab #sex-choice .sex-choice-btn[aria-pressed="true"] {
  color: var(--av-on-jade); background: var(--av-jade-lt); font-weight: 600;
}
body.studiolab #sex-choice .sex-choice-now {
  grid-column: 1 / -1; padding: 5px 10px 7px; opacity: 1;
  font-family: var(--av-sans); font-size: 10.5px; color: var(--av-ink-3);
}

/* the clip keys: the donor's rounded panel of pills, bottom-left of the stage */
body.studiolab .stage-anim {
  padding: 8px 10px; border-radius: 14px;
  border: 1px solid color-mix(in srgb, var(--av-ink) 10%, transparent); background: color-mix(in srgb, var(--av-well) 72%, transparent);
  box-shadow: 0 16px 34px -22px color-mix(in srgb, var(--av-sink) 95%, transparent);
}
body.studiolab .stage-anim .chip, body.studiolab .stage-anim .btn {
  border-radius: 999px; clip-path: none; min-height: 30px; padding: 6px 12px; font-size: 11px;
}

@media (min-width: 901px) {
  body.studiolab { --av-species-w: 246px; --av-rail-w: 388px; }
  body.studiolab .deck > .deck-row-l { padding: 16px 18px; gap: 13px; }
  body.studiolab .deck-row-l.deck-head { gap: 15px; padding-top: 16px; padding-bottom: 16px; }
  body.studiolab #design-name { height: 42px; font-size: 15px; }
  /* one field per row: the caption ABOVE its chips, as the donor's head reads */
  body.studiolab .deck-pattern.deck-field { grid-template-columns: minmax(0, 1fr); gap: 6px 0; }
  body.studiolab .deck-pattern .stage-foot-k, body.studiolab .deck-pattern #size-chips-k,
  body.studiolab .deck-pattern .skin-theme-label { grid-column: 1; }
  body.studiolab .deck-pattern .chips { grid-column: 1; gap: 8px; margin-bottom: 9px; }
  body.studiolab .deck-pattern .chips:last-child { margin-bottom: 0; }
  body.studiolab .chip { min-width: 46px; min-height: 40px; padding: 0 12px; font-size: 13px; }
  /* the wells: a wrapping grid, two-up as soon as the column can hold it */
  body.studiolab #channel-list {
    display: grid; grid-template-columns: repeat(auto-fit, minmax(158px, 1fr)); gap: 9px;
  }
  body.studiolab .channel {
    position: relative; grid-template-columns: auto minmax(0, 1fr);
    min-height: 56px; padding: 10px; gap: 10px;
  }
  body.studiolab .channel input[type="color"] { width: 36px; height: 28px; }
  body.studiolab .channel[data-slot="eyes"] input[type="color"] { width: 28px; border-radius: 50%; }
  body.studiolab .channel[data-slot="eyes"] input[type="color"]::-webkit-color-swatch { border-radius: 50%; }
  body.studiolab .channel[data-slot="eyes"] input[type="color"]::-moz-color-swatch { border-radius: 50%; }
  body.studiolab .channel label { font-size: 12px; white-space: normal; overflow: visible; text-overflow: clip; }
  /* no hex box on a well - the donor's rows never carried one; typed hex
     lives in the colour lab (.clab-hex), one press away on every card */
  body.studiolab .channel .hex { display: none; }
  /* the lab door stays in the DOM for the keyboard and covers the card's
     name side as an invisible target - the swatch keeps the native picker */
  body.studiolab .channel .clab-open {
    position: absolute; inset: 0 0 0 52px; width: auto; height: auto; margin: 0; padding: 0;
    opacity: 0; border: 0; background: transparent; cursor: pointer;
  }
  body.studiolab .channel:has(.clab-open:focus-visible) { outline: 2px solid var(--av-focus); outline-offset: -2px; }
  body.studiolab .channel-preview-note { grid-column: 1 / -1; }
  /* actions: 44px keys two-up, the apply key on its own 50px line */
  body.studiolab .deck-row-l.deck-actions { gap: 9px; padding: 16px 18px; }
  body.studiolab .deck-actions .btn { flex: 1 1 calc(50% - 5px); min-height: 44px; padding: 10px 12px; font-size: 12px; }
  body.studiolab .deck-actions .deck-apply { flex: 1 1 100%; min-height: 50px; font-size: 13px; }
  body.studiolab #lab-tabs .lab-tab { font-size: 15px; }
  /* the donor's column is ONE plain scroll: nothing sticky, nothing blurred.
     A sticky action bar covered the lower wells at 1440x900 and made the
     editor's "Show N more" key fire on rows that were merely under it. */
  body.studiolab .deck-row-l.deck-head, body.studiolab .deck-row-l.deck-actions {
    position: static; -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  /* the editor's own status bar (Show N more / Try ...) is a child of the
     wells host; in a two-up grid it must span the row, never sit in a cell */
  body.studiolab #channel-list > :not(.channel) { grid-column: 1 / -1; }
}
@media (min-width: 901px) and (max-width: 1023px) {
  body.studiolab { --av-species-w: 0px; --av-rail-w: 388px; }
}
@media (min-width: 1280px) {
  body.studiolab { --av-rail-w: clamp(400px, 26vw, 480px); }
}

/* =========================================================================
   14. THE SAVED FACE — the library given the editor's own card language.
   (2026-08-25, this owner, from his own screenshot of the SAVED tab, which
   he flagged as still wrong.)

   ★★★★★ 14a IS A DROPPED DECLARATION, NOT A LOOK. `#lab-col` was added on
   2026-08-25 to make the right rail a two-row grid, and the rewrite lost
   `grid-column: 1` from its three children. `#lab-col` declares
   `grid-template-rows` and NO columns, so two children both pinned to
   `grid-row: 2` do not stack in one cell - auto-placement drops the second
   into an IMPLICIT SECOND COLUMN. Measured on a 1440 monitor: the 336px rail
   split 190 / 146, the hidden editor deck kept the 190 and the library got
   the 146. That single missing declaration is every defect in his shot -
   the four-line stack, the one-word-per-line name, the clipped tag, and the
   dead black band down the left of the panel, which is the hidden deck's
   phantom track and not the specimen stage at all. Restoring it takes the
   panel 145.9px -> 336px and the row 170.3px -> 113.6px with nothing else
   changed. It is the FIRST rule here because everything below assumes a
   column that is actually a column. Sibling proof: a sibling owner's live
   box still serves the pre-`#lab-col` sheet and measures the full 336px, so
   this reached this owner with the wave and reaches every other lab
   owner the moment the shared sheet ships.

   ★ WHY THIS BLOCK IS AT THE FOOT. `vanguard.css` and the tone blocks above
   are linked/ordered AFTER the base library rules at section 8, and
   `html[data-site-theme="vanguard"] select` (0,1,2) and `... .btn` (0,2,1)
   both outrank a lone class. Every rule below is written at

   `body.studiolab` + a class or deeper, and appended, so it wins on
   specificity AND on order rather than on `!important`. A dropped
   declaration is silent; raise the selector, never `!important`.

   ★ NO NEW WORDS. Every string on this face is the owner's own
   (`labels.studio_*`, `content.studio.*`). This block adds none.

   ★ TOKEN ONLY. Zero colour literals, zero `!important`. This owner's
   green, the rift ember and the donor's jade all arrive through the same
   `--av-*` pair the tone blocks re-point.
   ========================================================================= */

/* 14a. THE COLUMN IS ONE COLUMN. ---------------------------------------- */
body.studiolab #lab-col > #lab-tabs { grid-row: 1; grid-column: 1; }
body.studiolab #lab-col > #lab-dock-panel,
body.studiolab #lab-col > #panel { grid-row: 2; grid-column: 1; }

/* 14b. THE CARD. The row IS the card now - one plate per saved thing, cut
   from `--av-clip-seat` like every other seat on this page (`.deck-row`,
   `.channel`, `.glitch-tile`), on the same 3%-ink bed, behind the same
   hairline. Before this the row was five loose controls on a bare ground and
   read as a browser form. ------------------------------------------------ */
body.studiolab #panel .design-row {
  position: relative;
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 5px; margin: 0 0 7px; padding: 9px 10px 10px;
  clip-path: var(--av-clip-seat);
  background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  border: 1px solid var(--av-line-dim);
  transition: border-color .15s, background-color .15s;
}
body.studiolab #panel .design-row:hover { border-color: var(--av-line); }
/* the row the editor currently holds open, marked the way `.deck-row.on` is */
body.studiolab #panel .design-row.editing {
  border-color: var(--av-edge-lit); background: color-mix(in srgb, var(--av-jade) 10%, transparent);
}

/* 14c. THE HEAD — the name owns its own line, and the tag is a chip that
   cannot be cut. `.design-load` was a bordered plate INSIDE a bordered row:
   two frames, one object. Here it is the card's own head, so the card has
   one frame. ------------------------------------------------------------- */
body.studiolab #panel .design-row > .design-load {
  flex: 1 1 100%; min-width: 0; width: 100%;
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin: 0 0 3px; padding: 0 32px 8px 0;
  clip-path: none; border: 0; border-bottom: 1px solid var(--av-line-dim);
  background: none; text-align: left;
}
body.studiolab #panel .design-row > .design-load:hover {
  border-color: transparent; border-bottom-color: var(--av-line); background: none;
}
/* the NAME. The editor titles its cards in the serif at 14px
   (`.glitch-tile h3`); the library said 12.5px sans and read as a form
   label. `min-width: 0` is the half that lets it shrink instead of pushing
   the tag off the edge; `overflow-wrap` keeps a single long word inside the
   card rather than clipped by it. */
body.studiolab #panel .design-row > .design-load b {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--av-serif); font-weight: 500; font-size: 14px;
  letter-spacing: 0; line-height: 1.25; color: var(--av-ink);
  overflow-wrap: anywhere;
}
/* the TAG. `flex: 0 0 auto` is why it stops being cut: an auto-basis chip is
   sized by its own word, never by a share of a track it does not fit in. */
body.studiolab #panel .design-row > .design-load span {
  flex: 0 0 auto; align-self: center;
  padding: 2px 6px;
  font-family: var(--av-sans); font-size: 9px; font-weight: 400;
  letter-spacing: .16em; text-transform: uppercase; line-height: 1.5;
  color: var(--av-ink-3); border: 1px solid var(--av-line-dim);
  white-space: nowrap;
}
body.studiolab #panel .skin-row .skin-name span { color: var(--av-jade-lt); }
/* a saved DESIGN's head is a button and opens it in the editor; a bought
   skin's head is a div and opens nothing. The cursor has to tell them apart
   or the card promises a press it will not answer. */
body.studiolab #panel .design-row > button.design-load { cursor: pointer; }
body.studiolab #panel .design-row > button.design-load:hover b { color: var(--av-jade-lt); }
body.studiolab #panel .design-row > div.design-load { cursor: default; }
body.studiolab #panel .design-row > .design-load:focus-visible {
  outline: 2px solid var(--av-focus); outline-offset: 2px;
}

/* 14d. THE PATTERN PICKER — the instrument's own control, not the browser's.
   Same `<select>`, same options, same handler: only `appearance` and the
   dress change. A native select was the single most out-of-place object on a
   page where every other control is a milled plate. The caret is drawn from
   two gradients, so nothing is fetched and nothing can 404.
   ★ LONGHANDS ON PURPOSE. The rule three hundred lines up sets `background:`
   as a SHORTHAND; a `background-image` written in shorthand syntax is
   dropped in silence, and a shorthand written here would wipe the colour. */
body.studiolab #panel .design-row .skin-pattern {
  /* ★ A CONTENT BASIS, NEVER A TRACK. `flex: 1 1 9em` gave the picker a
     share of a row it did not fit in and "Pattern: as saved" came back as
     "Pattern: as sa..." on a 336px column - the equal-flex-track defect in a
     second costume, and this label is the one carrying the STATE.
     `flex: 0 0 auto` starts it at its OWN words and forbids the shrink, so a
     row that can no longer fit takes a SECOND LINE rather than shaving it -
     and it stays right for an owner translation we have never seen, which a
     measured `min-width` would not.
     ★ `min-width: max-content` was tried first and is WRONG here: measured
     in Chromium a `<select>`'s max-content resolved to 281px - the whole
     row - against 90px of actual option text, so the picker ate the line. */
  /* ★ AND `display` IS THE HALF THAT DECIDES IT. site.css dresses every
     form control as a full-width block field; a `display: block` select with
     `width: auto` fills its containing block, so `flex: 0 0 auto` was
     honestly reporting a 281px CONTENT size. `inline-block` is what makes a
     select shrink-wrap its own longest option - measured 281px -> 145px. */
  display: inline-block; width: auto;
  flex: 0 0 auto; min-width: 0; max-width: 100%;
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  min-height: 32px; padding: 6px 20px 6px 9px;
  font-family: var(--av-sans); font-size: 11px; letter-spacing: .02em;
  clip-path: var(--av-clip-key); border-radius: 0;
  border: 1px solid color-mix(in srgb, var(--av-jade-lt) 34%, transparent);
  background-color: color-mix(in srgb, var(--av-sink) 85%, transparent);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--av-ink-3) 50%),
    linear-gradient(135deg, var(--av-ink-3) 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: right 10px center, right 6px center;
  background-repeat: no-repeat, no-repeat;
  color: var(--av-ink); text-overflow: ellipsis;
}
body.studiolab #panel .design-row .skin-pattern:hover { border-color: var(--av-edge-lit); }
body.studiolab #panel .design-row .skin-pattern:focus-visible {
  outline: 2px solid var(--av-focus); outline-offset: 2px;
}
/* the popup list is the OS's, and on a dark page it inherits a white one
   unless the option itself is painted. */
body.studiolab #panel .design-row .skin-pattern option {
  background-color: var(--av-sink); color: var(--av-ink);
}

/* 14e. ONE COMMIT, GHOST SECONDARIES, AND A DESTRUCTIVE THAT IS NOT A HUE.
   ★★★★★ TWO REDS ON A DARK GROUND CANNOT BE TOLD APART BY COLOUR, and this
   owner's brand IS red. So the split is carried by SHAPE, FILL and WEIGHT,
   and the rule lives here in the stylesheet rather than only in a doc:
     - APPLY is the only FILLED control in the row - the editor's own
       `.btn-primary` plate, `--av-jade-lt` under `--av-on-jade` ink.
     - every other key is a hairline ghost on the card's own bed.
     - DELETE is the QUIETEST thing in the row at rest - no red at all - is
       the only ROUND object on a page where everything else is milled
       square, and is pushed to the row's end by `margin-left: auto` so it is
       not part of the action group. It fills with `--av-bad` only while
       pointed at or focused, so the brand red and the danger red are never
       both filled at the same moment.
   ★ The WORD leg is carried by the control's own `title` / `aria-label`,
     which are the OWNER's localised strings ("Delete {name}"). It is NOT
     rendered inline: every string the page has for this key embeds the
     design's name and would run wider than the card. A visible short word
     needs a new content key and one line in studio.mjs. */
body.studiolab #panel .design-row .btn {
  flex: 0 0 auto; min-height: 32px; padding: 6px 9px;
  font-size: 11px; letter-spacing: .02em; white-space: nowrap;
  background: color-mix(in srgb, var(--av-ink) 4%, transparent); border-color: var(--av-line);
  color: var(--av-btn-ink);
}
body.studiolab #panel .design-row .btn:hover {
  background: color-mix(in srgb, var(--av-jade-lt) 14%, transparent); border-color: var(--av-edge-lit);
}
/* THE COMMIT. `.design-apply` and `.skin-wear` had no rule of their own in
   this sheet at all, which is why Apply rendered as a plain grey key beside
   Export - two equal controls where one of them is the whole point of the
   card. */
body.studiolab #panel .design-row .design-apply,
body.studiolab #panel .design-row .skin-wear {
  order: 5;
  background: var(--av-jade-lt); border-color: var(--av-jade-lt);
  color: var(--av-on-jade); font-weight: 600;
}
body.studiolab #panel .design-row .design-apply:hover,
body.studiolab #panel .design-row .skin-wear:hover {
  background: var(--av-jade-hot); border-color: var(--av-jade-hot);
}
/* the secondaries, in a fixed reading order whichever ones the server drew */
body.studiolab #panel .design-row .design-edit,
body.studiolab #panel .design-row .skin-edit { order: 6; }
body.studiolab #panel .design-row .skin-export,
body.studiolab #panel .design-row .design-code { order: 7; }
body.studiolab #panel .design-row .skin-sell,
body.studiolab #panel .design-row .skin-delist { order: 8; }
/* a listed skin is a state, and a state is worth a different ink */
body.studiolab #panel .design-row .skin-delist { color: var(--av-amber-lt); }

/* THE DESTRUCTIVE KEY. */
/* ★★★★★ THE DESTRUCTIVE KEY LEAVES THE ACTION ROW ALTOGETHER.
   In flow with `margin-left: auto` it still competed for the row's width,
   and on the owners whose tone block UPPERCASES the lab keys (rift, and
   vanguard's own `.deck .btn` rule) "APPLY EXPORT" plus a 28px key no longer
   fit 303px - so the key wrapped and sat ALONE on a line of its own, which
   is his original screenshot's shape coming back by another road. Measured
   on two sibling owners at 1440: row 89.6px -> 122.6px.
   Absolutely placed in the card's top-right corner it costs the action row
   NOTHING at any width, in any owner's words, and it gains a fixed position
   and a third axis of separation from the safe keys beside it. The head
   reserves the corner with its own padding, so a long name cannot run under
   it. */
body.studiolab #panel .design-row .design-del {
  position: absolute; top: 7px; right: 8px; z-index: 2;
  width: 28px; height: 28px; min-height: 28px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; clip-path: none;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  color: var(--av-ink-3); font: 15px/1 var(--av-sans); cursor: pointer;
  transition: background-color .15s, border-color .15s, color .15s;
}
/* ★★★★★ THE HOT STATE IS A TINT, NEVER A FILL - AND THAT IS A RULING,
   not a preference. The fleet law says danger may be "a filled plate with an
   icon AND the word BEHIND A CONFIRM"; this key has no confirm on a saved
   design (one press deletes it), and on this owner brand red and danger
   red measure 1.14:1 apart, so a filled red key here is indistinguishable
   from the site's own brand plate. The studio tone test pins that for
   this owner and refuses any `var(--av-bad)` background in this sheet. The
   split is carried instead by SHAPE (the only round object on the page),
   POSITION (the card's corner, out of the action row), INK and BORDER, and
   the owner's own `title`/`aria-label` words - and the 12% tint is the base
   sheet's own long-standing idiom for exactly this.
   (`feedback_two_reds_on_a_dark_ground_cannot_be_told_apart_by_colour`) */
body.studiolab #panel .design-row .design-del:hover,
body.studiolab #panel .design-row .design-del:focus-visible {
  background: color-mix(in srgb, var(--av-bad) 14%, transparent); border-color: var(--av-bad);
  color: var(--av-bad-lt);
}
body.studiolab #panel .design-row .design-del:focus-visible {
  outline: 2px solid var(--av-focus); outline-offset: 2px;
}

/* 14f. THE DISCARD QUESTION. studio.mjs replaces the whole row with a
   sentence and two keys; the sentence names the skin. Here the question owns
   the card, YES is the filled danger plate WITH the owner's word on it, and
   NO is the ghost - the one place on this face where a red fill and a word
   arrive together, which is exactly where the law wants them. */
body.studiolab #panel .design-row > .dim {
  flex: 1 1 100%; min-width: 0; margin: 0 0 7px;
  font-family: var(--av-sans); font-size: 11.5px; line-height: 1.45;
  color: var(--av-ink-2);
}
/* YES is the only key on this face that carries the danger ink AND a word
   (the owner's `studio_discard_yes`) AND a sentence naming the skin above
   it. It is still not FILLED, for the reason written on the delete key. */
body.studiolab #panel .design-row .skin-discard-yes {
  order: 5; background: color-mix(in srgb, var(--av-bad) 14%, transparent);
  border-color: var(--av-bad); color: var(--av-bad-lt); font-weight: 600;
}
body.studiolab #panel .design-row .skin-discard-yes:hover {
  background: color-mix(in srgb, var(--av-bad) 26%, transparent); border-color: var(--av-bad-lt);
}
body.studiolab #panel .design-row .skin-discard-no { order: 6; }

/* 14g. THE SLOT ROWS take the same card, because they are the same list to
   anybody looking at this tab. Two grammars stacked in one column is what
   made the face read as unfinished. ------------------------------------- */
body.studiolab #panel .slotdeck-row {
  display: flex; flex-wrap: wrap; align-items: center;
  gap: 6px; margin: 0 0 7px; padding: 9px 10px 10px;
  clip-path: var(--av-clip-seat);
  background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  border: 1px solid var(--av-line-dim);
  transition: border-color .15s;
}
body.studiolab #panel .slotdeck-row:hover { border-color: var(--av-line); }
body.studiolab #panel .slotdeck-row > .slotdeck-name {
  flex: 1 1 100%; min-width: 0; width: 100%;
  align-items: baseline; gap: 10px;
  margin: 0 0 3px; padding: 0 0 8px;
  clip-path: none; border: 0; border-bottom: 1px solid var(--av-line-dim);
  background: none;
}
body.studiolab #panel .slotdeck-row > .slotdeck-name b {
  flex: 1 1 auto; min-width: 0;
  font-family: var(--av-serif); font-weight: 500; font-size: 14px;
  letter-spacing: 0; line-height: 1.25; color: var(--av-ink);
  overflow-wrap: anywhere;
}
body.studiolab #panel .slotdeck-row > .slotdeck-name span {
  flex: 0 0 auto; align-self: center; padding: 2px 6px;
  font-size: 9px; letter-spacing: .16em; border: 1px solid var(--av-line-dim);
  white-space: nowrap;
}
body.studiolab #panel .slotdeck-row .btn {
  flex: 0 0 auto; min-height: 32px; padding: 6px 10px; font-size: 11px;
  background: color-mix(in srgb, var(--av-ink) 4%, transparent); border-color: var(--av-line);
  color: var(--av-btn-ink);
}
body.studiolab #panel .slotdeck-row .btn:hover {
  background: color-mix(in srgb, var(--av-jade-lt) 14%, transparent); border-color: var(--av-edge-lit);
}
/* the slot's own destructive key keeps the SAME grammar as the library's -
   round, quiet, isolated - and its ARMED state is the filled danger plate.
   `.is-armed` carries the owner's word already, so this is the second place
   shape, fill and words all arrive together. */
body.studiolab #panel .slotdeck-row .slotdeck-del {
  order: 9; margin-left: auto;
  width: 30px; height: 30px; min-height: 30px; padding: 0;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 50%; clip-path: none;
  border: 1px solid var(--av-line); background: color-mix(in srgb, var(--av-ink) 3%, transparent);
  color: var(--av-ink-3); font: 15px/1 var(--av-sans);
}
body.studiolab #panel .slotdeck-row .slotdeck-del:hover,
body.studiolab #panel .slotdeck-row .slotdeck-del:focus-visible {
  background: color-mix(in srgb, var(--av-bad) 14%, transparent); border-color: var(--av-bad);
  color: var(--av-bad-lt);
}
/* ARMED is where the word arrives (studiolab.js swaps the glyph for the
   owner's own confirm label), so this is the strongest danger state on the
   face - ink, border, weight and a word, on the same tint. */
body.studiolab #panel .slotdeck-row .slotdeck-del.is-armed {
  width: auto; border-radius: 999px; padding: 0 12px; font-size: 11px;
  background: color-mix(in srgb, var(--av-bad) 26%, transparent); border-color: var(--av-bad);
  color: var(--av-bad-lt); font-weight: 600;
}
body.studiolab #panel .slotdeck-pick {
  flex: 1 1 100%; padding: 2px 0 0; gap: 6px;
}
body.studiolab #panel .slotdeck-row .slotdeck-target {
  display: inline-block; width: auto;
  flex: 0 1 auto; min-width: 0; max-width: 100%;
  -webkit-appearance: none; -moz-appearance: none; appearance: none;
  min-height: 32px; padding: 6px 26px 6px 10px; font-size: 11px;
  clip-path: var(--av-clip-key);
  background-color: color-mix(in srgb, var(--av-sink) 85%, transparent);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--av-ink-3) 50%),
    linear-gradient(135deg, var(--av-ink-3) 50%, transparent 50%);
  background-size: 5px 5px, 5px 5px;
  background-position: right 14px center, right 9px center;
  background-repeat: no-repeat, no-repeat;
}

/* 14h. THE EMPTY AND FIRST-RUN STATES. A player with nothing saved gets the
   owner's own sentence on the same bed the cards use, so the list reads as
   an empty shelf rather than a page that failed to load. ----------------- */
body.studiolab #panel .designs-empty,
body.studiolab #panel .preset-empty {
  margin: 0; padding: 14px 12px;
  clip-path: var(--av-clip-seat);
  background: color-mix(in srgb, var(--av-ink) 2%, transparent);
  border: 1px dashed var(--av-line-dim);
  font-family: var(--av-sans); font-size: 11.5px; line-height: 1.5;
  color: var(--av-ink-3);
}
body.studiolab #panel .slotdeck-pick .designs-empty {
  flex: 1 1 100%; padding: 8px 10px;
}

/* 14i. TOUCH AND PHONE. The `(pointer: coarse)` tier above gives every
   `#panel button` a 44px MINIMUM HEIGHT - which on a 30px round key makes an
   ellipse, so the width has to follow it. The card itself needs nothing: it
   is a wrapping row and the bottom sheet is simply narrower. ------------- */
@media (pointer: coarse) {
  body.studiolab #panel .design-row .design-del,
  body.studiolab #panel .slotdeck-row .slotdeck-del {
    width: 44px; height: 44px; min-height: 44px; font-size: 18px;
  }
  /* the corner grew, so the head's reservation grows with it */
  body.studiolab #panel .design-row .design-del { top: 4px; right: 5px; }
  body.studiolab #panel .design-row > .design-load { padding-right: 46px; }
  body.studiolab #panel .slotdeck-row .slotdeck-del.is-armed {
    width: auto; height: 44px;
  }
  body.studiolab #panel .design-row .skin-pattern,
  body.studiolab #panel .slotdeck-row .slotdeck-target { min-height: 44px; }
}
@media (max-width: 900px) {
  /* the sheet is the full width of a phone, so the head can breathe */
  body.studiolab #panel .design-row > .design-load b,
  body.studiolab #panel .slotdeck-row > .slotdeck-name b { font-size: 15px; }
}




/* =========================================================================
   16. THE 2026-09-07 REDESIGN — THE PHOTOGRAPH, THE SCENE CYCLE, AND THE
   TYPE.

   His words: "for /studio redesign it fully. use a visual similar to [the
   reference]. for the background I want default background [the mountain
   lake]. and give people the option to change and cycle background [seven
   images]."

   SEATED LAST, so every rule here is the sheet's last word on the seat it
   names without one `!important` - the same way sections 14 and 15 were
   seated, and for the same reason.

   THREE THINGS THIS SECTION DOES, AND NOTHING ELSE:
     1. THE GROUND IS THE OWNER'S PHOTOGRAPH. `--art-scene-studio` is emitted
        ONLY where an owner typed one (webcore/theme.py::scene_tokens), so
        every read below carries `, none` and an owner who typed nothing keeps
        the painted chamber exactly as it was. That is the whole
        plug-and-play contract: a server born tomorrow gets this layout, this
        type and this palette, and no picture.
     2. THE ROOM STANDS DOWN OVER A PICTURE. A photograph brings its own
        weather; drifting puffs, rising motes and a light shaft drawn on top
        of one read as dirt on the lens. studiolab.js publishes
        `body.lab-scene` when a scene token is really there, and only the
        contact haze under the feet, the grain and the edge vignette stay.
     3. THE TYPE IS THE SITE'S. Tracked capitals in the display face for every
        title and key, the UI face for labels, the mono face for numbers -
        which is what the rest of this site has said since its own redesign.
   ========================================================================= */

/* ---- 16a. THE PHOTOGRAPH -------------------------------------------------
   Two layers under everything: the owner's blur-up cut, then the real file
   fading over it. `.viewport-wrap` already paints the chamber gradient as its
   own background, so with no scene typed both layers resolve to `none` and
   the chamber is what shows - no second rule, no class, no script.
   THE CANVAS IS ALREADY TRANSPARENT (studio.mjs boots the renderer with
   `alpha: true`); what used to make it read opaque was the ROOM's own
   backdrop sphere and its repainted ground disc, and both of those now stand
   down for a scene - see studiolab.js::buildRoom and studio.mjs::bootStage. */
body.studiolab .viewport-wrap::before,
body.studiolab .viewport-wrap::after {
  content: ""; position: absolute; inset: 0; z-index: 0;
  pointer-events: none;
  background-repeat: no-repeat;
  background-position: 50% 56%;
  background-size: cover;
}
body.studiolab .viewport-wrap::before {
  background-image: var(--art-scene-studio-placeholder, none);
  filter: saturate(88%);
}
/* ★ `--lab-scene` IS THE CYCLE'S ONE WRITE. studiolab.js sets it on the body
   to `var(--art-scene-studio-alt-N)` - a token reference, never a URL - so no
   asset path is ever written by a script either. Absent (no alternates, or a
   page whose script did not run) the read falls straight through to the
   owner's default cut. */
body.studiolab .viewport-wrap::after {
  background-image: var(--lab-scene, var(--art-scene-studio, none));
  filter: saturate(94%);
  opacity: 0;
  animation: lab-scene-in .45s ease .18s forwards;
}
@keyframes lab-scene-in { to { opacity: 1; } }
/* the phone cut, where the owner typed one. A scene the player CHOSE wins at
   every width: the alternates ship one file each, and handing a phone the
   default cut because the chosen one has no portrait sibling would be the
   control quietly not working. */
@media (max-width: 900px) {
  body.studiolab .viewport-wrap::after {
    background-image: var(--lab-scene-phone, var(--art-scene-studio-phone, var(--art-scene-studio, none)));
    background-position: 50% 62%;
  }
}
/* A full-bleed cross-fade is exactly the large-area motion the reduced-motion
   rule exists to stop, so the real file simply IS there. */
@media (prefers-reduced-motion: reduce) {
  body.studiolab .viewport-wrap::after { animation: none; opacity: 1; }
}

/* ---- 16b. THE SCRIM GOES WHERE THE TEXT IS, AND NOWHERE ELSE ------------
   A photograph under a full-page scrim is a grey rectangle. The two bands
   that carry type - the growth pill and the nameplate at the top, the clip
   keys and the tool rail at the foot - get their own contrast; the middle of
   the frame, which is the animal, is left alone. Drawn inside `.vp-overlay`
   (z-index 4) at z-index -1, so it is above the canvas and behind every
   control the overlay carries. */
body.studiolab.lab-scene .vp-overlay::before {
  content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
  background:
    linear-gradient(180deg,
      color-mix(in srgb, var(--av-page-deep) 76%, transparent) 0%,
      color-mix(in srgb, var(--av-page-deep) 34%, transparent) 11%,
      transparent 20%),
    linear-gradient(0deg,
      color-mix(in srgb, var(--av-page-deep) 80%, transparent) 0%,
      color-mix(in srgb, var(--av-page-deep) 36%, transparent) 10%,
      transparent 19%);
}

/* ---- 16c. THE ROOM STANDS DOWN OVER A PICTURE --------------------------- */
body.studiolab.lab-scene #lab-cloud .puff,
body.studiolab.lab-scene #lab-parts,
body.studiolab.lab-scene #lab-vig::after { display: none; }
/* the contact haze STAYS - it is what sits the animal on the ground - and the
   edge vignette stays, because it is the frame's own edge. The grain goes
   quiet: film grain over a photograph is grain on grain. */
body.studiolab.lab-scene #lab-grain { opacity: .022; }
body.studiolab.lab-scene #lab-cloud { top: 42%; }

/* ---- 16d. THE VIEW KEYS — the scene, and the light on it ----------------
   Built by studiolab.js, both of them honestly: #scene-cycle exists only
   where the owner typed alternates to walk, #stage-light only where the stage
   is really running. On a phone studiolab.js hoists the whole group into the
   foot strip, because nothing interactive may sit over the model band. */
body.studiolab .vp-viewkeys {
  position: absolute; right: 18px; bottom: calc(var(--lab-bottom, 18px) + 58px);
  z-index: 7; display: flex; align-items: center; gap: 8px;
  pointer-events: auto;
}
body.studiolab .vp-viewkeys:empty { display: none; }
body.studiolab .viewkey {
  display: inline-flex; align-items: center; gap: 2px;
  padding: 4px 6px; border-radius: 999px;
  background: color-mix(in srgb, var(--av-well) 82%, transparent);
  border: 1px solid color-mix(in srgb, var(--av-ink) 14%, transparent);
  box-shadow: 0 16px 34px -22px var(--av-shadow);
}
body.studiolab .viewkey .vk-label {
  font-family: var(--av-sans); font-size: 9.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--av-ink-3); padding: 0 6px 0 8px;
}
body.studiolab .viewkey .vk-count {
  font-family: var(--av-mono); font-size: 10.5px; letter-spacing: .06em;
  color: var(--av-jade-lt); min-width: 3.4em; text-align: center;
  font-variant-numeric: tabular-nums;
}
body.studiolab .viewkey button {
  width: 26px; height: 26px; min-height: 26px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: inherit; font-size: 14px; line-height: 1; padding: 0;
  border: 1px solid transparent; background: transparent;
  color: var(--av-ink-3); cursor: pointer; transition: .15s;
}
body.studiolab .viewkey button:hover {
  color: var(--av-ink); background: color-mix(in srgb, var(--av-jade) 20%, transparent);
}
body.studiolab .viewkey button:focus-visible { outline: 2px solid var(--av-focus); outline-offset: 2px; }
/* the light pair is a STATE, so the chosen one is filled the way every other
   chosen thing on this page is */
body.studiolab #stage-light button[aria-pressed="true"] {
  color: var(--av-on-jade); background: var(--av-jade-lt);
}
body.studiolab #stage-light button:disabled { opacity: .4; cursor: not-allowed; }

/* ---- 16e. THE CAPTION STACK -------------------------------------------- */
/* the growth pill: one line, the reading in the mono face, the reference's
   own rhythm - label, track, number, stage */
body.studiolab .stage-growth {
  top: 22px; gap: 12px; padding: 6px 8px 6px 16px;
  background: color-mix(in srgb, var(--av-well) 78%, transparent);
  border-color: color-mix(in srgb, var(--av-ink) 13%, transparent);
  box-shadow: 0 16px 34px -24px var(--av-shadow);
}
body.studiolab .stage-growth .gr-label { letter-spacing: .22em; font-size: 9.5px; }
body.studiolab .stage-growth .gr-readout {
  font-family: var(--av-mono); letter-spacing: .1em; min-width: 0;
  padding-right: 8px; color: var(--av-ink-2);
}
body.studiolab .vp-center-stack { top: 68px; gap: 8px; }
body.studiolab .vp-specnav { gap: 18px; }
body.studiolab .vp-specnav .spec-status { letter-spacing: .34em; font-size: 10px; }
body.studiolab .vp-specnav .nav-arrow {
  width: 40px; height: 40px;
  background: color-mix(in srgb, var(--av-well) 78%, transparent);
  border-color: color-mix(in srgb, var(--av-ink) 14%, transparent);
  box-shadow: 0 16px 34px -24px var(--av-shadow);
}

/* ---- 16f. THE TOOL RAIL ------------------------------------------------- */
body.studiolab .lab-rail-stack {
  gap: 10px; padding: 7px 12px;
  border-color: color-mix(in srgb, var(--av-amber) 30%, transparent);
  background: linear-gradient(color-mix(in srgb, var(--av-rail-hi) 88%, transparent),
                              color-mix(in srgb, var(--av-rail-lo) 94%, transparent));
  -webkit-backdrop-filter: blur(7px); backdrop-filter: blur(7px);
}
body.studiolab .rail-action {
  font-size: 10.5px; letter-spacing: .14em; text-transform: uppercase;
  padding: 8px 12px;
}
body.studiolab .rail-colors button, body.studiolab .rail-custom {
  width: 20px; height: 20px; border-radius: 50%;
}
body.studiolab .rail-colors button.active {
  box-shadow: 0 0 0 2px var(--av-rail-lo), 0 0 0 3px var(--av-jade-lt);
}
body.studiolab .vp-rot { letter-spacing: .26em; font-size: 9.5px; }

/* ---- 16g. THE DOORS ----------------------------------------------------- */
/* Tracked capitals in the display face, with the accent under the open one -
   the tab grammar every other page on this site keeps. */
body.studiolab #lab-tabs { min-height: 46px; }
body.studiolab #lab-tabs .lab-tab {
  font-family: var(--av-serif); font-weight: 500; font-size: 11.5px;
  letter-spacing: .18em; text-transform: uppercase;
  padding: 15px 6px 13px;
}
body.studiolab #lab-tabs .lab-tab.active { border-bottom-color: var(--av-jade-lt); }

/* ---- 16h. THE SPECIES LIST — a plate, a name, a chevron ----------------
   The reference gives every row a small specimen plate, the name, and a
   chevron. The plate is a PLATE, not a photograph: this core ships no
   per-species picture and a made-up one would be a picture of the wrong
   animal. studiolab.js writes the initial into it off the row's own label. */
body.studiolab .ctx-title {
  font-family: var(--av-serif); font-weight: 500; font-size: 12px;
  letter-spacing: .26em; text-transform: uppercase; color: var(--av-ink-2);
  height: 46px;
}
body.studiolab .deck-row {
  position: relative; align-items: center; gap: 11px;
  padding: 8px 26px 8px 8px;
}
body.studiolab .deck-row .dr-thumb {
  flex: 0 0 auto; width: 30px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  clip-path: var(--av-clip-seat);
  background: color-mix(in srgb, var(--av-ink) 5%, transparent);
  border: 1px solid color-mix(in srgb, var(--av-ink) 10%, transparent);
  font-family: var(--av-serif); font-weight: 500; font-size: 13px;
  letter-spacing: 0; color: color-mix(in srgb, var(--av-jade-lt) 70%, transparent);
}
body.studiolab .deck-row b {
  flex: 1 1 auto; min-width: 0; letter-spacing: .06em;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
body.studiolab .deck-row span { flex: 0 0 auto; letter-spacing: .16em; }
/* the chevron: the row's own affordance, drawn not fetched */
body.studiolab .deck-row::after {
  content: ""; position: absolute; right: 11px; top: 50%;
  width: 6px; height: 6px; margin-top: -3px;
  border-right: 1px solid var(--av-ink-3); border-top: 1px solid var(--av-ink-3);
  transform: rotate(45deg); transition: border-color .14s;
}
body.studiolab .deck-row.on::after, body.studiolab .deck-row:hover::after {
  border-color: var(--av-jade-lt);
}
body.studiolab .deck-row.on .dr-thumb {
  border-color: var(--av-edge-lit);
  background: color-mix(in srgb, var(--av-jade) 14%, transparent);
  color: var(--av-jade-lt);
}
body.studiolab #deck-filter {
  font-size: 11.5px; letter-spacing: .1em; padding: 9px 11px;
}

/* ---- 16i. THE EDITOR COLUMN -------------------------------------------- */
/* every group in this column now says what it is, in the same voice */
body.studiolab .deck-eyebrow {
  display: block; width: 100%; margin: 0 0 9px;
  font-family: var(--av-sans); font-size: 10px; letter-spacing: .22em;
  text-transform: uppercase; color: var(--av-ink-3);
}
body.studiolab .micro-label, body.studiolab .stage-foot-k {
  letter-spacing: .2em;
}
body.studiolab #design-name {
  font-family: var(--av-sans); font-size: 13.5px; letter-spacing: .04em;
}
body.studiolab .chip {
  font-family: var(--av-sans); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase;
}
body.studiolab .channel label {
  font-family: var(--av-sans); letter-spacing: .16em; text-transform: uppercase;
  font-size: 10.5px;
}
body.studiolab .deck-actions .btn {
  font-family: var(--av-sans); letter-spacing: .12em; text-transform: uppercase;
  font-size: 11px;
}
body.studiolab .deck-actions .deck-apply { letter-spacing: .18em; }
body.studiolab .code-lead, body.studiolab .deck-codes .btn {
  letter-spacing: .14em; text-transform: uppercase;
}
body.studiolab .save-note { letter-spacing: .04em; }

/* ---- 16j. THE PHONE ----------------------------------------------------- */
@media (max-width: 900px) {
  /* the view keys ride in the foot strip, under the band, beside the clip
     keys - studiolab.js moves the element, this is what lets it sit there */
  body.studiolab #lab-footstrip .vp-viewkeys,
  body.studiolab #lab-footstrip .vp-tools {
    position: static; flex: 0 0 auto; bottom: auto; right: auto; left: auto; top: auto;
  }
  body.studiolab #lab-footstrip .vp-rotate,
  body.studiolab #lab-footstrip .vp-exposure { min-height: 38px; }
  body.studiolab .vp-specnav .spec-name { letter-spacing: .12em; }
  body.studiolab .stage-growth { top: 12px; }
  body.studiolab .vp-center-stack { top: 52px; }
}
@media (pointer: coarse) {
  body.studiolab .viewkey button { width: 40px; height: 40px; min-height: 40px; }
}

/* =========================================================================
   16k. THE 09-07 MEASURE PASS — rendered at 1440x900 and 390x844 signed in,
   and every rule below is a number that shot was wrong about.
   ========================================================================= */

/* ★ A STATUS THAT ALWAYS SAYS OK IS NOT A STATUS. The mesh chip read
   "TYRANNOSAURUS: READY" for the whole session, 206px wide at the top-left of
   the stage - where it also ran INTO the growth pill ("ROWTH STAGE", measured
   at 1440). It speaks while the specimen is loading and while it has failed,
   which is when a player needs it; the rest of the time the animal on the
   stage is the readier answer. `.online` is studiolab.js's own word for
   READY (wireInstruments), so this cannot drift from the state it hides. */
body.studiolab .vp-mesh-status { width: auto; max-width: 260px; }
body.studiolab .vp-mesh-status.online { display: none; }
body.studiolab .vp-readout { top: 18px; }
body.studiolab .vp-autorotate { top: 58px; }

/* ★ THE FOOT IS THREE FIXED SEATS ON ONE LINE, and none of them can reach
   another. The hint LEFT, the clip keys CENTRED and capped so they can never
   grow into either neighbour, the view keys RIGHT - all at the same height,
   with the tool rail on its own line underneath. */
body.studiolab .vp-rot {
  left: 18px; right: auto;
  bottom: calc(var(--lab-bottom, 18px) + 64px);
}
body.studiolab .stage-anim { max-width: calc(100% - 400px); }
body.studiolab .vp-viewkeys { bottom: calc(var(--lab-bottom, 18px) + 58px); }

/* ★ THE RAIL MAY TAKE A SECOND LINE. It carries eleven controls now and a
   `nowrap` stack simply ran off the end of a 795px stage - the exposure
   slider was cut by the panel's edge at 1440. Wrapping is what the tab row
   and the clip row already do for the same reason: a control that will not
   fit beside its neighbours takes the next line WHOLE. */
body.studiolab .lab-rail-stack { flex-wrap: wrap; row-gap: 8px; max-width: 100%; }
body.studiolab .rail-colors { gap: 5px; }
body.studiolab .rail-action { padding: 7px 10px; font-size: 10px; }
body.studiolab .vp-rotate input[type="range"],
body.studiolab .vp-exposure input[type="range"] { width: 62px; }

/* ★ FOUR DOORS, ONE LINE. At .18em "SAVED DESIGNS" alone measured past its
   quarter of a 400px rail and the row took a second line with one tab
   stranded on it. The row may still wrap - that law does not move - but at
   this tracking it does not have to. */
body.studiolab #lab-tabs .lab-tab {
  font-size: 10.5px; letter-spacing: .1em; padding: 14px 4px 12px;
}

/* ★ THE SPECIES NAME IS THE ROW. The plate and the chevron took 67px out of
   a 246px rail and "Pachycephalosaurus" came back as "Pachyceph...". The rail
   buys the width back from the stage (which has a whole photograph to spare),
   the plate loses 4px, and the DIET is the one that yields: it is a
   scan-time nicety, and the nameplate says it in full the moment a row is
   picked. */
@media (min-width: 1024px) {
  body.studiolab { --av-species-w: 272px; }
}
body.studiolab .deck-row .dr-thumb { width: 26px; height: 26px; font-size: 12px; }
body.studiolab .deck-row { gap: 9px; padding: 7px 24px 7px 7px; }
body.studiolab .deck-row b { font-size: 12px; }
body.studiolab .deck-row span { font-size: 9px; letter-spacing: .12em; }

/* ★ THE COLUMN ENDS ABOVE THE FOLD AT 1440x900. Measured: the apply key sat
   below the deck's own scroll at the commonest laptop size this fleet sees,
   which is the one control on the page that must never need finding. Every
   number here is a few pixels off a seat that had them to give. */
@media (min-width: 901px) {
  body.studiolab .deck > .deck-row-l { padding: 12px 16px; }
  body.studiolab .deck-row-l.deck-head { padding-top: 12px; padding-bottom: 12px; gap: 11px; }
  body.studiolab #design-name { height: 36px; }
  body.studiolab .channel { min-height: 48px; padding: 8px 10px; }
  body.studiolab #channel-list { gap: 7px; }
  body.studiolab .deck-row-l.deck-actions { padding: 12px 16px; gap: 7px; }
  body.studiolab .deck-actions .btn { min-height: 38px; }
  body.studiolab .deck-actions .deck-apply { min-height: 44px; }
  body.studiolab #sex-choice .sex-choice-btn { min-height: 34px; }
}

/* ★ 16L. THE TWO FINE SLIDERS GO BACK BESIDE THE INSTRUMENTS (measured at
   1440: eleven controls on one bar wrapped the rail onto a second line, and
   the second line covered the orbit hint and the scene cycler under it). The
   rail is the reference's four groups and nothing else; the turntable and the
   light live with the readout and the turntable key, one small row under
   them - and they now travel to the phone's foot strip instead of vanishing
   at 900px. */
body.studiolab .vp-tools {
  position: absolute; left: 18px; top: 98px; z-index: 6;
  display: flex; align-items: center; gap: 6px; pointer-events: auto;
}
body.studiolab .vp-rotate, body.studiolab .vp-exposure {
  padding: 6px 11px; border-radius: 999px;
  background: color-mix(in srgb, var(--av-well) 78%, transparent);
  border: 1px solid color-mix(in srgb, var(--av-ink) 13%, transparent);
  box-shadow: 0 16px 34px -24px var(--av-shadow);
}
body.studiolab .vp-rotate input[type="range"],
body.studiolab .vp-exposure input[type="range"] { width: 58px; }
body.studiolab .lab-rail-stack { flex-wrap: nowrap; }
body.studiolab .rail-action { padding: 8px 12px; font-size: 10.5px; }

/* ★ 16m. THE TOP-LEFT CORNER IS A CONTROL, NOT A NOTICEBOARD (measured at
   1440: "TYRANNOSAURUS: NO MODEL" ran straight through the growth pill, and
   in the ordinary case the same corner carried two readings that both said
   the render was fine). The two CONTROLS take the top; the two READINGS sit
   under them and each one hides itself when it has nothing to report. */
body.studiolab .vp-autorotate { top: 18px; }
body.studiolab .vp-tools { top: 58px; }
body.studiolab .vp-readout { top: 100px; }
body.studiolab .vp-mesh-status { top: 140px; left: 18px; max-width: 300px; }
/* studiolab.js writes the render state onto the reading itself; at rest the
   well goes, exactly as the mesh chip does. */
body.studiolab .vp-readout:has(.v[data-state="READY"]) { display: none; }
/* the orbit hint lies over whatever the photograph put at the foot of the
   frame, so it carries its own contrast rather than trusting the scrim */
body.studiolab .vp-rot {
  text-shadow: 0 1px 3px var(--av-page-deep), 0 0 10px var(--av-page-deep);
  opacity: .95;
}

/* ★ 16n. THE FRAME HAS FOUR CORNERS AND EACH ONE HOLDS ONE THING (measured at
   1440 with the nav back on one line: the turntable pill and the two sliders
   at top-left ran into the nameplate's first letter, because a 38px tracked
   species name centred in a 768px stage reaches 204px from each edge and the
   instrument column was 250px wide. Two absolutely-positioned neighbours that
   both grow with their content will always find each other eventually; the
   cure is to stop seating them on the same line.)

     TOP-LEFT     the two READINGS, and both hide themselves at rest - so the
                  ordinary studio has an empty corner, as the reference does
     TOP-CENTRE   growth, then the nameplate
     TOP-RIGHT    the orbit hint
     FOOT ROW 2   the view CONTROLS left, the clip keys centred, the scene and
                  the light right
     FOOT ROW 1   the tool rail
   ------------------------------------------------------------------------- */
body.studiolab .vp-readout { top: 18px; }
body.studiolab .vp-mesh-status { top: 58px; }

/* the hint takes the one corner nothing else wants */
body.studiolab .vp-rot {
  left: auto; right: 18px; top: 20px; bottom: auto;
}

/* the view controls: the turntable key and the two sliders it refines, one
   cluster at the left of the same row the scene keys sit at the right of */
body.studiolab .vp-autorotate {
  left: 18px; top: auto; bottom: calc(var(--lab-bottom, 18px) + 58px);
  font-size: 10px; letter-spacing: .16em; padding: 8px 13px;
}
body.studiolab .vp-tools {
  left: 172px; top: auto; bottom: calc(var(--lab-bottom, 18px) + 58px);
}
body.studiolab .vp-rotate, body.studiolab .vp-exposure { padding: 5px 10px; }
/* ...and the clip keys keep clear of both */
body.studiolab .stage-anim { max-width: calc(100% - 600px); }

/* ★ 16o. THE CLIP KEYS TAKE THEIR OWN TIER. Squeezed onto the controls row
   they had `calc(100% - 600px)` of a 768px stage to live in - 168px, which is
   two pills and then a wrap for every species that ships four. They are the
   one row whose width is the SPECIES' business rather than ours, so they get
   the whole width and a line of their own above it, which is also where the
   reference stacks them. */
body.studiolab .stage-anim {
  bottom: calc(var(--lab-bottom, 18px) + 108px);
  max-width: calc(100% - 48px);
}

/* ★ 16p. THE READINGS CANNOT REACH THE NAMEPLATE. Both hide themselves at
   rest, so this is the FAILURE frame - and a failure frame is exactly when a
   player needs to read both of them. Capped so the left column ends before a
   centred nameplate can begin at any stage width this layout produces, and
   the chip may take a second line rather than lose a word. */
body.studiolab .vp-readout, body.studiolab .vp-mesh-status { max-width: 168px; }
body.studiolab .vp-mesh-status {
  white-space: normal; line-height: 1.35; font-size: 9.5px;
}

@media (max-width: 900px) {
  /* the scene keys and the sliders ride the rail's sheet here (studiolab.js
     moves them); inside it they are full-width rows, not floating pills */
  body.studiolab #lab-rail .vp-viewkeys,
  body.studiolab #lab-rail .vp-tools {
    position: static; bottom: auto; right: auto; left: auto; top: auto;
    flex: 1 1 100%; justify-content: center;
  }
  body.studiolab #lab-rail .vp-rotate,
  body.studiolab #lab-rail .vp-exposure { flex: 1 1 0; min-width: 0; }
  body.studiolab #lab-rail .vp-rotate input[type="range"],
  body.studiolab #lab-rail .vp-exposure input[type="range"] {
    width: auto; flex: 1 1 auto; min-width: 0;
  }
  /* the stack is a wrapping sheet at this width, not the desktop's one line */
  body.studiolab #lab-rail.expanded .lab-rail-stack { flex-wrap: wrap; }
}
