/* =============================================================================
   _studio.css — the skin studio's own surfaces
   

   _term.css owns the world: the ground, the frame, the type scale, the ink
   stacks, the panel, the table, the button. NOTHING in here restates a token
   it already declares, and nothing in here invents a colour. This sheet adds
   only the surfaces the studio needs and could not borrow.

   ---------------------------------------------------------------------------
   WHAT HE SAID ABOUT THE FIRST VERSION, AND WHAT EACH LINE OF IT BOUGHT
   ---------------------------------------------------------------------------
   "looks so bad"           -> the stage was the problem and the stage is fixed
                               in skinstage.mjs, not here: a lit 9-unit floor
                               disc with its fog switched off rendered as a
                               flat mid-grey plate across two thirds of the
                               frame, and the animal covered 37% of the height.
                               The page was framing a grey rectangle.
   "whole skin studio is
    too wide"               -> the workbench was full-bleed: 1,870px of rails
                               at 1920. It is capped at 1480 and centred now,
                               so a channel row and a species name are the
                               same reach apart as they are on a laptop.
   "species are too packed
    together"               -> the deck was 8px rows at a 1px gap, twenty of
                               them, in a 302px scroller. Rows breathe now and
                               the deck is the tallest thing in its rail.
   "make all this together" -> CHANNELS and MIXER were two framed panels
                               describing one job. One panel, one frame.
   "remove all this"        -> the STAGE backdrop chips, A/B, the "SEVEN
                               CHANNELS - RAW sRGB" footer and the WEARING IT
                               panel are gone from the markup entirely, not
                               hidden. Their rules are gone from here too.
   "add glitch skin tab"    -> the frame those two panels were spending on
                               separating themselves paid for the tab strip.

   THE LAWS THIS SHEET KEEPS (from the terminal system, not negotiable)
     1. No border-radius. Every corner on this site is square, and one rounded
        box reads as a foreign control dropped into a terminal.
     2. Any text that can sit over the bright plate carries an ink stack
        (--sh-micro / --sh-body / --sh-lead). The stack is a STROKE, not a glow:
        its only geometry is the glyph, so it can never read as a rectangle.
     3. Green is ONLINE-class state and nothing else. Amber is caution, red is
        a refusal, and every other meaning is blue at one of its three levels.
     4. The smaller the type, the wider the tracking. 12px labels ride .10em.
     5. One authored moment of motion per surface, .14s-.2s, and nothing that
        loops. The one exception on this page is the stage's blue mote layer,
        
   ========================================================================= */


/* ---------- the head strip -----------------------------------------------

   The first build opened with three boot lines, a 30px heading and a
   three-line paragraph: 380px of reading before the first control, on a
   surface whose whole job is a control. A tool opens on its work. The
   heading, the one-line claim and the wire's status now share one baseline. */

.wbhead{display:flex;align-items:baseline;gap:14px clamp(14px,2vw,28px);
  flex-wrap:wrap;padding:clamp(13px,1.8vw,20px) var(--pad) clamp(11px,1.5vw,15px);
  max-width:var(--wb-max);margin:0 auto;position:relative;z-index:1}
.wbhead .h2{margin:0;font-size:clamp(19px,2vw,26px)}
/* 74ch, not the sheet's usual 70: this line is ONE sentence sharing a baseline
   with a heading and a status, and a measure that wraps it to two lines turns
   a strip back into the block this rebuild just removed. */
.wbhead .sub{margin:0;flex:1 1 24ch;min-width:0;font-size:13.5px;max-width:74ch}
/* This line has no panel under it - it sits straight on the plate, over the
   lit half - so it cannot be rescued by a ground and takes the brighter ink
   instead. Measured 2.90:1 at --blue-dim. */
.wbhead .boot{margin:0;flex:none;font-size:12px;letter-spacing:.10em;
  color:var(--blue);text-shadow:var(--sh-body)}
.wbhead .boot .ok{color:var(--green)}


/* ---------- the workbench ------------------------------------------------

   Three rails. The stage is the centre and gets the width, because the animal
   is the thing being judged; the two rails are instruments around it.

   ★ THE CAP. --wb-max is the whole answer to "too wide". Full-bleed rails on
     a 1920 monitor put the species filter 1,600px from the hex field that
     acts on it, and a studio is used with BOTH hands on one animal. 1480 is
     the width at which the stage still reads at ~700px and the eye can cross
     the instrument without leaving the animal. It is one token, in one place,
     because a max-width scattered across four rules drifts within a week. */

:root{--wb-max:1480px}

.wb{display:grid;grid-template-columns:268px minmax(0,1fr) 392px;
  gap:var(--pad);padding:0 var(--pad) var(--pad);align-items:start;
  max-width:var(--wb-max);margin:0 auto}
.rail{display:grid;gap:var(--pad);min-width:0}

@media (max-width:1360px){
  /* The right rail keeps its full width when the layout drops to two columns.
     Narrowing it here is what wrapped every channel row onto two lines at
     1100px on the first build - the rail was thinner, not the row shorter. */
  .wb{grid-template-columns:minmax(0,1fr) 392px}
  /* ★ align-items:start, because a grid ROW stretches its items to the tallest
     one by default. Side by side, SPECIES is a 23-name deck and LIBRARY on a
     first run holds one sentence, so the library was drawn as a 470px frame
     around 40px of text - the largest empty box on the page, at the width
     where the page has least room to waste. Each panel is its own height. */
  .rail-l{grid-column:1 / -1;grid-template-columns:1fr 1fr;display:grid;
    align-items:start}
}
@media (max-width:960px){
  .wb{grid-template-columns:minmax(0,1fr)}
  .rail-l{grid-template-columns:1fr}
}

/* A panel in a rail must be able to shrink; the default min-width:auto on a
   grid item is what lets a wide <pre> push a whole column off the screen. */
.wb .panel{min-width:0}

/* ★ A PANEL HEADER IN A 268px RAIL IS NOT THE ONE _term.css SIZED.
     Its own 16px title and 13px meta are drawn for the home page's 1.12fr
     columns. In the species rail they measure 117 + 127 + 12 = 256 inside 242,
     and flex resolves the overflow by wrapping the TITLE - which is how the
     first build shipped a panel captioned "[ SPECIES" with a lone "]" on the
     line under it. The title never wraps; the numbers give up the pixels. */
.wb .panel .ph{flex-wrap:nowrap}
.wb .panel .ph h2{white-space:nowrap;font-size:15px}
.wb .panel .ph .meta{font-size:11.5px;min-width:0}

/* ★ HIERARCHY. Eight panels wearing one identical frame is eight things of
   equal importance, which is the same as none. The specimen is what this page
   is for: it keeps the full frame and the lit spine. Every instrument around
   it drops to a hairline spine and a dimmer border, so the eye lands on the
   animal first and finds the tools when it goes looking. */
/* ★ CONTRAST IS FIXED AT THE GROUND, NOT AT THE INK.
   Six elements measured below 4.5:1 against the REAL pixels behind them -
   worst "MOUNTED: 21" at 2.84:1 and the wire status line at 2.90:1 - and the
   offender was never the colour. --blue-dim is 5.64:1 on pure black and
   passes; the alley PHOTOGRAPH is what pushes it under, and the brightest
   tile of that photograph sits exactly behind the species header and the head
   strip. Raising the panel's own ground from .62 to .80 lifts every glyph in
   every panel at once, without touching one authored colour - and it also
   stops the neon signage blazing through a list of species names. */
.wb .panel{border-color:rgba(30,111,224,.62);
  background:rgba(1,4,10,.80);
  box-shadow:inset 2px 0 0 rgba(75,180,252,.30)}
