/* ============================================================================
   app.css — every rule the page paints with.

   Responsibility: appearance only. No rule here knows what an object is;
   the page adds and removes classes, and this decides what they look like.

   Rules a test depends on are marked where they sit. Several of them exist
   because that exact rule shipped broken once — a catcher that swallowed
   every click, a drop mark that painted nothing, an insertion line clipped
   by its own border-radius. The tests read this file through
   `document.styleSheets`, so moving a rule between files is safe but
   deleting one is not.
   ============================================================================ */
  :root{
    --bg:#0e1116; --panel:#141a23; --panel2:#19212c; --line:#232c39;
    --ink:#c8d4e4; --ink-dim:#7d8b9e; --ink-bright:#eaf1fa;
    --a:#38bdf8; --b:#f472b6; --r:#a3e635; --warn:#fbbf24; --bad:#f87171; --ok:#34d399;
    --mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,"Liberation Mono",monospace;
  }
  *{box-sizing:border-box}
  /* `display` from a class outranks the hidden attribute, so a flex row stays on
     screen however often the script sets el.hidden = true. This puts the
     attribute back in charge for every element, whatever classes it carries. */
  [hidden]{display:none!important}
  html,body{height:100%}
  body{margin:0;background:var(--bg);color:var(--ink);
       font:13px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;overflow:hidden}
  .app{display:grid;grid-template-columns:minmax(0,1fr) 400px;height:100vh}

  /* ---------- stage ---------- */
  .stage{position:relative;min-width:0;display:flex;align-items:center;justify-content:center;padding:18px}
  svg#stage{width:100%;height:100%;display:block;touch-action:none;user-select:none}
  .hint{position:absolute;left:26px;bottom:22px;font:11px/1.6 var(--mono);color:var(--ink-dim);
        background:rgba(14,17,22,.82);border:1px solid var(--line);border-radius:8px;padding:8px 11px;pointer-events:none}
  .hint b{color:var(--ink);font-weight:600}
  /* ---- the project bar ----
     Sits on the shell, top-left, where the legend is not. The rigging examples
     are the way INTO the system, and a control you have to scroll a side panel
     to find is one a reader never learns exists.

     It takes the pointer (the rest of the stage chrome does not) because it is
     a control rather than a readout, so `pointer-events` is set back on. */
  .projbar{position:absolute;left:26px;top:22px;display:flex;align-items:center;gap:8px;
        font:11px/1 var(--mono);color:var(--ink-dim);
        background:rgba(14,17,22,.9);border:1px solid var(--line);
        border-radius:8px;padding:7px 9px;z-index:2}
  .projbar label{white-space:nowrap}
  .projbar select{flex:0 1 auto;min-width:0;max-width:300px;font-size:11px;padding:4px 7px}
  .projbar button.mini{flex:none;width:22px;height:22px;padding:0;line-height:1}
  /* Open, the note reads as a continuation of the bar rather than a tooltip —
     it is prose to be read while working, not a hover hint that vanishes. */
  .projnote{position:absolute;left:26px;top:64px;max-width:380px;z-index:2;
        font:11.5px/1.65 var(--sans,inherit);color:var(--ink);
        background:rgba(14,17,22,.94);border:1px solid var(--line);
        border-left:2px solid var(--bone);border-radius:8px;padding:10px 12px}
  .projnote p{margin:0}
  .projnote p + p{margin-top:7px;color:var(--ink-dim)}
  .projnote b{color:var(--bone)}
  .legend{position:absolute;right:26px;top:22px;display:flex;flex-direction:column;gap:6px;
          font:11px/1 var(--mono);background:rgba(14,17,22,.82);border:1px solid var(--line);
          border-radius:8px;padding:9px 11px;pointer-events:none}
  .legend i{display:inline-block;width:10px;height:10px;border-radius:2px;margin-right:7px;vertical-align:-1px}

  path.src{vector-effect:non-scaling-stroke;cursor:grab;pointer-events:all}
  path.src.dragging,path#pathR.dragging{cursor:grabbing}
  path.src.hidden{display:none}
  /* An applied intersection painted per overlap draws through these pieces;
     its own path is transparent. Without this rule, hiding the group (its eye,
     hide all, a preview) hid only that invisible path and left the overlap
     on screen. */
  g.pieces.hidden{display:none}
  path#pathR{vector-effect:non-scaling-stroke;pointer-events:none}
  /* the masked result is its own layer: it survives the sources being hidden,
     and it never shows the mask shapes themselves */
  g#layerMask{pointer-events:none}
  g#layerMask.hidden{display:none}
  g#layerMask path{vector-effect:non-scaling-stroke}
  path#pathR.hidden{display:none}
  /* the capture rect sits above every shape, so while a tool is armed a drag
     starts a new shape instead of being grabbed by the shape under the cursor */
  rect#drawCatch{pointer-events:none;cursor:crosshair}
  rect#drawCatch.armed{pointer-events:all}
  /* the select tool needs its own catcher: it must see clicks on empty canvas to
     clear the marks, which the shapes' own handlers would never report */
  rect#selCatch{pointer-events:none;cursor:default}
  /* The select tool is the resting state, so it keeps the ordinary arrow rather
     than a crosshair: a crosshair promises drawing, and this tool picks and
     moves things. Shapes carry their own grab/grabbing cursor, and the band
     keeps the arrow while it is dragged out. */
  rect#selCatch.armed{pointer-events:all;cursor:default}
  rect#selBand{fill:rgba(56,189,248,.12);stroke:var(--a);stroke-width:1;
               stroke-dasharray:4 3;pointer-events:none;vector-effect:non-scaling-stroke}
  rect#selBand.idle{display:none}
  path#pathDraw{pointer-events:none;fill:rgba(163,230,53,.13);stroke:var(--r);
                stroke-width:1.5;stroke-dasharray:6 4;vector-effect:non-scaling-stroke}
  path#pathDraw.idle{display:none}
  /* The pen's own preview. Unlike pathDraw it carries NO fill: an open path
     being drawn is a stroke, and a fill would paint a phantom region across the
     chord back to the first anchor. */
  path#pathPen{pointer-events:none;fill:none;stroke:var(--r);stroke-width:1.6;
               vector-effect:non-scaling-stroke}
  path#pathPen.idle{display:none}
  .penreport{display:block;margin-top:5px;color:var(--r)}
  .drawhint{left:auto;right:26px;bottom:22px;color:var(--r);border-color:#3d5326}
  .selhint{left:auto;right:26px;bottom:22px;color:var(--a);border-color:#28455d}
  #drawTools button.on{background:#26351a;border-color:#4a6b28;color:#ddf5b4}
  #drawTools button.on:hover{background:#2c3d1e}
  /* The shape buttons, icon-only. Smaller than `.seltool` (28px against 34)
     because there are five of them and they are a picker rather than the mode
     switches below — the row should read as one strip, not five controls.

     Matching `.btn-row button`'s specificity for the same reason `.seltool`
     does: that rule sets a 96px minimum and would otherwise stretch each of
     these to label width and wrap the row. */
  .btn-row button.drawtool,
  .drawtool{flex:0 0 auto;width:28px;min-width:28px;height:26px;padding:0;
        display:inline-flex;align-items:center;justify-content:center}
  /* 16px on a 24px grid: the glyph keeps its own stroke weight and simply sits
     smaller, which is what stops it going muddy at this size. */
  .drawtool svg{width:16px;height:16px;display:block}
  /* The shape library. Tiles in a grid that fills the panel's width, grouped
     under foldable headings styled like the other sub-headings; each tile a
     thumbnail over its name. The shape is drawn in the object colour it will
     not keep (a new object takes the next palette colour) — the thumbnail is
     about the outline, so it is drawn the one way for every tile. */
  .assetnote{margin:0 0 9px}
  .assetfilter{width:100%;box-sizing:border-box;margin:0 0 6px}
  .assetgroup > summary{cursor:pointer;list-style:none;padding:7px 0 6px;
        font:600 10px/1 var(--mono);letter-spacing:.08em;text-transform:uppercase;color:var(--ink-dim)}
  .assetgroup > summary::-webkit-details-marker{display:none}
  .assetgroup > summary::before{content:"▸ ";color:#4a596b}
  .assetgroup[open] > summary::before{content:"▾ "}
  .assetgroup > summary:hover{color:var(--ink)}
  .assetgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(58px,1fr));gap:6px;padding:0 0 6px}
  .assettile{display:flex;flex-direction:column;align-items:center;gap:4px;padding:7px 3px 5px;
        background:var(--panel2);border:1px solid var(--line);border-radius:7px;color:var(--a);
        cursor:grab;user-select:none;min-width:0}
  .assettile:hover,.assettile:focus-visible{border-color:#3d5670;background:#1d2733;outline:none}
  .assettile:active{cursor:grabbing}
  .assettile svg{width:34px;height:34px;display:block;overflow:visible}
  .assettile svg path{fill:currentColor;fill-opacity:.25;stroke:currentColor;stroke-width:1.4;
        vector-effect:non-scaling-stroke}
  /* two lines at most, so "right triangle" reads whole rather than cut short
     (a word too long for a tile carries a soft hyphen — assets_panel.js) */
  .assetname{font-size:9.5px;line-height:1.2;color:var(--ink-dim);max-width:100%;
        text-align:center;min-height:2.4em;overflow:hidden;display:-webkit-box;
        -webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2}
  /* Icon-only, so they read as tools rather than sentences.

     `.btn-row button` sets a 96px minimum and is more specific than a bare
     class, so it wins wherever both apply — which made every tool button as
     wide as a labelled one and wrapped the row as soon as a fourth tool
     arrived. Matching that specificity here puts the icon size back in charge. */
  .btn-row button.seltool,
  .seltool{flex:0 0 auto;width:34px;min-width:34px;height:30px;padding:0;
        font-size:15px;line-height:1}
  .seltool.on{background:#12303f;border-color:#2b566f;color:#dff1ff}
  .seltool.on:hover{background:#173a4c}
  circle.xnode{fill:#fff;stroke:#0e1116;stroke-width:1.25;pointer-events:none}
  circle.rnode{fill:var(--r);stroke:#0e1116;stroke-width:1.25;pointer-events:none}

  /* ---------- panel ---------- */
  .panel{background:var(--panel);border-left:1px solid var(--line);overflow-y:auto;overflow-x:hidden}
  .panel::-webkit-scrollbar{width:10px}
  .panel::-webkit-scrollbar-thumb{background:#2a3441;border-radius:6px;border:3px solid var(--panel)}
  .panel > header{padding:18px 18px 14px;border-bottom:1px solid var(--line)}
  h1{margin:0;font-size:15px;letter-spacing:.2px;color:var(--ink-bright);font-weight:650}
  h1 em{font-style:normal;color:var(--r)}
  .sub{margin:6px 0 0;color:var(--ink-dim);font-size:11.5px;line-height:1.55}
  section{padding:14px 18px;border-bottom:1px solid var(--line)}
  .sec-title{font:600 10.5px/1 var(--mono);letter-spacing:.14em;text-transform:uppercase;
             color:var(--ink-dim);margin:0 0 11px}
  /* A foldable section: the heading IS the twisty, so there is one thing to
     aim at rather than a title with a control beside it.

     `details{margin-top:8px}` further down is for foldable blocks INSIDE a
     section; this one wraps the whole section, whose padding already provides
     the spacing — so the margin is taken back off. */
  .sec-fold{margin:0}
  /* Collapsed, the heading is the only thing showing and its bottom margin
     would read as a gap before the next section's rule. Open, the margin is
     what separates the heading from the controls, so it comes back. */
  .sec-fold > summary.sec-title{margin:0;cursor:pointer;list-style:none;
             font:600 10.5px/1 var(--mono);letter-spacing:.14em;
             text-transform:uppercase;color:var(--ink-dim)}
  .sec-fold[open] > summary.sec-title{margin:0 0 11px}
  .sec-fold > summary.sec-title:hover{color:var(--ink)}
  /* The arrow sits BEFORE the heading and matches the one `summary` already
     draws elsewhere, so a folded section and a folded block read the same. */
  .sec-fold > summary.sec-title::before{content:"▸ ";color:#4a596b}
  .sec-fold[open] > summary.sec-title::before{content:"▾ "}
  /* The Objects row of controls. It used to share a line with the heading,
     which is why that heading was a `.shape-head`/`.nm` and looked unlike
     every other section title. The heading is now a plain `.sec-title`
     summary like the rest, and the buttons moved to their own line here —
     which is also what lets them wrap instead of squeezing. */
  /* A group of sections behind one heading. The wrapper carries the heading
     and nothing else: its children are ordinary sections with their own
     padding and their own rules, so it takes none of its own or every panel
     inside would sit in a double margin. */
  section.sec-group{padding:0;border-bottom:1px solid var(--line)}
  section.sec-group > .sec-fold > summary.sec-title{padding:14px 18px;margin:0}
  /* Open, the heading needs no bottom margin: the first section inside brings
     its own padding, and a margin on top of that reads as a gap. */
  section.sec-group > .sec-fold[open] > summary.sec-title{margin:0;
        border-bottom:1px solid var(--line)}
  /* The last section in the group would otherwise draw a rule immediately
     above the group's own. */
  section.sec-group section:last-child{border-bottom:none}
  /* A heading INSIDE a section: same family as `.sec-title` but lower-case
     and dimmer, so it reads as a part of the section rather than as another
     section. Used where a block earned its own name without earning a rule
     across the panel — "appearance" under Result. */
  .sec-sub{font:600 10px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;
           color:#5f6c7d;margin:0 0 9px}
  .objects-bar{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-bottom:10px}
  /* Show-all / hide-all as icons: two words each, on a row that already holds
     four other controls. 24px square, matching `.helpbtn`'s height so the bar
     reads as one line of equal-sized controls. */
  .objects-bar button.visbtn{width:24px;min-width:24px;height:24px;padding:0;
        display:inline-flex;align-items:center;justify-content:center}
  .objects-bar button.visbtn svg{width:14px;height:14px;display:block}
  /* A run of a bar's buttons pushed to its far end, keeping the bar's own
     spacing between them. */
  .objects-bar .bar-end{display:flex;align-items:center;gap:inherit;margin-left:auto}
  label.field{display:flex;align-items:center;justify-content:space-between;gap:10px;margin:0 0 9px}
  label.field > span{font-size:12px;color:var(--ink-dim);white-space:nowrap}
  /* a length row: its unit after the number (ui/widgets/fields.js) — the
     label takes the free space, so the number stays right-aligned */
  label.field > span:first-child{margin-right:auto}
  label.field > span.unit{flex:none;min-width:20px;margin-left:-4px;font:11px/1 var(--mono)}
  /* Number inputs are styled with the rest: left out, they fall back to the
     browser's own light control and read as a hole in a dark panel. */
  select,input[type=text],input[type=number],input[type=search],textarea,button{font-family:inherit;color:var(--ink);
        background:var(--panel2);border:1px solid var(--line);border-radius:6px;padding:6px 8px;
        font-size:12px;outline:none}
  select{flex:1;min-width:0;cursor:pointer}
  select:focus,textarea:focus,input[type=number]:focus,input[type=search]:focus{border-color:#3d5670}
  /* `color-scheme` is what darkens the parts the page cannot reach: a select's
     drop-down list, and a number input's spinner. Without it those stay light
     however the control itself is painted. */
  select,input[type=number],input[type=checkbox]{color-scheme:dark}
  input[type=number]{width:76px;flex:0 0 auto;text-align:right}
  textarea{width:100%;font:11px/1.5 var(--mono);resize:vertical;min-height:64px;color:#a9c3dd}
  input[type=range]{flex:1;min-width:0;accent-color:var(--a);height:18px}
  button{cursor:pointer;transition:background .12s,border-color .12s}
  button:hover{background:#202b38;border-color:#334357}
  button:active{background:#1a232e}
  button:disabled{opacity:.4;cursor:not-allowed;background:var(--panel2);border-color:var(--line)}
  button:disabled:hover{background:var(--panel2);border-color:var(--line)}
  button.primary{background:#1d3a4d;border-color:#2b566f;color:#dff1ff}
  button.primary:hover{background:#234657}
  .btn-row{display:flex;gap:8px;flex-wrap:wrap}
  .btn-row button{flex:1;min-width:96px}
  .swatch{display:inline-block;width:9px;height:9px;border-radius:2px;margin-right:8px;vertical-align:0}
  .shape-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}
  .shape-head .nm{font:600 11px/1 var(--mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ink)}
  .val{font:11px/1 var(--mono);color:var(--ink-dim);min-width:46px;text-align:right}
  .checks{display:grid;grid-template-columns:1fr 1fr;gap:7px 12px}
  .appear{display:grid;grid-template-columns:34px 30px 1fr 30px 1fr 30px;gap:9px 7px;align-items:center}
  .appear .wv{font:10.5px/1 var(--mono);color:var(--ink-bright);text-align:right}
  .appear .ah{font:10px/1 var(--mono);color:var(--ink-dim);text-align:center;letter-spacing:.05em}
  .appear .an{font:600 10.5px/1 var(--mono)}
  .appear .sw{width:30px;height:22px;padding:0;border-radius:5px;cursor:pointer;border:1px solid var(--line)}
  .appear .sw:hover{border-color:#4d6076}
  .appear input[type=range]{width:100%}
  /* a native <input type=color> opens its dialog outside the window from a
     right-hand panel, so the picker is drawn in the page and clamped to it */
  .cpop{position:fixed;z-index:60;display:flex;flex-direction:column;gap:9px;width:196px;padding:11px;
        background:var(--panel2);border:1px solid #33404f;border-radius:10px;
        box-shadow:0 16px 38px rgba(0,0,0,.6)}
  .cp-sv{position:relative;height:112px;border-radius:7px;cursor:crosshair;touch-action:none}
  .cp-dot{position:absolute;width:11px;height:11px;margin:-6px 0 0 -6px;border-radius:50%;
          border:2px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.65);pointer-events:none}
  .cp-hue{-webkit-appearance:none;appearance:none;width:100%;height:14px;border-radius:7px;padding:0;border:none;
          background:linear-gradient(to right,#f00,#ff0,#0f0,#0ff,#00f,#f0f,#f00);outline:none;cursor:pointer}
  .cp-hue::-webkit-slider-runnable-track{height:14px;border-radius:7px;background:transparent}
  .cp-hue::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;border-radius:50%;
          background:#fff;border:1px solid rgba(0,0,0,.55);margin-top:1px}
  .cp-hue::-moz-range-track{height:14px;border-radius:7px;background:transparent}
  .cp-hue::-moz-range-thumb{width:12px;height:12px;border-radius:50%;background:#fff;border:1px solid rgba(0,0,0,.55)}
  .cp-row{display:flex;align-items:center;gap:8px}
  .cp-prev{width:24px;height:24px;border-radius:5px;border:1px solid var(--line);flex:none}
  .cp-row input{flex:1;min-width:0;font:11px/1 var(--mono)}
  .cp-swatches{display:grid;grid-template-columns:repeat(6,1fr);gap:5px}
  .cp-swatches button{height:17px;padding:0;border-radius:4px;border:1px solid rgba(255,255,255,.09)}
  .cp-swatches button:hover{border-color:#fff}
  #lblA{color:var(--a)} #lblB{color:var(--b)} #lblR{color:var(--r)}
  #foA,#swA{accent-color:var(--a)} #foB,#swB{accent-color:var(--b)} #foR,#swR{accent-color:var(--r)}
  #dragNote{color:#5b6879}
  .opsel button{flex:1;font:600 12px/1 var(--mono);padding:8px 6px;letter-spacing:.03em}
  .opsel button.on{background:#26351a;border-color:#4a6b28;color:#ddf5b4}
  .opsel button.on:hover{background:#2c3d1e}
  /* ---------- operation options ----------
     A SETTING of the operation, drawn so it cannot be read as an operation of
     its own. The operations above are separate, bordered, bold blocks and the
     chosen one fills green; an option row is ONE recessed pill-shaped track
     whose segments share their borders, in lighter, smaller type, with a
     caption naming what it sets. The chosen segment is lifted out of the track
     rather than filled, and marks itself with a dot in the result colour.

     The caption is `data-cap`, drawn as ::before, because the page reads each
     row's `.children` as exactly its buttons and hides the row with `hidden`.
     A caption element would be counted as a button and outlive the row. */
  .optsel{display:flex;align-items:center;margin-top:8px}
  .optsel[hidden]{display:none}
  .optsel::before{content:attr(data-cap);flex:0 0 64px;font:500 9.5px/1 var(--mono);
        letter-spacing:.12em;text-transform:uppercase;color:var(--ink-dim)}
  .optsel button{flex:1;min-width:0;margin:0;padding:5px 8px;font:400 11px/1.2 var(--mono);
        color:var(--ink-dim);background:#0d1218;border:1px solid var(--line);border-radius:0;
        white-space:nowrap}
  .optsel button + button{border-left-width:0}
  .optsel button:first-of-type{border-radius:13px 0 0 13px}
  .optsel button:last-of-type{border-radius:0 13px 13px 0}
  .optsel button:hover{color:var(--ink);background:#141b24;border-color:var(--line)}
  .optsel button.on{color:var(--ink-bright);background:#223041;border-color:#3a4d63}
  .optsel button.on::before{content:'';display:inline-block;width:6px;height:6px;margin:0 6px 1px 0;
        border-radius:50%;background:var(--r);vertical-align:middle}
  .optsel button.on + button{border-left-width:0}
  .optsel button:focus-visible{outline:1px solid var(--a);outline-offset:1px;position:relative;z-index:1}
  /* Why the result is empty: a notice, not a control, so it is set off by a
     warm edge rather than drawn as another row of options. Its buttons are
     the remedies, sized down so they read as offers inside the notice. */
  .whyempty{margin-top:9px;padding:8px 10px;border-left:2px solid var(--warn);
        background:rgba(251,191,36,.06);border-radius:0 6px 6px 0}
  .whyempty[hidden]{display:none}
  /* a union in pieces is a fact, not a fault: the note keeps its place but
     loses the warning edge, and there is no remedy row under it */
  .whyempty.fact{border-left-color:var(--a)}
  .whyempty.fact .note{margin-bottom:0}
  .whyempty .note{margin:0 0 7px;color:var(--ink)}
  .whyempty .btn-row{gap:6px}
  .whyempty .btn-row button{flex:0 0 auto;min-width:0;padding:4px 9px;font-size:11px}
  /* ---------- object list ---------- */
  .cnt{font:10px/1 var(--mono);color:var(--ink-dim);background:var(--panel2);
       border:1px solid var(--line);border-radius:9px;padding:2px 6px;margin-left:6px}
  .helpbtn{width:20px;height:20px;padding:0;border-radius:50%;font:600 11px/1 var(--mono);
           color:var(--ink-dim);flex:none}
  /* the operation note sits beside its help button rather than under it */
  .noterow{display:flex;align-items:flex-start;gap:9px;margin-top:9px}
  .noterow .note{margin:0;flex:1;min-width:0}
  .helpbox h4{margin:0 0 6px;font:600 11px/1.3 var(--mono);letter-spacing:.06em;
              text-transform:uppercase;color:var(--ink-bright)}
  .helpbox h4 .sym{color:var(--r);margin-right:6px}
  .helpbox p{margin:0 0 7px}
  .helpbox p:last-child{margin-bottom:0}
  .helpbox .eg{font:11px/1.5 var(--mono);color:#9dbfd8;background:#0b0f14;
               border:1px solid var(--line);border-radius:5px;padding:7px 9px;margin:7px 0 0}
  .helpbtn:hover{color:var(--ink)}
  .helpbtn.on{background:#1d3a4d;border-color:#2b566f;color:#dff1ff}
  .helpbox{margin:0 0 10px;padding:10px 11px;background:var(--panel2);
           border:1px solid var(--line);border-radius:8px}
  .helplist{display:grid;grid-template-columns:auto 1fr;gap:5px 10px;margin:8px 0 0;
            font-size:11.5px;line-height:1.5;color:var(--ink-dim)}
  .helplist dt{margin:0}
  .helplist dd{margin:0}
  .helplist .k{display:inline-block;min-width:24px;text-align:center;font:11px/1.4 var(--mono);
               color:var(--ink);background:#0b0f14;border:1px solid var(--line);
               border-radius:4px;padding:2px 5px}
  .helplist .dim2{color:#5b6879}
  .obj{display:flex;align-items:center;gap:8px;padding:5px 6px;border-radius:6px;cursor:pointer;
       border:1px solid transparent}
  .obj:hover{background:var(--panel2)}
  .obj .grip{flex:none;width:11px;text-align:center;color:var(--ink-dim);
             cursor:grab;user-select:none;line-height:1}
  .obj .grip:active{cursor:grabbing}
  .obj.sel{background:#1b2733;border-color:#2f4457}
  /* ---- dragging a row ----
     The drop target is shown as a LINE between rows, or a frame around a group
     the row would go into. Those are genuinely different outcomes — reorder
     versus reparent — so they must not look alike.

     These come AFTER `.sel` deliberately. They carry the same specificity, so
     source order is what decides, and a selected row is exactly the one most
     likely to be dragged: listed first, `.sel` repainted over every drop mark
     and the row being dragged onto showed no feedback at all. */
  .obj.dragging{opacity:.4}
  /* The insertion line is drawn as a BACKGROUND stripe, not an inset shadow.
     An inset shadow is clipped by the row's own border-radius, so its ends
     curve away and the line stops short of the row's width — it reads as a
     border that only partly drew. A background gradient ignores the radius and
     runs edge to edge, which is what an insertion point has to do to say
     exactly where the row will land. */
  .obj.drop-before,
  .obj.drop-after{background-repeat:no-repeat;background-size:100% 2px}
  .obj.drop-before{background-image:linear-gradient(var(--a),var(--a));
                   background-position:left top}
  .obj.drop-after{background-image:linear-gradient(var(--a),var(--a));
                  background-position:left bottom}
  /* Dropping INTO a group is a different outcome from dropping between rows,
     so it is shown differently: the whole row is framed and lit rather than
     given an edge line. The group's own marks brighten with it, so the row
     that will receive the shape is unmistakable at a glance. */
  .obj.drop-inside{background:#20323f;border-color:var(--a);
                   box-shadow:0 0 0 1px var(--a), 0 0 8px rgba(56,189,248,.35)}
  .obj.drop-inside .nm2{color:var(--ink-bright)}
  .obj.drop-inside .gmark{opacity:1}
  .obj.drop-inside .twisty{color:var(--a)}
  .obj.drop-no{border-color:#7f3b3b;cursor:not-allowed}
  .obj .dot{width:11px;height:11px;border-radius:3px;flex:none;border:1px solid rgba(255,255,255,.14)}
  /* A group reads as a container: a twisty where a colour swatch would be, a
     folder mark naming what it is, and a blank where its checkbox would be,
     because it never operates on anything.

     The twisty and the folder are deliberately separate. One is a CONTROL that
     toggles, the other says WHAT THE ROW IS — sharing one glyph for both meant
     a collapsed group and an expanded one looked like different kinds of row,
     and a group could only be told from a shape by noticing the absence of a
     colour swatch. */
  /* The twisty and the folder mark share ONE column, the one a shape spends on
     its colour swatch. Given a slot each they add a whole extra 21px step to
     every group row, so a group's name no longer lines up with a shape's and
     the gap after the grip reads as doubled. They sit side by side inside that
     slot instead, which keeps every row's columns in the same places. */
  .obj .gcell{display:flex;align-items:center;gap:2px;flex:none}
  .obj .twisty{width:11px;flex:none;text-align:center;font-size:11px;line-height:1;
               color:var(--ink-dim);cursor:pointer}
  .obj .twisty:hover{color:var(--ink)}
  .obj .gmark{flex:none;text-align:center;font-size:11px;line-height:1;
              color:var(--a);opacity:.85;user-select:none}
  /* In the Operation list an applied group's square chooses the group the
     header works on, and is framed while it is the one. An outline, not a
     border, so the frame never moves the row's columns. */
  .obj .gmark.pick{cursor:pointer;border-radius:2px;outline:1px solid transparent;outline-offset:1px}
  .obj .gmark.pick:hover{opacity:1;outline-color:var(--ink-dim)}
  .obj .gmark.target{opacity:1;outline-color:var(--a);box-shadow:0 0 6px rgba(56,189,248,.45)}
  .obj .cbgap{width:13px;flex:none}
  .obj.grp .nm2{color:var(--ink-bright);font-weight:600}
  .obj .nm2{flex:1;min-width:0;font:11.5px/1.3 var(--mono);color:var(--ink);
            white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  .obj.off .nm2{color:#5b6879;text-decoration:line-through}
  .obj.dim .dot{opacity:.28}
  .obj .eye{font-size:9.5px;min-width:22px;opacity:.75}
  .tag{margin-left:7px;font:9px/1 var(--mono);letter-spacing:.06em;text-transform:uppercase;
       color:#ddf5b4;background:#26351a;border:1px solid #4a6b28;border-radius:3px;padding:2px 4px}
  .tag.mask,.tag.whole{color:#cfe3ff;background:#1b2b3d;border-color:#2f527a}
  .tag.content,.tag.cut{color:#8b98a8;background:#1a1f27;border-color:#2a323d}
  .obj .role,.obj .cutmode{font-size:9px;min-width:22px;opacity:.55}
  .obj .role.on,.obj .cutmode.on{opacity:1;color:#cfe3ff;background:#1b2b3d;border-color:#2f527a}
  .obj.dim .eye{opacity:.4}
  .obj .mini{padding:2px 6px;font-size:10.5px;line-height:1.3;flex:none}
  .obj input[type=checkbox]{flex:none;margin:0}
  /* while masking, a mask shape is the tool that cuts the result, not part of
     what the result shows — draw it as an outline so the two never read alike */
  path.src.maskshape{stroke-dasharray:7 5}
  /* one outline per picked object, so every object taking part in the operation
     is marked the same way — a lone element could only ever mark one of them */
  g#layerSel{pointer-events:none}
  g#layerSel.hidden{display:none}
  /* The selection is shown as a box AROUND what is picked, not as a second
     outline traced over each object. Tracing changed how the objects themselves
     read — a dash laid on an object's own edge competes with its stroke, and a
     thin one vanishes under a thick one. A box touches none of that: the
     objects are drawn exactly as they are, and the marquee sits clear of them.

     `vector-effect` keeps the dashes one width whatever the object scale, and
     `pointer-events:none` on the layer keeps the box from swallowing clicks. */
  g#layerSel rect.selbox{fill:none;stroke:var(--a);stroke-width:1.5;
                         stroke-dasharray:6 4;vector-effect:non-scaling-stroke}
  /* Anchor gizmos. A square dot is a corner, a round one is smooth — the same
     distinction the anchor-type tool edits. `vector-effect` keeps them the same
     size on screen whatever the object is scaled to, so picking feels the same
     everywhere. */
  g#layerNodes{pointer-events:none}
  g#layerNodes .anchor{fill:var(--bg);stroke:var(--a);stroke-width:1.5;
                       vector-effect:non-scaling-stroke}
  g#layerNodes .anchor.on{fill:var(--a);stroke:#fff}
  g#layerNodes .anchor.focus{stroke:#fff;stroke-width:2.25}
  g#layerNodes .handle{stroke:var(--a);stroke-width:1;opacity:.72;
                       vector-effect:non-scaling-stroke;fill:none}
  g#layerNodes .handle-dot{fill:var(--a);stroke:none;opacity:.72}
  /* Direction arrows. Reversing a path changes nothing you can see in the
     outline, so the arrows are the only way to tell it worked. */
  g#layerNodes .dir{fill:var(--a);stroke:none;opacity:.85}
  rect#nodeCatch{pointer-events:none;cursor:default}
  rect#nodeCatch.armed{pointer-events:all;cursor:crosshair}
  /* A catcher is inert until its tool is armed. Without this the topmost one
     swallows every click on the canvas — including the drags that move
     objects — because a bare <rect> takes pointer events by default. */
  rect#penCatch{pointer-events:none;cursor:default}
  rect#penCatch.armed{pointer-events:all;cursor:crosshair}
  /* THE HAND. `grab` says "this can be moved" before the drag starts and
     `grabbing` confirms it during — the pair is what makes a pan feel like
     moving paper rather than like a click that happened to scroll. */
  /* UNARMED IT MUST BE INVISIBLE TO THE POINTER, like every other catcher.
     It is the last rect in the document — deliberately, so an armed hand wins
     the drag — which also means that without this base rule it sits over
     every shape and steals the hit test whether or not the tool is on. */
  rect#handCatch{pointer-events:none;cursor:default}
  rect#handCatch.armed{pointer-events:all;cursor:grab}
  rect#handCatch.armed.dragging{cursor:grabbing}
  /* ---- the skeleton ----
     Bones get one reserved hue nothing else on the canvas uses, so a bone is
     never mistaken for content — the same reasoning that draws a mask shape as
     a dashed outline while masking is live. The canvas is dark, so that hue is
     a light amber: it reads over the grid and over every colour in the object
     palette, none of which is amber.

     The body is translucent so a shape underneath stays readable — a rig is
     something you look THROUGH at the drawing it moves. Its outline is not,
     which is what keeps a bone lying over a bright shape still legible. */
  /* A target is a GOAL, not a bone: it says where something is being asked to
     reach, which a bone never does. So it gets its own reserved hue rather
     than the bone amber — sharing one would make "the skeleton" and "what the
     skeleton is chasing" read as a single kind of thing. Cyan sits far from
     the amber and stays bright on the dark canvas. */
  :root{ --bone:#fcd34d; --bone-on:#fff1c2; --target:#5eead4;
    /* ONION GHOSTS GET TWO HUES, one for each direction in time — that is the
       whole point of onion skin: seeing where the motion came FROM and where
       it is going TO. One colour for both makes past and future
       indistinguishable, which is the state this shipped in.

       Warm for the past and cool for the future, which is the convention every
       animation tool uses and the one a rigger already has in their hands. Both
       are picked to read on the dark canvas and to stay clearly apart from the
       bone amber, so a ghost is never mistaken for a bone. */
    --ghost-back:#fb7185; --ghost-fwd:#38bdf8; }

  /* ---- the morph overlay ----
     A morph needs its own reserved hue for the same reason a bone does: it is
     tooling laid over the drawing, and sharing a colour with content would
     make "where this point goes" read as part of the picture. The canvas is
     dark, so it is a bright violet — far from the bone amber, the target cyan
     and both onion hues, and absent from the object palette.

     THE TWO OUTLINES ARE NOT THE SAME WEIGHT. Where the shape is NOW is what
     the artist is looking at; where it is going is a reference. So the
     destination is dashed and dimmer, the way a mask shape is drawn as a
     dashed outline while masking is live.

     The link lines are thin and translucent because there are forty of them:
     what matters is the pattern they make — parallel is right, a fan is a bad
     phase — not any one line. */
  :root{ --morph:#c084fc; --morph-dim:#8b5cf6; }
  g#layerMorph{pointer-events:none}
  .morphgz{pointer-events:none}
  .morph-from{fill:none;stroke:var(--morph);stroke-width:1.5;opacity:.95}
  .morph-to{fill:none;stroke:var(--morph-dim);stroke-width:1.25;
        stroke-dasharray:5 4;opacity:.8}
  .morph-link{fill:none;stroke:var(--morph);stroke-width:.75;opacity:.45}
  .morph-start{fill:var(--morph);stroke:var(--bg);stroke-width:1.25}
  .morph-arrow{fill:var(--morph);stroke:none;opacity:.9}
  /* A region that grows in or shrinks away is not a correspondence, so its
     links say nothing worth reading — the outlines still do. */
  .morphgz[data-kind="appear"] .morph-link,
  .morphgz[data-kind="disappear"] .morph-link{opacity:.18}
  /* Ghosts never take the pointer and never carry their own fill: a posed copy
     painted in its object's colour reads as a second shape rather than as a
     trace. An outline in the bone hue says "this is the rig showing you
     where", and the opacity per ghost carries the sense of time. */
  g#layerOnion{pointer-events:none}
  g#layerOnion path{fill:none;stroke:var(--bone);stroke-width:1.1;
        vector-effect:non-scaling-stroke;stroke-linejoin:round}
  /* Which way in time. The class is set per ghost from the `side` the gizmo
     layer already computes — it was carried through the whole pass and
     discarded here, so every ghost drew the same colour. */
  g#layerOnion path.ghost-back{stroke:var(--ghost-back)}
  g#layerOnion path.ghost-fwd{stroke:var(--ghost-fwd)}
  g#layerRig{pointer-events:none}
  /* The body carries a dark halo as well as its own outline. A single light
     stroke is legible over the canvas and over most shapes, but it disappears
     into a bright warm fill — a flame is exactly that case. `paint-order` puts
     the dark stroke underneath, so the bone keeps its hue and gains an edge. */
  g#layerRig .bone-body{fill:var(--bone);fill-opacity:.22;stroke:var(--bone);
        stroke-width:1.2;stroke-linejoin:round;vector-effect:non-scaling-stroke;
        paint-order:stroke fill;filter:drop-shadow(0 0 1.5px rgba(8,10,14,.95))}
  g#layerRig .bonegz.on .bone-body{fill-opacity:.4;stroke:var(--bone-on);stroke-width:1.8}
  /* The joint is the parent end and the handle a chain hangs from, so it is
     drawn solid; the tip is hollow, which is also how it reads as "pull from
     here". Both carry a dark rim so they stay visible over a light shape. */
  g#layerRig .bone-joint{fill:var(--bone);stroke:#0e1116;stroke-width:1.25;
        vector-effect:non-scaling-stroke}
  g#layerRig .bone-tip{fill:#0e1116;stroke:var(--bone);stroke-width:1.5;
        vector-effect:non-scaling-stroke}
  g#layerRig .bonegz.on .bone-joint{fill:var(--bone-on)}
  g#layerRig .bonegz.on .bone-tip{stroke:var(--bone-on)}
  /* A weight dot is a readout, so it is drawn flat and rimmed rather than
     given the bone hue: its colour IS the information, and tinting it toward
     the rig's palette would make two different scales look like one. */
  g#layerRig .weight-dot{stroke:#0e1116;stroke-width:1;vector-effect:non-scaling-stroke}
  /* The wedge of angles a bone may reach. Filled and faint rather than
     outlined: it is a REGION, not a handle, and an outline would read as one
     more thing to grab. It brightens as the bone nears an end — the opacity is
     set per frame by the renderer — so the limit is seen coming rather than
     discovered by the bone refusing to move. Bone hue, because it describes a
     bone rather than a goal. */
  g#layerRig .limit-arc{fill:var(--bone);stroke:var(--bone);stroke-width:.8;
        stroke-linejoin:round;vector-effect:non-scaling-stroke;
        stroke-opacity:.55}
  /* Where a mirror would fold the rig. Dashed and faint, in the bone hue
     because it is about the skeleton — shown only while the mirror control is
     being worked, since it describes an intent rather than a state. */
  g#layerRig .mirror-axis{fill:none;stroke:var(--bone);stroke-width:1;
        stroke-dasharray:6 5;stroke-opacity:.6;vector-effect:non-scaling-stroke}
  /* A chain target: a hollow ring with a cross through it. Hollow because a
     filled blob reads as content, and a target sits on top of the drawing
     where being mistaken for content is exactly the failure. The cross keeps
     the centre findable when the ring lies over a shape of the same value.
     Both carry a dark rim so they survive over a light fill. */
  g#layerRig .chain-target-ring{fill:none;stroke:var(--target);stroke-width:1.8;
        vector-effect:non-scaling-stroke;
        filter:drop-shadow(0 0 1.5px rgba(8,10,14,.95))}
  g#layerRig .chain-target-cross{fill:none;stroke:var(--target);stroke-width:1.2;
        vector-effect:non-scaling-stroke;
        filter:drop-shadow(0 0 1.5px rgba(8,10,14,.95))}
  /* Out of reach: the ring is dashed, so the gap between where the target is
     and where the limb stopped reads as a STATE rather than as a bug. The limb
     is still drawn, pointing at what it cannot grasp. */
  g#layerRig .chaintgt.far .chain-target-ring{stroke-dasharray:4 3}
  g#layerRig .chaintgt.far .chain-target-cross{stroke-opacity:.5}
  /* switched off, but still findable — a target nobody can see is a setting
     nobody can turn back on */
  g#layerRig .chaintgt.off .chain-target-ring,
  g#layerRig .chaintgt.off .chain-target-cross{stroke-opacity:.38;
        stroke-dasharray:3 3}
  /* A chain's SPAN: a faint polyline through the joints the chain owns, with a
     small mark at each joint it may turn. Drawn in the TARGET hue because it
     belongs to the goal rather than to the skeleton, and well under the ring's
     opacity so it annotates the chain rather than competing with the handle.

     pointer-events are off on purpose. A chain is a relationship, not a part:
     there is nothing to grab, because the target is the handle. A line that
     looked draggable and ignored the drag would be worse than no line. */
  g#layerRig .chainspan{pointer-events:none}
  g#layerRig .chain-span-line{fill:none;stroke:var(--target);stroke-opacity:.42;
        stroke-width:1.1;stroke-dasharray:6 4;vector-effect:non-scaling-stroke;
        filter:drop-shadow(0 0 1.5px rgba(8,10,14,.9))}
  g#layerRig .chain-span-joint{fill:var(--target);fill-opacity:.55;
        stroke:#0e1116;stroke-width:.9;vector-effect:non-scaling-stroke}
  /* The shortfall, when a chain cannot reach: the gap from where the tip
     stopped to where the target is. Drawn so the distance is READABLE rather
     than left for the reader to infer from a limb that stopped early. */
  g#layerRig .chain-span-gap{fill:none;stroke:var(--target);stroke-opacity:.5;
        stroke-width:1;stroke-dasharray:2 3;vector-effect:non-scaling-stroke}
  /* switched off, but still findable — the same rule the target ring follows */
  g#layerRig .chainspan.off .chain-span-line{stroke-opacity:.2}
  g#layerRig .chainspan.off .chain-span-joint{fill-opacity:.25}
  /* the bone being pulled out: dashed, because it does not exist yet */
  g#layerRig .bone-draft{fill:var(--bone);fill-opacity:.13;stroke:var(--bone);
        stroke-width:1.3;stroke-dasharray:5 4;vector-effect:non-scaling-stroke}
  rect#boneCatch{pointer-events:none;cursor:default}
  rect#boneCatch.armed{pointer-events:all;cursor:crosshair}
  /* ---- the transport ----
     Icon buttons sized like the canvas tools, because they are the same kind of
     thing: one glyph, no label. The generic `.btn-row button` minimum would
     otherwise stretch five of them across two rows. */
  .btn-row button.anim-btn{flex:0 0 auto;width:38px;min-width:38px;height:28px;
        padding:0;font-size:12px;line-height:1}
  .btn-row button.anim-btn.playing{background:#3b2a12;border-color:#6b4c1c;color:#fcd34d}
  .anim-row{display:flex;align-items:center;gap:8px;margin-top:8px}
  .anim-row label{font-size:12px;color:var(--ink-dim);min-width:38px}
  .anim-of{font:11px/1 var(--mono);color:var(--ink-dim)}
  /* The scrub bar is the timeline until the timeline panel arrives. Its thumb
     takes the bone hue, so "where the animation is" reads as rig chrome rather
     than as another blue control. */
  /* `accent-color` paints the thumb and the filled part, but the empty part of
     the groove is left to the browser and comes out nearly black on this panel.
     Giving the track its own background is what makes the range readable. */
  input[type=range].anim-scrub{width:100%;margin:9px 0 2px;accent-color:var(--bone);
        background:transparent;cursor:pointer;height:18px}
  input[type=range].anim-scrub::-webkit-slider-runnable-track{height:5px;
        border-radius:3px;background:var(--panel2);border:1px solid var(--line)}
  input[type=range].anim-scrub::-moz-range-track{height:5px;border-radius:3px;
        background:var(--panel2);border:1px solid var(--line)}
  .anim-grid{display:grid;grid-template-columns:auto 1fr auto 1fr;gap:7px 9px;
        align-items:center;margin-top:9px}
  .anim-grid label{font-size:12px;color:var(--ink-dim)}
  .anim-grid input[type=number]{width:100%}
  /* ---- the bone list ----
     Rows echo the object list's shape so the two read as the same kind of
     control, but they carry the bone hue rather than the selection blue: a
     skeleton is rig chrome, and keeping the colours apart is what stops a
     picked bone looking like a picked shape. */
  .bonelist{display:flex;flex-direction:column;gap:2px;max-height:190px;overflow-y:auto}
  .bonerow{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:6px;
        cursor:pointer;border:1px solid transparent;font-size:12px;color:var(--ink)}
  .bonerow:hover{background:var(--panel2)}
  .bonerow.on{background:#3b2a12;border-color:#6b4c1c;color:#fcd34d}
  .bonerow .bonedot{flex:none;width:8px;height:8px;border-radius:50%;
        background:var(--bone);opacity:.75}
  .bonerow.on .bonedot{opacity:1}
  .bonerow .bonenm{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
        white-space:nowrap}
  .bonerow .bonekids{flex:none;font:10px/1 var(--mono);color:var(--ink-dim)}
  /* A bone a chain solves. In the TARGET hue, because what drives it is a
     goal rather than the skeleton — the same distinction the canvas makes
     between a bone and the ring it reaches for. */
  .bonerow .bonedriven{flex:none;font:9px/1 var(--mono);letter-spacing:.5px;
        color:var(--target);border:1px solid currentColor;border-radius:3px;
        padding:2px 3px;opacity:.85}
  .bonerow.driven .bonenm{color:var(--ink-dim)}
  /* A smart-bone row. Picked in the TARGET hue, because a driver is a goal
     the way a chain's ring is — something the rig is being asked to follow
     rather than a part of the skeleton. */
  .driverlist{display:flex;flex-direction:column;gap:2px;max-height:120px;overflow-y:auto}
  .driverrow{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:6px;
        cursor:pointer;border:1px solid transparent;font-size:12px;color:var(--ink)}
  .driverrow:hover{background:var(--panel2)}
  .driverrow.on{background:#0f3733;border-color:#1d6b60;color:var(--target)}
  .driverrow.off{color:var(--ink-dim);font-style:italic}
  /* An action row is picked in the app's own accent rather than a rig hue: an
     action is a document, like a project, not a part of the skeleton. */
  .actionlist{display:flex;flex-direction:column;gap:2px;max-height:130px;overflow-y:auto}
  .actionrow{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:6px;
        cursor:pointer;border:1px solid transparent;font-size:12px;color:var(--ink)}
  .actionrow:hover{background:var(--panel2)}
  .actionrow.on{background:#1e293b;border-color:#3b5473;color:#cfe3ff}
  /* the mix readout sits with its slider rather than under it */
  #blendReadout{min-width:38px;text-align:right;font:11px/1 var(--mono);color:var(--ink-dim)}
  /* A chain row is picked in the TARGET hue, not the bone one — the same
     distinction the canvas makes, kept in the panel so the two agree about
     what kind of thing is selected. */
  .chainlist{display:flex;flex-direction:column;gap:2px;max-height:140px;overflow-y:auto}
  .chainrow{display:flex;align-items:center;gap:7px;padding:4px 6px;border-radius:6px;
        cursor:pointer;border:1px solid transparent;font-size:12px;color:var(--ink)}
  .chainrow:hover{background:var(--panel2)}
  .chainrow.on{background:#0f3733;border-color:#1d6b60;color:var(--target)}
  /* switched off but still listed, because a chain nobody can find is a
     setting nobody can turn back on */
  .chainrow.off{color:var(--ink-dim);font-style:italic}
  /* a warning is a readout, not an alarm: amber text, no box, no icon */
  .note.warn{color:#fbbf24}
  /* A stale binding is not an error — the shape simply falls back to its rest
     outline — but it is something the user has to be told, because nothing on
     the canvas says why a bound shape stopped following its bone. Amber, like
     every other warning here, and never red: nothing is broken. */
  .note.bindwarn{color:var(--warn);border-left:2px solid var(--warn);padding-left:8px}

  /* ---- the morph panel -------------------------------------------------
     Every colour here is one of the page's own tokens, so the rows sit on
     the dark panel the same way every other list does rather than glowing
     against it. The fate word is the only colour in a row: it is what the
     eye should land on, and a row where everything is coloured says
     nothing. */
  .morph-table{display:flex;flex-direction:column;gap:3px;margin-top:5px}
  .morph-row,.morph-decision{display:flex;align-items:baseline;gap:8px;
        padding:4px 7px;background:var(--panel2);border:1px solid var(--line);
        border-radius:5px;font:11px/1.45 var(--mono)}
  .morph-decision{margin-top:3px}
  .morph-fate{flex:0 0 auto;font-weight:600;letter-spacing:.03em}
  .morph-what{flex:1 1 auto;min-width:0;color:var(--ink);
        overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .morph-row .k,.morph-decision .k{flex:0 0 auto;color:var(--ink-dim)}
  .morph-ok{color:var(--ok)}
  .morph-a{color:var(--a)}
  .morph-b{color:var(--b)}
  .morph-warn{color:var(--warn)}
  .morph-dim{color:var(--ink-dim)}
  /* The shapes each end of a morph names. It fills the row the way a value
     field does, and truncates rather than wrapping — a state of nine shapes
     must not push the button it sits beside off the panel. */
  .morph-set{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;
        white-space:nowrap;font:11px/1.5 var(--mono);color:var(--ink);
        background:var(--panel2);border:1px solid var(--line);border-radius:5px;
        padding:3px 8px}
  /* A worked example says two things: what it is, and what to do to see it.
     The second is marked, because it is the actionable half and a wall of
     undifferentiated prose gets skipped. */
  .note.sampletry{color:var(--ink);border-left:2px solid var(--bone);padding-left:8px;
        margin-top:6px}
  /* ---- the timeline ----
     Drawn rather than marked up, because a row per channel with a diamond per
     key is dynamic. `preserveAspectRatio:none` lets the view stretch to the
     panel's width while the controller keeps working in its own units, so a
     frame's x is arithmetic rather than a measurement. */
  /* The curve view. Same ground and frame as the timeline, because it is the
     same keys seen a second way — the timeline says WHEN, this says HOW. */
  svg.cvview{display:block;width:100%;height:124px;margin-top:8px;
        background:var(--panel2);border:1px solid var(--line);border-radius:6px;
        touch-action:none;cursor:crosshair}
  /* one faint upright per key frame, so the eye can relate the curve to the
     timeline above it */
  svg.cvview .cv-grid{stroke:var(--line);stroke-width:1;fill:none;
        vector-effect:non-scaling-stroke}
  /* the value zero: the only absolute reference a value axis has, and the
     line overshoot is measured against */
  svg.cvview .cv-zero{stroke:var(--line);stroke-width:1;stroke-dasharray:2 3;
        vector-effect:non-scaling-stroke}
  /* A COMPARISON CURVE: another channel, drawn for its timing rather than for
     editing. Faint and cool against the bone hue, so which curve is being
     edited is never a question — and thin, because several of them at once
     must not read as a thicket. No pointer events: the handles belong to the
     picked channel, and a ghost that could be grabbed would edit the wrong
     one. */
  svg.cvview .cv-ghost{fill:none;stroke:#64748b;stroke-width:1.1;
        stroke-opacity:.55;stroke-linejoin:round;pointer-events:none;
        vector-effect:non-scaling-stroke}
  /* the curve in the bone hue, like the keys it describes */
  svg.cvview .cv-line{fill:none;stroke:var(--bone);stroke-width:1.6;
        stroke-linejoin:round;vector-effect:non-scaling-stroke}
  svg.cvview .cv-key{fill:#0e1116;stroke:var(--bone);stroke-width:1.5;
        vector-effect:non-scaling-stroke}
  svg.cvview .cv-key.on{fill:var(--bone-on);stroke:var(--bone-on)}
  /* A handle is a GOAL — where the curve is being asked to go — so it borrows
     the target hue, the same distinction the canvas makes between a bone and
     the thing it reaches for. */
  svg.cvview .cv-stem{stroke:var(--target);stroke-width:1;stroke-opacity:.55;
        vector-effect:non-scaling-stroke}
  svg.cvview .cv-knob{fill:var(--target);stroke:#0e1116;stroke-width:1.25;
        vector-effect:non-scaling-stroke;cursor:grab}
  /* THE RECTANGLE BEING DRAGGED. Drawn in the target hue rather than the bone
     hue: it is a thing being reached for, not a thing that exists yet — the
     same distinction the canvas makes. It takes no pointer events, or it would
     swallow the very drag that is drawing it. */
  svg.tlview .tl-marquee{fill:var(--target);fill-opacity:.12;
        stroke:var(--target);stroke-width:1;stroke-dasharray:3 2;
        pointer-events:none;vector-effect:non-scaling-stroke}
  /* THE VIEW BAR. Right-hand side and ABOVE every hint, not beside one.

     Two hints share the bottom row: `.hint` on the left and `.selhint` — each
     tool's own — on the RIGHT, at the same `bottom:22px`. The stage is also
     narrower than the window by the panel's 400px, so a bar on that row
     overlaps one or the other long before any window breakpoint fires
     (measured at 1000px against `.hint`, and again against the hand tool's
     `.selhint` on the right). Sitting above both cannot collide at any width
     — as long as `bottom` clears the TALLER of them: `.hint`'s four lines
     reach 110px up the stage, the tools' hints 93px. Muted until hovered:
     it is furniture, not part of the picture. */
  .viewbar{position:absolute;right:14px;bottom:118px;display:flex;align-items:center;
        gap:6px;padding:5px 8px;border-radius:8px;background:#141a23cc;
        border:1px solid #222c38;font-size:11px;color:#7d8897;opacity:.72;
        transition:opacity .12s}
  .viewbar:hover{opacity:1}
  .viewbar button{min-width:26px;height:22px;padding:0 6px;border-radius:5px;
        background:#1b232e;color:#c7d0db;border:1px solid #2a3441;cursor:pointer;
        font:inherit;line-height:1}
  .viewbar button:hover{background:#232d3a;color:#e6ecf3}
  #zoomReadout{display:inline-block;min-width:34px;text-align:center}
  .viewhint{margin-left:4px;opacity:.75}
  /* The words go first when the stage is tight; the buttons never do. */
  @media (max-width:1250px){ .viewhint{display:none} }

  svg.tlview{display:block;width:100%;height:92px;margin-top:8px;
        background:var(--panel2);border:1px solid var(--line);border-radius:6px;
        touch-action:none;cursor:pointer}
  svg.tlview .tl-lane{fill:#0e1116}
  svg.tlview .tl-grid{stroke:var(--line);stroke-width:1;vector-effect:non-scaling-stroke}
  svg.tlview .tl-tick{fill:var(--ink-dim);font:9px var(--mono)}
  svg.tlview .tl-name{fill:var(--ink-dim);font:9px var(--mono)}
  /* A key is a diamond in the bone hue, so the timeline reads as part of the
     rig rather than as another blue control. The picked one is brighter and
     rimmed, which is the only difference a user needs to see. */
  svg.tlview .tl-key{fill:var(--bone);stroke:#0e1116;stroke-width:1;
        vector-effect:non-scaling-stroke}
  svg.tlview .tl-key.on{fill:var(--bone-on);stroke:var(--bone);stroke-width:1.5}
  /* the playhead has to be findable against both the lane and a key */
  svg.tlview .tl-head{stroke:var(--r);stroke-width:1.5;vector-effect:non-scaling-stroke}
  svg.tlview .tl-headtop{fill:var(--r)}
  .checks label{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--ink);cursor:pointer}
  input[type=checkbox]{accent-color:var(--r);width:13px;height:13px;cursor:pointer;
        background:var(--panel2)}
  .stats{display:grid;grid-template-columns:auto 1fr;gap:5px 14px;font:11.5px/1.45 var(--mono)}
  .stats dt{color:var(--ink-dim)}
  .stats dd{margin:0;color:var(--ink-bright);text-align:right}
  pre.out{margin:10px 0 0;padding:9px 10px;background:#0b0f14;border:1px solid var(--line);border-radius:6px;
          font:11px/1.55 var(--mono);color:#9fb3c8;white-space:pre-wrap;word-break:break-word;max-height:230px;overflow:auto}
  pre.out .ok{color:var(--ok)} pre.out .bad{color:var(--bad)} pre.out .dim{color:#5f6c7d}
  pre.out b{color:var(--ink-bright);font-weight:600}
  details{margin-top:8px}
  summary{cursor:pointer;font-size:11.5px;color:var(--ink-dim);list-style:none;user-select:none}
  summary::-webkit-details-marker{display:none}
  summary::before{content:"▸ ";color:#4a596b}
  details[open] > summary::before{content:"▾ "}
  summary:hover{color:var(--ink)}
  .note{font-size:11.5px;line-height:1.65;color:var(--ink-dim);margin:9px 0 0}
  .note code{font-family:var(--mono);font-size:10.5px;color:#9dbfd8;background:#0b0f14;padding:1px 4px;border-radius:3px}
  .note b{color:var(--ink)}
  .note ol,.note ul{margin:7px 0 0;padding-left:18px}
  .note li{margin:4px 0}
  .badge{float:right;font:10px/1 var(--mono);padding:3px 6px;border-radius:4px;background:#12241c;
         color:var(--ok);border:1px solid #1e4032}
  .badge.bad{background:#2a1416;color:var(--bad);border-color:#4a2126}
  @media (max-width:980px){
    .app{grid-template-columns:1fr;grid-template-rows:52vh 1fr;height:auto;min-height:100vh}
    body{overflow:auto}
    .panel{border-left:none;border-top:1px solid var(--line)}
    /* The stage is short here, so the project bar and the legend would meet in
       the middle. The bar keeps the corner and gives up its label; the note is
       held to what is left rather than running under the legend. */
    .projbar label{display:none}
    .projbar select{max-width:180px}
    .projnote{max-width:min(380px, calc(100% - 52px))}
  }
  /* an applied intersection's overlaps: seen, never hit — the group's own
     (invisible) path takes the pointer, so it still selects and drags */
  .pieces{pointer-events:none}

  /* ---------- the operation's own objects ---------- */
  #opList{margin-top:6px}
  /* the row itself does nothing on a click — its checkbox picks — so it
     does not look clickable */
  .obj.opobj{cursor:default}
  .obj.opobj .nm2{flex:1;min-width:0}
  /* a shape inside a member group: listed so the group can be read, but it
     takes part through the group, so it is drawn a step quieter */
  .obj.opobj.opchild .nm2{color:var(--ink-dim)}
  /* ---------- the context menu ----------
     A floating list of actions, drawn like the colour popover so it reads as
     part of the same app; disabled items stay visible, greyed, with a reason. */
  .ctxmenu{position:fixed;z-index:70;min-width:190px;padding:5px;display:flex;flex-direction:column;
        background:var(--panel2);border:1px solid #33404f;border-radius:8px;
        box-shadow:0 12px 30px rgba(0,0,0,.55)}
  .ctxitem{all:unset;box-sizing:border-box;padding:6px 10px;border-radius:5px;font-size:12px;
        color:var(--ink);cursor:pointer;white-space:nowrap}
  .ctxitem:hover:not(:disabled),.ctxitem:focus-visible{background:#223041;color:var(--ink-bright)}
  .ctxitem:disabled{color:var(--ink-dim);opacity:.55;cursor:default}
  .ctxsep{height:1px;margin:4px 6px;background:var(--line)}
  /* the preview toggle, pressed: the same "chosen" green the operations use */
  #btnOpPreview.on{background:#26351a;border-color:#4a6b28;color:#ddf5b4}
  #btnOpPreview.on:hover{background:#2c3d1e}
  /* ---------- the row toggles: visibility and lock ----------
     Icons, not glyphs, and the same ones as the panel's show all / hide all.
     An open padlock is the ordinary state, so it is drawn quieter; a closed
     one is the exception worth noticing. */
  .obj .mini.eye,.obj .mini.lock{padding:2px 4px;display:inline-flex;align-items:center;justify-content:center}
  .obj .mini svg{width:13px;height:13px;display:block}
  .obj .mini.eye.off{color:var(--ink-dim)}
  .obj .mini.lock{color:var(--ink-dim);opacity:.7}
  .obj .mini.lock.on{color:var(--warn);opacity:1;border-color:#6b5a1c}