.wb .pnl-stage{border-color:var(--rule-hot);
  box-shadow:inset 3px 0 0 var(--rule-hot),0 0 60px -30px rgba(61,155,255,.8)}


/* ---------- small controls shared across the studio ---------------------- */

/* .act is _term.css's button. .sm is the studio's dense variant: same world,
   same corner brackets on hover, less room, because a toolbar of full-size
   buttons would out-shout the animal. */
.act.sm{padding:6px 10px;font-size:11px;letter-spacing:.10em}
.act.sm[disabled],.act.sm:disabled{opacity:.38;pointer-events:none}
.act.sm[aria-pressed="true"],.act.sm.on{color:#A8E7FF;border-color:#79D9FF;
  background:rgba(30,111,224,.20);box-shadow:inset 0 -2px 0 var(--blue-hot)}

/* A labelled row: the label is a 12px key, the control takes the rest. */
.row{display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding:9px 0;border-top:1px solid rgba(30,111,224,.30)}
.row:first-of-type{border-top:0}
.row>.k{flex:none;min-width:78px}

.tgrp{display:flex;align-items:center;gap:6px;flex-wrap:wrap;min-width:0}
.tgrp>.k{flex:none;margin-right:2px}
.tgrp-r{margin-left:auto}
/* A caption with nothing after it is a promise the page did not keep. When a
   species' motion file is refused by the rig gate there are no clip buttons,
   and the label has to say what you are looking at instead of trailing off. */
#clip-bar>.k:only-child::after{content:" [ STILL ]";color:var(--amber)}

/* A sub-heading INSIDE a panel. It is a rule with a word sitting on it rather
   than a second <h2>, because a panel has one title and everything under it
   is the same panel - two headings of equal weight is the two-panel problem
   this rebuild just removed, drawn back in with type. */
.sub-h{display:flex;align-items:center;gap:10px;margin:15px 0 3px}
.sub-h .hr{flex:1;height:0;border-top:1px solid rgba(30,111,224,.34)}

/* A text field in the terminal world is a hairline box with a lit caret. */
.fld{display:flex;align-items:center;gap:9px;padding:8px 0}
.fld>.k{flex:none}
input[type="text"],input[type="search"]{
  flex:1;min-width:0;background:rgba(1,4,10,.55);color:#79D9FF;
  border:1px solid rgba(30,111,224,.55);padding:7px 9px;
  font-family:var(--mono);font-size:12px;letter-spacing:.08em;
  text-shadow:var(--sh-micro);
  transition:border-color .15s linear,background .15s linear}
input[type="text"]::placeholder{color:var(--blue-dim);opacity:.72;letter-spacing:.10em}
input[type="text"]:focus{outline:none;border-color:var(--blue-hot);
  background:rgba(1,4,10,.78)}

/* The range input is drawn by hand so it is a terminal readout, not a pill. */
:root{
  /* THE SLIDER ROW'S GEOMETRY, DECLARED ONCE.
     The band ruler under the growth rail has to line up with the TRACK, and
     the track is what is left of the row after the label, the readout and two
     gaps. When those three numbers were written out separately, the ruler was
     measured 141px wider than the track it describes - so its four quarters
     each ran ~35px long and it told the player ADULT begins at 97-103% growth
     when the game's own edge is 75%. At 125% browser zoom it claimed 126%.
     A readout that lies about the thing under it is worse than no readout.
     ★ --sl-ua is Chrome's UA `margin:2px` on input[type=range]. It is real,
       it is 2px each side, and it is why "89px" measured 91. */
  --sl-k: 78px;    /* the "> GROWTH" label */
  --sl-v: 126px;   /* the "100% - ADULT" readout */
  --sl-gap: 11px;  /* the row's own gap */
  --sl-ua: 2px;    /* the UA margin on the range input */
}
.slider{display:flex;align-items:center;gap:var(--sl-gap);padding:10px 0;
  border-top:1px solid rgba(30,111,224,.30)}
.slider>.k{flex:none;min-width:var(--sl-k)}
.slider>.val{flex:none;font-size:12px;letter-spacing:.10em;color:#79D9FF;
  text-shadow:var(--sh-micro);min-width:var(--sl-v);text-align:right}
input[type="range"]{flex:1;min-width:0;-webkit-appearance:none;appearance:none;
  height:16px;background:transparent;cursor:ew-resize}
input[type="range"]::-webkit-slider-runnable-track{height:3px;
  background:linear-gradient(90deg,rgba(75,180,252,.55),rgba(30,111,224,.30));
  border:0}
input[type="range"]::-moz-range-track{height:3px;
  background:linear-gradient(90deg,rgba(75,180,252,.55),rgba(30,111,224,.30))}
input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
  width:5px;height:16px;margin-top:-7px;background:var(--blue-hot);border:0;
  box-shadow:0 0 0 1px rgba(1,5,11,.9),0 0 10px -2px rgba(61,155,255,.9)}
input[type="range"]::-moz-range-thumb{width:5px;height:16px;border:0;border-radius:0;
  background:var(--blue-hot);box-shadow:0 0 0 1px rgba(1,5,11,.9)}
input[type="range"]:focus-visible{outline:1px solid var(--blue-hot);outline-offset:4px}

/* Chips: the pattern letters, the library tags, the glitch families. Square,
   hairline, bracketed by their own text rather than by pseudo-elements, so a
   chip is one node. */
.chips{display:flex;flex-wrap:wrap;gap:6px;min-width:0}
.chip{font-family:var(--wide);font-size:10.5px;letter-spacing:.14em;
  padding:3px 8px;border:1px solid rgba(30,111,224,.55);color:var(--blue-dim);
  background:transparent;cursor:pointer;text-shadow:var(--sh-micro);
  transition:color .14s linear,border-color .14s linear,background .14s linear}
.chip:hover{color:#79D9FF;border-color:#79D9FF}
.chip[aria-pressed="true"],.chip.on{color:var(--blue-hot);border-color:var(--blue-hot);
  background:rgba(30,111,224,.20);box-shadow:inset 0 -2px 0 var(--blue-hot)}

/* The ticker ships `.g` for green and nothing else, and green on this site is
   ONLINE-class state - "[ BY DESIGN ]", "[ THE GAME'S OWN CURVE ]", things that
   are correct. "[ NOT IN THIS BUILD ]" is an ABSENCE, which is amber's job, and
   painting it green would be the page telling a player a missing lane is a
   healthy one. One class, in the studio's own sheet, restating no token. */
.ticker .tape .w{color:var(--amber)}

/* ★ TWO TAB STOPS LANDED OFF-SCREEN. The shell is position:fixed and stays
   display:flex when closed, so its [ CLOSE ] button (y=1095) and its command
   input (y=1468) kept their place in the tab order below a 1080px viewport -
   tab off the end of the studio and focus vanishes into a panel that is not on
   screen. visibility:hidden removes an element from the tab order without
   touching the display the open/close transition depends on. */
.con:not(.on){visibility:hidden}
.con.on{visibility:visible}

.hint{font-size:12px;letter-spacing:.10em;color:var(--blue-dim);
  text-shadow:var(--sh-micro);line-height:1.7;margin:0}
.hint.warn{color:var(--amber)}
.hint.bad{color:var(--red)}
.hint.good{color:var(--green)}

/* Any scroller inside a panel gets a solid floor. The plate behind this site
   is a lit neon alley, and a .62-alpha panel over its bright half leaves a
   list of names sitting on a photograph of a window. The frame stays
   translucent - the CONTENT does not. */
.scroll{background:rgba(1,4,10,.55)}


/* ---------- the species deck --------------------------------------------

   
     packed twenty 8px rows two-across into a 302px window on the argument
     that all twenty should be visible without scrolling. That argument was
     wrong in the way density arguments usually are: it optimised for SEEING
     the list and made READING it work. Picking your animal is the first thing
     you do on this page and the one thing a new player does slowly.

     What changed: one column instead of two, so a name is never truncated and
     the eye tracks straight down; 11px of vertical padding instead of 8; a
     3px gutter instead of a 1px hairline, so rows are separate objects rather
     than a striped block; and a taller window, because the deck is now the
     tallest thing in its rail and it should be. Scrolling a list you can read
     beats scanning a block you cannot. */

/* ★ THE LEFT RAIL WAS THE TALLEST COLUMN ON THE PAGE and nothing in it was
   the reason to be here. Measured at 1440x900 after the right rail was fixed:
   left 946px, centre 751px, right 623px - so the page ran to 1235px and the
   bottom third of it was a species list scrolling beside 450px of empty
   background. A deck at 52vh is 430px of names on a 900px screen, which is
   more list than the animal it is a list of. At 40vh it is 360px, the rail
   comes down to ~800, and the three columns end within one panel of each
   other. Tall screens still get the full 430 because the cap did not move. */
.deck-wrap{max-height:min(40vh,430px);overflow-y:auto;padding:2px}
.deck{display:grid;grid-template-columns:1fr;gap:3px}
.deck-row{display:flex;align-items:center;gap:9px;min-width:0;
  padding:11px 12px;background:rgba(1,4,10,.72);
  border:1px solid rgba(30,111,224,.26);cursor:pointer;
  font-family:var(--mono);font-size:12.5px;letter-spacing:.08em;
  color:var(--blue);text-align:left;text-shadow:var(--sh-micro);
  transition:color .14s linear,background .14s linear,border-color .14s linear}
.deck-row>b{flex:1;min-width:0;font-weight:430;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.deck-row:hover{color:#79D9FF;background:rgba(30,111,224,.16);
  border-color:rgba(75,180,252,.5)}
.deck-row.on,.deck-row[aria-selected="true"]{color:var(--blue-hot);
  background:rgba(30,111,224,.26);border-color:var(--blue-hot);
  box-shadow:inset 3px 0 0 var(--blue-hot)}
/* A species with no model on disk is still listed, and says so, because a
   
   showed two of its twenty-five (2026-08-09). */
.deck-row.nomodel{color:var(--blue-dim);cursor:not-allowed}
.deck-row.nomodel::after{content:"NO MODEL";flex:none;font-size:10px;
  letter-spacing:.14em;color:var(--amber)}
/* The empty note is the deck's own last child and must not inherit a cell. */
#sp-deck>.hint{grid-column:1 / -1;background:rgba(1,4,10,.72);padding:11px 12px}

/* Two across again once the left rail is running as a wide two-column band -
   at that width a row has room to breathe AND to pair. */
@media (max-width:1360px) and (min-width:961px){
  .deck{grid-template-columns:1fr 1fr}
  .deck-wrap{max-height:340px}
}


/* ---------- the stage ----------------------------------------------------

   The animal stands in a hairline frame with the same corner ticks the panels
   wear. The ground under it is drawn by the renderer, never photographed. */

.pnl-stage .stagebox{position:relative;width:100%;
  aspect-ratio:16/10;min-height:360px;
  border:1px solid var(--rule);background:
    radial-gradient(120% 80% at 50% 108%,rgba(30,111,224,.20),transparent 62%),
    rgba(1,4,10,.72);
  overflow:hidden;touch-action:none}
.pnl-stage .stagebox canvas{display:block;width:100%;height:100%}

/* The four corner ticks. They are the panel's own idiom borrowed inward so
   the stage reads as part of the instrument and not as an embedded video. */
.stagebox::before,.stagebox::after{content:"";position:absolute;width:11px;height:11px;
  border:1px solid var(--rule-hot);pointer-events:none;z-index:3}
.stagebox::before{top:-1px;left:-1px;border-right:0;border-bottom:0}
.stagebox::after{bottom:-1px;right:-1px;border-left:0;border-top:0}

.stagegate{position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;text-align:center;padding:var(--pad);z-index:4;
  background:rgba(1,4,10,.62)}
.stagegate>.lbl{font-family:var(--wide);font-size:12px;letter-spacing:.16em;
  color:var(--blue-dim);text-shadow:var(--sh-body)}
.stagegate[hidden]{display:none}

.stagehud{position:absolute;top:9px;left:9px;display:flex;gap:6px;z-index:3;
  pointer-events:none}
.stagehud .tag{font-family:var(--wide);font-size:10.5px;letter-spacing:.16em;
  color:var(--blue-dim);border:1px solid rgba(30,111,224,.5);
  background:rgba(1,4,10,.66);padding:2px 7px;text-shadow:var(--sh-micro)}
.stagehud .tag[hidden]{display:none}

/* The pack meter used to be a panel footer under the stage, on a line that
   
   that line. The meter is the half of it that was doing work - it is the only
   thing that says a 5MB mesh is still arriving - so it moved INTO the frame
   it describes, bottom-left, where a loading readout belongs. */
.stageload{position:absolute;left:9px;bottom:9px;z-index:3;pointer-events:none;
  display:flex;align-items:center;gap:8px;
  font-size:11px;letter-spacing:.10em;color:var(--blue-dim);
  text-shadow:var(--sh-micro);opacity:1;transition:opacity .3s linear}
/* Silence when there is nothing to say: a meter reading 100% forever is a
   permanent green light that teaches the eye to stop reading it. */
.stageload.done{opacity:0}

/* While the model is being picked at, the cursor says so. */
.stagebox.pickable{cursor:crosshair}

/* ★ THE STAGE TOOLBAR IS TWO ROWS BECAUSE IT IS TWO KINDS OF THING.
     It shipped as one flex line holding a MOTION label, six clip chips and
     two buttons, and at 702px they did not fit, so flex-wrap put the buttons
     on a second line ANYWAY - by accident, hard against the right edge, with
     nothing to say the break was meant. Nine controls at one weight is a
     toolbar the eye has to read left to right every time.

     Now the break is deliberate and it falls where the meaning changes.
     MOTION is a SET OF STATES: exactly one clip is playing and picking
     another swaps it. The row under it is three ACTIONS: roll the colours,
     start the turntable, take the picture. A hairline between them costs 9px
     and tells the eye which half it is in. */
.tools{display:grid;gap:9px;
  padding:11px 0 1px;border-top:1px solid rgba(30,111,224,.30);margin-top:11px}
/* The actions sit at the trailing edge, under the growth readout they line up
   with. .tgrp-r's margin-left:auto is a FLEX ROW idiom and this is a grid
   column now, where it would centre the group instead of ending it. */
.tools .tgrp-r{margin-left:0;justify-content:flex-end;
  padding-top:9px;border-top:1px solid rgba(30,111,224,.30)}

/* ★ RANDOMISE IS THE LOUD ONE, AND IT SAYS SO THE WAY THIS THEME SAYS IT.
     Every other control on this bar changes one thing: which clip, spinning or
     not, take a frame. Randomise rewrites all seven colours at once, which is
     the largest single act on the page, and a player is entitled to see that
     coming before they press it.
     The mark is the LIT SPINE - the same idiom the specimen panel wears to say
     "this is the one that matters" - and not a new colour. Amber is caution,
     red is refusal, green is ONLINE; a fourth meaning invented for a button
     would break the only colour rule this site has. It is deliberately NOT the
     filled ground that .act.sm[aria-pressed] uses, because that ground already
     means "this toggle is currently ON" and TURNTABLE is standing right next
     to it wearing exactly that state. A 3px spine at 2px of extra inset is
     visible at a glance and collides with nothing. */
/* ★ box-shadow REPLACES, it does not add. .act carries two of its own - the
   ink stack's outer and inner dark rings - and a bare `box-shadow:inset 3px`
   here silently deleted both, so the one button being marked as important was
   also the only button on the page without the stroke every other button
   wears. Measured: .act computes
   `rgba(1,5,11,.9) 0 0 0 1px, rgba(1,5,11,.7) 0 0 0 1px inset`. Both are
   restated below the spine so the mark ADDS to the idiom instead of trading
   against it. */
.act.sm.roll{border-color:rgba(75,180,252,.62);padding-left:13px;
  box-shadow:0 0 0 1px rgba(1,5,11,.9),inset 0 0 0 1px rgba(1,5,11,.7),
             inset 3px 0 0 var(--blue-hot)}
.act.sm.roll:hover{border-color:#79D9FF;
  box-shadow:0 0 0 1px rgba(1,5,11,.9),inset 0 0 0 1px rgba(1,5,11,.7),
             inset 3px 0 0 #79D9FF}
.act.sm.roll:active{background:rgba(30,111,224,.24)}

/* ★ THE PLAYING CLIP WAS BARELY DISTINGUISHABLE from the five that were not.
   The shared .chip.on is a dim-blue chip lifting to hot blue, which is enough
   in a row of tags and not enough in a row of six identical words where the
   answer to "which one is running" has to be readable at a glance. The active
   clip gets a filled ground and near-white ink. No layout changes with the
   state - a marker glyph that appears on selection would reflow the whole row
   under the cursor every time a clip is picked. */
#clip-bar .chip.on,#clip-bar .chip[aria-pressed="true"]{
  color:#CFF1FF;background:rgba(30,111,224,.34);border-color:#79D9FF;
  box-shadow:inset 0 -2px 0 var(--blue-hot)}

/* The four growth bands, drawn as a ruler under the rail they belong to. The
   words are equal quarters because the game's own edges ARE quarters: 0-25
   hatchling, 25-50 juvenile, 50-75 sub-adult, 75-100 adult. If those edges
   ever stop being quarters this becomes a lie, so it is written here once and
   the words it prints are the same four GROWTH_BANDS carries in JS. */
/* ★ minmax(0,1fr), NOT 1fr. `1fr` is `minmax(AUTO,1fr)`, so a column can never
   shrink below its own label's min-content width - and at 412px "HATCHLING"
   does not fit in a quarter of a 203px rail. The four columns silently stopped
   being quarters and the ruler measured 0 / 34.1 / 65.3 / 86.1 instead of
   0 / 25 / 50 / 75: it told a player that ADULT begins at 86% growth where the
   game's own edge is 75%, which is the SAME lie the hardcoded margin used to
   tell, arriving by a different route. The one thing this element exists to do
   is be four equal quarters, so it is now unable to be anything else, and the
   type shrinks to fit them below 430px rather than the quarters stretching to
   fit the type. */
.bands{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:0;
  list-style:none;margin:0;padding:0 0 2px;
  /* ★ THE RULER IS THE WIDTH OF THE TRACK, NOT OF THE ROW.
     It used to start in the right place and then run the full row width - so
     it was measured 141px WIDER than the track it describes (the readout's
     126px + one 11px gap + the UA's 2x2px), its four quarters each ran ~35px
     long, and it told the player ADULT begins at 97-103% growth where the
     game's own edge is 75%. At 125% browser zoom it claimed 126%, drawn past
     the end of the track. Both margins are now derived from the same tokens
     the row itself uses, so the two cannot drift apart again. */
  margin-right:calc(var(--sl-v) + var(--sl-gap) + var(--sl-ua));
  margin-left:calc(var(--sl-k) + var(--sl-gap) + var(--sl-ua))}
.bands li{position:relative;font-family:var(--wide);font-size:9.5px;
  letter-spacing:.12em;color:var(--blue-dim);opacity:.75;
  text-shadow:var(--sh-micro);padding-left:6px;
  border-left:1px solid rgba(30,111,224,.42)}
.bands li:first-child{padding-left:5px}


/* ---------- the right rail: one panel, two tabs -------------------------- */

.pnl-paint{min-height:0}
.pnl-paint>.ph{align-items:flex-end;gap:10px;margin-bottom:9px}

.tabs{display:flex;align-items:flex-end;gap:2px;min-width:0}
.tab{font-family:var(--wide);font-size:14px;font-weight:560;letter-spacing:.06em;
  color:var(--blue-dim);background:transparent;border:0;padding:1px 9px 7px;
  cursor:pointer;text-shadow:var(--sh-body);position:relative;
  transition:color .14s linear}
.tab:first-child{padding-left:0}
.tab:hover{color:#79D9FF}
/* The lit underline is the whole selected state. A filled tab would need a
   ground colour, and a ground colour on a tab strip that sits ON the panel
   header is a second panel drawn inside the first. */
.tab::after{content:"";position:absolute;left:0;right:0;bottom:0;height:2px;
  background:var(--blue-hot);transform:scaleX(0);transform-origin:left;
  transition:transform .16s cubic-bezier(.2,.8,.2,1)}
.tab:first-child::after{left:0}
.tab.on,.tab[aria-selected="true"]{color:var(--blue-hot)}
.tab.on::after,.tab[aria-selected="true"]::after{transform:scaleX(1)}
.tab:focus-visible{outline:1px solid var(--blue-hot);outline-offset:2px}

.pane{display:none;min-width:0}
.pane.on{display:block}
.pane[hidden]{display:none}
/* ★ The pane scrolls, the PAGE does not. This is the difference between an
     instrument and a document: the first build let seven channel rows, five
     harmony buttons and a mixer push the page to 1,843px at 1920, so the
     wire panel and the animal could never be on screen together. The rail is
     bounded by the viewport and its contents scroll inside it. */
.pane-scroll{max-height:min(64vh,600px);overflow-y:auto;padding-right:3px;
  position:relative}

/*
     shipped build.  He was describing this box, and the measurement agrees
     with him exactly. Measured in a browser at 1440x900 on the shipped
     geometry, with one channel open as it is on arrival:

       scrollHeight 733, clientHeight 540  ->  193px hidden

     193px is four channel rows. At 1920x1080 it was 113. And it was hidden
     while the rail UNDER the panel had 270px of nothing in it, which is the
     detail that makes it a defect rather than a trade-off: the page was not
     short of room, it was spending it in the wrong place.

     The cause was that ONE scroller held two different kinds of thing. The
     seven channels are a FIXED list - it is seven, it is always seven, and it
     is the only reason the rail exists - and under them sat the MIXER, six
     more rows of harmony, pattern, variation and source. Open a channel and
     its three H/S/L rails add 97px more. So the list you can count on one hand
     ended up behind a scrollbar, which (ministudio.css says this in as many
     words) reads as a broken box rather than as a scroller.

     The mixer is out of this scroller now - it is the <details> below - and
     what is left can no longer fill it:

       seven rows at 44px            308
       six gaps at 3px                18
       the one open row's H/S/L       97
                                     ---
                                      423   inside min(64vh,600px)

     At the 900px viewport height he was complaining about that is 423 inside
     576, so the box does not scroll, does not draw a scrollbar, and hides
     nothing. It stays bounded rather than becoming height:auto because a
     240px-tall browser window is still a browser window, and an unbounded
     rail there would push the undo/redo footer off the bottom of the screen
     with no way back to it. */

/* ★ AN AFFORDANCE FOR A SCROLL THAT CANNOT HAPPEN IS A LIE.
     The floor fade means "there is more below this line". The glitch pane
     always has more below the line - it is a catalogue - so it always wears
     it. The paint pane now only overflows when 64vh drops under its 423px of
     content, which is a viewport shorter than 661px, so that is the only place
     the fade is drawn. Painting it unconditionally would have put a permanent
     "keep scrolling" cue under a list that ends where it appears to end. */
#pane-paint .pane-scroll::after{display:none}
@media (max-height:660px){
  #pane-paint .pane-scroll::after{display:block}
}
/* ★ IT HID 109px OF PAINT AND 353px OF GLITCH - 36% of the pane - with no
   fade, no visible scrollbar and no affordance, so a row cut through its
   middle by the panel footer read as a rendering fault rather than as a
   scroller. A sticky gradient at the foot is the cheapest honest cue: it sits
   ON the scrolled content, moves with it, and hides nothing (the pane keeps
   its bottom padding so the last row clears it). */
.pane-scroll::after{content:"";display:block;position:sticky;bottom:-1px;
  height:26px;margin-top:-26px;pointer-events:none;
  background:linear-gradient(180deg,rgba(1,4,10,0),rgba(1,4,10,.92))}
.pane-scroll{padding-bottom:4px}


/* ---------- the channel rows --------------------------------------------

   Seven rows, each one part of the animal. The swatch is the loud element
   because colour is what the row is for; the hex field is the precise one.

   ★ ONE ROW OPEN AT A TIME. Hue/saturation/lightness are three unlabelled
     rails, and seven rows of them is twenty-one horizontal lines stacked in a
     392px column - which is what the first build shipped, and it read as
     static rather than as controls. They are not deleted, because H/S/L is
     how a colour is actually adjusted once it is roughly right. They belong
     to the row you are WORKING ON.

     The disclosure is bound to SELECTION, never to hover. A hover-expand on a
     stack of rows moves every row below the cursor the instant you arrive,
     so the control you were reaching for is somewhere else by the time you
     get there. Clicking a row opens it; clicking the animal opens the row for
     the part you clicked; tabbing into a row opens it too, so the keyboard
     never has to know the mouse rule.                                        */

.chans{display:grid;gap:3px;margin:2px 0 0}
/* ★ THE VERTICAL BUDGET, AND WHY ONLY THE VERTICAL MOVED.
     ministudio.css sizes the same list at a 38px row and says why: "SEVEN rows
     is a FIXED list and it must not scroll on any ordinary screen." Its row
     carries three controls; this one carries five, so it cannot be 38px, but
     it does not need to be 48 either. 7px of row padding and 6px inside the
     hex field puts the row at 44px, which takes the seven of them from 354px
     to 326px and buys 28px of headroom on a short laptop.

     The HORIZONTAL numbers are deliberately untouched. The width budget below
     is measured, it is one pixel from the edge, and flex-wrap does not
     negotiate: 30 chip + 84 name + 78 hex + 58 PICK + 58 LOCK + 4 gaps of 6 =
     332 inside 341. Nothing above changes a single one of those, so the
     arithmetic that was proven still holds and LOCK cannot drop to a second
     line. */
.ch{background:rgba(1,4,10,.7);border:1px solid rgba(30,111,224,.26);
  padding:7px 9px;cursor:pointer;
  transition:background .14s linear,border-color .14s linear}
.ch:hover{background:rgba(30,111,224,.10);border-color:rgba(75,180,252,.44)}
.ch.sel{background:rgba(30,111,224,.15);border-color:var(--blue-hot);
  box-shadow:inset 3px 0 0 var(--blue-hot);cursor:default}
.ch.hit{animation:chanflash .5s cubic-bezier(.2,.8,.2,1) 1}
@keyframes chanflash{from{background:rgba(75,180,252,.34)}to{background:rgba(30,111,224,.15)}}
/* A refused value marks its own row. Red is only ever a refusal here. */
.ch.bad{border-color:var(--red);box-shadow:inset 3px 0 0 var(--red)}
.ch.bad .ch-hex{border-color:var(--red);color:var(--red)}

/* The five controls have to hold ONE line. Their base widths plus the gaps
   were 358px inside a 354px row on the first build - four pixels over, and
   flex-wrap does not negotiate: it drops the last item to a second line and
   every channel row grows by 26px. The budget is written out here so the next
   edit knows what it is spending. The 392px rail pays 2x14px of panel padding,
   3px of scrollbar gutter, 2x1px of row border and 2x10px of row padding, so
   the row itself holds 341px:

     30 chip + 84 name + 78 hex + 58 PICK + 58 LOCK + 4 gaps of 6 = 332

   ★ It was 88 for the hex and the two buttons were measured at 56 rather than
     drawn: "> PICK" at 11px on .05em tracking with 6px of padding is 58, not
     56, and 30+84+88+58+58+24 is 342 inside 341. One pixel over, and flex-wrap
     does not negotiate - it drops LOCK to a second line and every one of the
     seven rows grows by 26px. The hex gave up the ten pixels because it had
     them: "#FFCD8D" needs 66 of its 78. */
/* ★★ THE ROW WRAPPED THE MOMENT YOU LOCKED IT, and the budget above never
      knew, because it costed a button that does not exist. `toggleLock` swaps
      the label to "> LOCKED", and measured in a browser at 11px on .05em:

        "> LOCK"    59.31px          "> LOCKED"  74.41px

      +15.10px into a row that had 0.38px spare - measured, unlocked:
      30.00 chip + 86.38 name + 78.00 hex + 59.31 PICK + 59.31 LOCK + 24.00
      gaps = 336.62 inside a REAL 337.00 of usable width (the donor's comment
      says 341; the row measures 337, which is the other half of why this was
      always going to tip). So every locked row wrapped LOCK onto a second
      line and grew from 44px to 76px. Seven locked rows is +224px, which put
      the seven channels straight back behind a scrollbar - the exact defect
      this rebuild exists to remove, reachable by pressing the button that is
      sitting in the row.

      Two changes, because one of them alone would only move the cliff:

      1. flex-wrap:NOWRAP. The row can no longer wrap at all, whatever any
         future label does. Its release valve is the NAME, which ellipsises -
         a name is recoverable from the swatch beside it and from its fixed
         position in a list of seven; a control on a second line is a 26px
         layout break in all seven rows at once. This is a structural
         guarantee, not a measurement that has to be re-proven every edit.
      2. The width is re-budgeted around the LONGER label, so the valve is
         never actually reached. The pixels come from the hex, which the donor
         itself says has them ("#FFCD8D needs 66 of its 78"), and from the
         gaps, and ministudio.css's --ms-hex-w:70px is the proven number:

           30 chip + 86 name + 70 hex + 60 PICK + 75 LOCK + 4 gaps of 4 = 337

         inside 341 (row padding 10 -> 9 pays the last two). 4px of headroom,
         with "> LOCKED" reserved at 75 rather than discovered at 74.41.
         Verified in a browser with all seven channels locked: every .ch-top
         still 28px, every row still 44px. */
.ch-top{display:flex;align-items:center;gap:4px;min-width:0;flex-wrap:nowrap}
/* UNDERBELLY is the longest label and it sets this floor. A key that reads
   "UNDERBE..." is a key the eye has to decode, which is the one thing a
   channel row cannot afford. */
.ch-name{flex:1 1 86px;min-width:0;font-family:var(--wide);font-size:11px;
  letter-spacing:.12em;color:#79D9FF;text-shadow:var(--sh-micro);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* The swatch: a native colour input wearing a square chip, so the operating
   system's own picker is one click away and nothing here re-implements it.
   The row publishes its colour as --sw, so the chip reads the truth even
   before the input paints its own well. */
.ch-chip{position:relative;flex:none;width:30px;height:22px;display:block;
  background:var(--sw,#000);
  border:1px solid rgba(75,180,252,.55);box-shadow:0 0 0 1px rgba(1,5,11,.9);
  overflow:hidden;cursor:pointer}
.ch-chip input[type="color"]{position:absolute;inset:-6px;
  width:calc(100% + 12px);height:calc(100% + 12px);
  border:0;padding:0;background:transparent;cursor:pointer;opacity:0}
.ch-chip::after{content:"";position:absolute;inset:0;pointer-events:none;
  box-shadow:inset 0 0 0 1px rgba(1,5,11,.55)}
/* ★ THE SWATCH HAD NO VISIBLE FOCUS AT ALL - measured, 0 pixels differed
   between focused and unfocused across the whole chip. The real control is an
   <input type=color> at opacity:0 inside an overflow:hidden box, so the
   browser's own ring was both invisible and clipped 9px on every side. The
   ring goes on the CHIP: an outline is painted outside the box and is not
   clipped by that box's own overflow. Seven swatches, keyboard-reachable and
   now keyboard-visible. */
.ch-chip:focus-within{outline:2px solid var(--blue-hot);outline-offset:2px}

/* Seven characters at this tracking. A hex field that clips its own value is
   a field that lies about the colour it is holding.

   ★ The selector is .ch-top .ch-hex and not .ch-hex, and that is the whole
     fix: the base rule above is `input[type="text"]`, which is an attribute
     selector plus a type and therefore OUTWEIGHS a bare class. Its `flex:1;
     min-width:0` was quietly beating `flex:none;width:92px`, so every field
     shrank to whatever was left and showed "#E08" for #E0883B. A dropped
     declaration is silent - nothing warns, and the value looks like a short
     colour rather than a clipped one. */
.ch-top .ch-hex{flex:none;width:70px;padding:6px 4px;letter-spacing:.02em;
  text-transform:uppercase;text-align:center}
/* Both buttons are sized to their own LONGEST label and never resize with
   their state: PICK never changes, LOCK becomes LOCKED, and a control that
   changes width in a row this tight is a defect in the label rather than in
   the budget. */
.ch-top .ch-eye{flex:none;width:60px;padding:5px 2px;letter-spacing:.05em}
.ch-top .ch-lock{flex:none;width:75px;padding:5px 2px;letter-spacing:.05em}
.ch-lock.on,.ch-lock[aria-pressed="true"]{color:var(--amber);border-color:var(--amber);
  background:transparent;box-shadow:inset 0 -2px 0 var(--amber)}

/* Three rails under the open row: hue, saturation, lightness. Each carries
   one letter, because an unlabelled rail is a mystery the second time you
   come back to it - the hue rail carries the spectrum and says what it is on
   sight, the other two do not. */
.ch-hsl{display:none;gap:4px;margin-top:9px}
.ch-hsl .hslrow{display:flex;align-items:center;gap:8px}
.ch-hsl .hslk{flex:none;width:9px;font-family:var(--wide);font-size:9.5px;
  letter-spacing:0;color:var(--blue-dim);text-shadow:var(--sh-micro)}
.ch.sel .ch-hsl,.ch:focus-within .ch-hsl{display:grid}
.ch-hsl input[type="range"]{height:14px;width:100%}
.ch-hsl input[type="range"]::-webkit-slider-thumb{height:14px;margin-top:-6px}
.ch-h::-webkit-slider-runnable-track{
  background:linear-gradient(90deg,#f55,#ff5,#5f5,#5ff,#55f,#f5f,#f55)}
.ch-h::-moz-range-track{
  background:linear-gradient(90deg,#f55,#ff5,#5f5,#5ff,#55f,#f5f,#f55)}
.ch-s::-webkit-slider-runnable-track{
  background:linear-gradient(90deg,rgba(120,140,160,.6),var(--sw,#4BB4FC))}
.ch-s::-moz-range-track{
  background:linear-gradient(90deg,rgba(120,140,160,.6),var(--sw,#4BB4FC))}
.ch-l::-webkit-slider-runnable-track{
  background:linear-gradient(90deg,#000,var(--sw,#4BB4FC),#fff)}
.ch-l::-moz-range-track{
  background:linear-gradient(90deg,#000,var(--sw,#4BB4FC),#fff)}

/* The row's EXPLANATION arrives with its controls, on the row you are on. */
.ch-blurb{display:none;margin:8px 0 0;
  font-size:11px;letter-spacing:.04em;line-height:1.62;
  color:#6BC6FF;text-shadow:var(--sh-micro)}
.ch.sel .ch-blurb,.ch:focus-within .ch-blurb{display:block}

/* A locked row is dimmed but never hidden: the lock is a statement about what
   randomise will touch, and a control you cannot see is a control you cannot
   unlock. */
.ch.lock .ch-chip,.ch.lock .ch-hsl{opacity:.45}
.ch.lock .ch-name{color:var(--amber)}


/* ---------- the mixer, folded ---------------------------------------------

   The mixer drives every channel at once - harmony off the body colour, the
   pattern index, the variation float, a palette lifted out of a photograph.
   It used to sit inside the same scroller as the seven channels, which is the
   

   It is a real <details>. That matters beyond the fold/unfold: a <summary>
   is focusable and operable from the keyboard with nothing written for it,
   find-in-page opens it to reveal a match inside, and it prints open. A div
   with a click handler has none of those and has to have all three written by
   hand, badly.

   It is CLOSED on arrival because the studio opens on its work: seven colours
   and an animal. The one mixer control a player reaches for constantly - the
   roll - is not in here at all any more; it is on the stage toolbar where
   */

.mix{margin-top:11px;border-top:1px solid rgba(30,111,224,.34)}
.mix>summary{list-style:none;display:flex;align-items:center;gap:10px;
  min-width:0;padding:10px 0 3px;cursor:pointer;
  transition:color .14s linear}
/* Safari/older WebKit draws its own triangle through a second pseudo. */
.mix>summary::-webkit-details-marker{display:none}
.mix>summary::marker{content:""}
/* The summary carries the contents in small type, so a folded panel still
   says what is behind it. A disclosure labelled only "MIXER" asks the player
   to open it to find out whether it is worth opening. */
.mix-note{flex:1;min-width:0;font-size:10.5px;letter-spacing:.10em;
  color:var(--blue-dim);opacity:.85;text-shadow:var(--sh-micro);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.mix-mk{flex:none;font-family:var(--wide);font-size:10.5px;letter-spacing:.06em;
  color:var(--blue-hot);text-shadow:var(--sh-micro)}
.mix-mk::after{content:"[ + ]"}
details[open]>summary .mix-mk::after{content:"[ \2013\ ]"}
.mix>summary:hover .k,.mix>summary:hover .mix-mk{color:#79D9FF}
.mix>summary:focus-visible{outline:1px solid var(--blue-hot);outline-offset:2px}

/* Bounded, with its own floor. Opening the mixer must never be able to shove
   a colour channel off the panel - that is the defect this whole section
   exists to make impossible - so it takes a fixed slice and scrolls inside
   it rather than growing without limit. */
.mix-body{max-height:min(46vh,360px);overflow-y:auto;padding-right:3px;
  position:relative}
.mix-body::after{content:"";display:block;position:sticky;bottom:-1px;
  height:22px;margin-top:-22px;pointer-events:none;
  background:linear-gradient(180deg,rgba(1,4,10,0),rgba(1,4,10,.92))}
.mix-body>.row:first-child{border-top:0;padding-top:4px}


/* ---------- the wire, folded ---------------------------------------------

   Only the READOUT folds. The four controls under it - copy the payload, the
   skin code field, copy the code, load a code - are outside the <details> and
   are on screen in every state, because they are the panel's actual verbs and
   a verb behind a disclosure is a verb nobody finds.                         */

.wirebox>summary{list-style:none;cursor:pointer;margin-bottom:0;
  transition:border-color .14s linear}
.wirebox>summary::-webkit-details-marker{display:none}
.wirebox>summary::marker{content:""}
.wirebox>summary:focus-visible{outline:1px solid var(--blue-hot);outline-offset:2px}
.wirebox>summary:hover h2{color:#A8E7FF}
.wirebox[open]>summary{margin-bottom:9px}
.wirebox+.wire-foot{margin-top:11px;padding-top:11px;
  border-top:1px solid rgba(30,111,224,.30)}
.wirebox[open]+.wire-foot{margin-top:9px}


/* ---------- the glitch lane ----------------------------------------------

   A glitch skin is the GAME'S OWN shader reacting to a sentinel value - an
   exact (0,0,0) channel, a variation of exactly 0.0 - and not a texture we
   paint. That is why it lives behind its own tab rather than as one more
   button in the mixer: everything on the paint tab is guaranteed never to
   emit a sentinel, and everything here is allowed to. A lane that can do
   both, silently, is the lane that ships a skin that will not load.         */

.glgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(96px,1fr));
  gap:5px;margin-top:9px;min-width:0}
.gl-card{position:relative;display:block;min-width:0;padding:0;cursor:pointer;
  border:1px solid rgba(30,111,224,.4);background:rgba(1,4,10,.72);
  transition:border-color .14s linear,box-shadow .14s linear}
.gl-card:hover{border-color:#79D9FF}
.gl-card.on,.gl-card[aria-selected="true"]{border-color:var(--blue-hot);
  box-shadow:inset 0 -2px 0 var(--blue-hot)}
.gl-card .nm{display:block;padding:5px 6px;font-family:var(--wide);
  font-size:9.5px;letter-spacing:.10em;color:var(--blue);
  text-shadow:var(--sh-micro);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ★★★ THE COLOUR PROXIMITY PLATE - what a glitch card shows now that no
   glitch skin ships a render (2026-08-11 owner order; glitchskins law 6b).
   Seven bands, ENGINE ORDER (body, markings, flank, underbelly, detail, eye,
   male display), each the documented fold of that slot: negative reads black,
   above one reads full. It is a colour READING of the design, never a picture
   of the animal - the game's own shader is the only thing that can draw that,
   and a photograph on the card was always a promise the site could not keep.

   The hairline ruling and the accent bloom under the bands are kept from the
   old no-art stand-in, so a card still sits in the same visual family as the
   rest of the studio instead of reading as a bare colour bar. */
.gl-card .gl-prox{display:flex;aspect-ratio:1/1;width:100%;overflow:hidden;
  background:
    repeating-linear-gradient(0deg,rgba(30,111,224,.16) 0 1px,transparent 1px 11px),
    repeating-linear-gradient(90deg,rgba(30,111,224,.16) 0 1px,transparent 1px 11px),
    radial-gradient(58% 58% at 50% 44%,var(--accent,#4BB4FC),rgba(1,4,10,.94) 76%)}
.gl-card .gl-prox i{flex:1 1 0;min-width:0;display:block;
  box-shadow:inset 0 0 0 1px rgba(1,4,10,.55)}
/* A row that could not be folded (an unreadable catalogue row) has no bands at
   all. The plate stays, wearing the accent bloom on its own - the same answer
   the studio has always given for "no picture", not an error state. */
.gl-card .gl-prox:empty{display:block}

/* The channel chips carry three states, so they carry three colours: OFF is
   the chip's own dim, GLOW is the hot blue that means "driven bright", VOID is
   amber, which on this site means caution and is exactly what a channel driven
   negative is. Red stays reserved for a refusal. */
.gl-slot[data-state="glow"]{color:var(--blue-hot);border-color:var(--blue-hot);
  background:rgba(30,111,224,.20);box-shadow:inset 0 -2px 0 var(--blue-hot)}
.gl-slot[data-state="void"]{color:var(--amber);border-color:var(--amber);
  background:transparent;box-shadow:inset 0 -2px 0 var(--amber)}
.gl-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;
  margin-top:auto;padding-top:11px}


/* ---------- the wire readout --------------------------------------------

   The exact payload the game reads, shown because this studio's whole claim
   is that nothing is re-interpreted on the way out. A claim you cannot check
   is marketing. Code gets a near-opaque ground of its own: at .55 over the
   lit half of the alley plate, a neon window sits behind the digits of a
   float and the one surface on this page that has to be read character by
   character is the one you cannot.                                          */

.wire-wrap{max-height:190px;overflow:auto;background:rgba(1,4,10,.9);
  padding:9px 10px;border:1px solid rgba(30,111,224,.3)}
.wire{margin:0;font-family:var(--mono);font-size:11.5px;line-height:1.62;
  letter-spacing:.02em;color:#6BC6FF;text-shadow:none;
  white-space:pre;tab-size:2}
.wire .kk{color:var(--blue-dim)}
.wire .nn{color:#79D9FF}
.wire .ss{color:var(--green)}
.wire-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.wire-foot input[type="text"]{min-width:150px}


/* ---------- the library --------------------------------------------------

   A flat capped list with tags and a search, because a folder tree in a rail
   this narrow is a toy. Every row is one skin: name, species, and the three
   things you can do to it.                                                   */

.lib-wrap{max-height:230px;overflow-y:auto;padding:2px}
/* ★ THE FIRST THING A NEW PLAYER SEES IN THIS PANEL is its empty state, and
   it shipped as a bare sentence floating in the rail - the one place on the
   page where a panel had a header, a search field, a tag strip and a footer
   drawn around nothing at all. An empty state is a state, so it gets drawn
   like one: the same ground, hairline and lit spine every other block of
   content in this rail wears, so the panel reads as ready rather than as
   unfinished. The words are in skinstudio.mjs - the controller owns this
   string because it also owns the "nothing MATCHES that" one. */
#lib-empty{display:block;margin:2px 0 0;padding:11px 12px;font-size:11.5px;
  background:rgba(1,4,10,.72);border:1px solid rgba(30,111,224,.26);
  box-shadow:inset 2px 0 0 rgba(75,180,252,.30)}
#lib-empty[hidden]{display:none}
.lib{list-style:none;margin:0;padding:0;display:grid;gap:3px}
.lib-row{background:rgba(1,4,10,.72);padding:9px 10px;
  border:1px solid rgba(30,111,224,.26);
  display:grid;grid-template-columns:minmax(0,1fr) auto;gap:5px 9px;
  align-items:start;transition:background .14s linear,border-color .14s linear}
.lib-row:hover{background:rgba(30,111,224,.14);border-color:rgba(75,180,252,.44)}
.lib-head{grid-column:1;grid-row:1;min-width:0;display:flex;
  flex-direction:column;gap:2px}
.lib-name{min-width:0;font-weight:560;font-size:12px;letter-spacing:.06em;
  color:#79D9FF;text-shadow:var(--sh-micro);
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.lib-sp{font-size:10.5px;letter-spacing:.14em;color:var(--blue-dim);
  text-shadow:var(--sh-micro)}
.lib-acts{grid-column:2;grid-row:1;display:flex;flex-wrap:wrap;
  justify-content:flex-end;gap:4px}
.lib-acts .act.sm{padding:4px 8px;font-size:10.5px}
.lib-note{grid-column:1 / -1;margin:2px 0 0;font-size:11px}
.lib-rename{grid-column:1 / -1;width:100%}
/* A delete that is one press from gone gets one sentence of friction, in the
   row itself. This site has exactly one modal and it is the shell. */
.lib-row.on{border-color:var(--blue-hot);box-shadow:inset 3px 0 0 var(--blue-hot)}
.lib-row .lib-note.bad,.lib-note.bad{color:var(--red)}

.lib-save{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.lib-save input[type="text"]{min-width:96px}
#design-name{flex:1.4}
#design-tags{flex:1}


/* ---------- the picture lane --------------------------------------------- */

.imgdrop{margin-top:10px;padding:16px;text-align:center;
  border:1px dashed rgba(75,180,252,.5);background:rgba(1,4,10,.5);
  transition:border-color .15s linear,background .15s linear}
.imgdrop.over{border-color:#79D9FF;background:rgba(30,111,224,.18)}
.imgdrop>.lbl{display:block;font-size:11px;letter-spacing:.10em;
  color:var(--blue-dim);text-shadow:var(--sh-micro)}
.imgdrop input[type="file"]{margin-top:9px;font-family:var(--mono);font-size:11px;
  color:var(--blue-dim);max-width:100%}
.imgdrop[hidden]{display:none}


/* ---------- states -------------------------------------------------------

   Every one of these is a real state the studio reaches, written in words a
   player can read. An empty rail that says nothing is the state that gets
   reported as a broken page.                                                */

/* The message line shares the footer with undo/redo/defaults. It takes the
   rest of the row and never pushes them onto a second line. */
.ch-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
.ch-foot #msg{flex:1 1 100%;min-width:0;display:block;min-height:1.7em}


/* ---------- motion -------------------------------------------------------

   One authored moment: a surface that has just arrived settles in from an
   already-visible default. Nothing loops, and reduced motion gets the settled
   state instantly.                                                           */

.wb .panel{animation:wbin .26s cubic-bezier(.2,.8,.2,1) both}
.wb .rail-c .panel{animation-delay:.04s}
.wb .rail-r .panel{animation-delay:.08s}
@keyframes wbin{from{opacity:.4;transform:translateY(6px)}to{opacity:1;transform:none}}

@media (prefers-reduced-motion:reduce){
  .wb .panel{animation:none}
  .ch.hit{animation:none}
  .tab::after{transition:none}
}


/* ---------- narrow -------------------------------------------------------

   The stage keeps its height on a phone because a 90px-tall dinosaur is not a
   preview. The rails stack, and the tool strip is allowed to wrap onto three
   rows rather than scroll sideways: no page on this site scrolls
   horizontally, down to 412px.                                              */

@media (max-width:760px){
  .wb{padding:0 var(--pad) 14px}
  /* The strip is a strip because a desktop rail has room for a heading, a
     sentence and a status on one baseline. A phone does not: at 412 the
     sentence's 24ch flex-basis still fits beside the title, so it wraps into a
     three-line column pressed against "SKIN STUDIO" and reads as a broken
     paragraph. Below this width each part takes its own line outright. */
  .wbhead{padding-bottom:11px;display:block}
  .wbhead .sub{max-width:none;margin-top:5px}
  .wbhead .boot{margin-top:7px}
  .deck-wrap{max-height:300px}
  .pnl-stage .stagebox{aspect-ratio:4/3;min-height:300px}
  /* ★★ THE SLIDERS WERE 0px WIDE BELOW THIS BREAKPOINT - MEASURED, ON A
     PAGE THAT LOOKED FINE. `width:100%` overrode the width but NOT the
     `flex:none` above, so the label took the whole non-wrapping row and the
     range input collapsed to nothing: #growth 456.7px at 761px viewport ->
     0.0px at 760, 759, 600 and 412. The readout was pushed to right=507 in a
     412 viewport and clipped away by the page's own overflow-x:hidden. Growth
     and variation could not be seen OR operated on a phone at all, and the
     band ruler underneath was describing a track that did not exist.
     The row wraps here instead: the label takes its own line, the track and
     its readout share the next one. */
  .slider{flex-wrap:wrap}
  .row>.k,.slider>.k{flex:1 0 100%;min-width:0;width:100%}
  .slider>input[type="range"]{flex:1 1 auto;min-width:0}
  /* ★ min-width:0 HERE BROKE THE RULER, 20px, at every width below this
     breakpoint. .bands derives its right margin from `--sl-v + --sl-gap +
     --sl-ua` - that is the whole point of the tokens, so the ruler and the
     track cannot drift apart - and --sl-v is only true while the readout
     actually reserves it. Dropped to 0 the readout shrank to its content, the
     track grew into the difference, and the ruler ended 20px short of the rail
     it describes: measured at 760, track 567 against bands 547, which put
     ADULT's edge 15px left of where the animal actually reaches it. The
     readout keeps its reserved width; it is `text-align:right` and flex:none,
     so nothing about the wrapped layout needed it to shrink. */
  .slider>.val{flex:none;min-width:var(--sl-v);text-align:right}
  .tgrp-r{margin-left:0}
  /* ★ THIS DECLARATION WAS DEAD. It is `.ch-hex` (0,1,0) and it was being
     beaten by the base `.ch-top .ch-hex` (0,2,0) above - the SAME specificity
     trap the base rule has a comment about, repeated in the media query that
     was written to work around it. Nothing warned; the field simply stayed
     70px on a rail that is 716px wide here. Qualified, it applies. */
  .ch-top .ch-hex{width:92px}
  /* The ruler's 89px offset is the desktop rail's label width. Once the label
     takes its own line the track starts at 0, and an offset ruler would put
     ADULT under the 75% mark of a track that begins 89px to its left. */
  .bands{margin-left:0}
  .pane-scroll{max-height:none;overflow:visible}
  /* Same reasoning as .pane-scroll: on a phone the PAGE is the scroller, and
     a 360px box scrolling inside a column that is already scrolling is two
     scrollbars fighting over one gesture. The floor fade goes with it - it
     would otherwise be drawn over content that is no longer clipped. */
  .mix-body{max-height:none;overflow:visible}
  .mix-body::after{display:none}
  /* The three stage actions read left-to-right with everything else once the
     column is one column wide; ending them at the right edge of a 412px
     screen only puts them under the growth readout's last two characters. */
  .tools .tgrp-r{justify-content:flex-start}
}
@media (max-width:430px){
  /* ★ HERE THE ROW IS ALLOWED TO WRAP AGAIN, ON PURPOSE. .ch-top is nowrap on
     the desktop rail to make an ACCIDENTAL wrap structurally impossible; below
     430px the five controls genuinely do not fit on one line (321px of row for
     337px of controls), so the deliberate two-line form takes over with the
     name on its own line above them. Without lifting nowrap here, that form
     could not happen and every control would be squeezed instead.
     Both overrides are qualified with .ch-top for the same specificity reason
     as above - as `.ch-name` and `.ch-hex` they lost to the base rules and the
     phone layout they describe never actually rendered. */
  /* The quarters are fixed (see .bands); the WORDS give up the pixels, which
     is the same rule the panel headers follow. At 8.5px on flat tracking
     "HATCHLING" measures inside a 50px quarter, so the ruler stays labelled
     AND true instead of trading one for the other. */
  .bands li{font-size:8.5px;letter-spacing:0;padding-left:4px;overflow:hidden}
  .bands li:first-child{padding-left:3px}
  .ch-top{flex-wrap:wrap}
  .ch-top .ch-name{flex:1 1 100%;min-width:0;order:-1}
  /* ★ AND THE HEX MUST NOT GROW HERE. As `flex:1 1 auto` it took every spare
     pixel of the second line - measured 221px of a 319px row - and pushed LOCK
     onto a THIRD line, so the phone row went from 68px to 96px and the lock
     ended up alone under a field five times its width. It keeps the 92px the
     760px rule already gives it, and the four controls hold one line:
     30 chip + 92 hex + 60 PICK + 75 LOCK + 3 gaps of 4 = 269 inside 319. */
  .lib-row{grid-template-columns:minmax(0,1fr)}
  .lib-acts{grid-column:1;grid-row:auto;justify-content:flex-start}
  .wire-foot input[type="text"]{min-width:0;width:100%}
  .glgrid{grid-template-columns:repeat(auto-fill,minmax(84px,1fr))}
}
