  /* THE PAPER SKIN, which is also the fallback. Every value here is
     overridden at runtime by applyTheme(); they are written out in full so
     that the stylesheet still renders correctly if the script never runs.

     WHAT IS NOT HERE: the colours that carry an analytic meaning — the probe,
     the governor, the subject, a crossed bounding node, an intervener. Those
     are what the legend on the canvas explains, and their separation from one
     another was derived by measurement over several builds, so a skin must not
     restate them. They live as literals at the point of use and are the same
     under every skin. The colours below are surfaces and chrome, plus the ink
     the tree is drawn in ON SCREEN; an export never reads any of them. */
  :root {
    /* TWO RADII, NOT NINE. The file had 1, 2, 3, 4, 5, 6, 7, 8, 10, 12 and 30
     pixel corners side by side — not a scale, just whatever was typed at the
     time, which is what makes an interface read as assembled rather than
     designed. Controls take one step, the surfaces that hold them take the
     other, and anything genuinely circular keeps its 50%.

     Small, because this program draws trees: a syntax tree is all straight
     lines and sharp junctions, and a chrome of soft pillows around it argues
     with the thing it is there to show. */
  /* ONE TRACKING VALUE for the small-caps labels. The file had 0.04, 0.05,
     0.06, 0.08, 0.09, 0.1 and 0.14em in the same interface — again not a scale
     but a series of guesses, and the differences are invisible except as a
     faint sense that nothing quite lines up. */
  --label-track: 0.06em;

  /* A SPACING SCALE, replacing eighteen values that had grown one rule at a
     time — 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 16, 18, 24 and 32 pixels in
     one interface. Most of them were already near a step of the scale below;
     what they were not was chosen. Two rules that differ by a pixel look, to a
     reader, like two rules that failed to line up.

     The steps are not a doubling series: a doubling scale gives 4, 8, 16, 32,
     which is too coarse for the dense controls this program is full of, and
     forces everything into either cramped or roomy with nothing between. This
     is finer at the bottom where the controls live and coarser at the top where
     the panels do. */
  /* THE THINGS A STYLE IS ACTUALLY MADE OF. Colour alone cannot carry a style:
     two skins that differ only in hue are two palettes. What distinguishes a
     Swiss panel from a brutalist one is the weight of its rules, whether its
     shadow is a blur or a hard offset block, how heavy its labels sit, and
     which typeface the chrome speaks in. All of that was written into the
     stylesheet as literals, so a skin could not reach it — which is why the
     first two style skins came out looking like recoloured versions of the
     same thing.

     The TREE is deliberately not here. Its serif, its line weight and its
     spacing belong to the figure rather than to the interface, and a reader
     switching skins must not find the diagram they are drawing has changed
     shape underneath them. */
  --bd-hair: 0.5px;    /* the default rule between controls */
  --bd-line: 1px;      /* panel and dialog edges */
  --bd-bold: 2px;      /* emphasis; the whole vocabulary in a brutalist skin */
  --shadow-panel: 0 2px 8px var(--shadow);
  --shadow-float: 0 4px 16px var(--shadow);
  --shadow-modal: 0 12px 48px var(--shadow-strong);
  --fw-label: 500;     /* section headings and control labels */
  --fw-strong: 600;    /* the one item that must be read first */
  --ui-sans: 'Inter', system-ui, sans-serif;   /* chrome only, never the tree */
  /* THE DISPLAY FACE — the wordmark, dialog titles, the empty-state heading.
     Three elements is not many, but they are the only text in the interface
     with room to show a typeface, and separating them from the body text is
     what lets a skin set two families against each other rather than one.
     Defaults to the body face, so a skin that does not care sees no change. */
  /* THE WORDMARK is the one place in the interface with a voice of its own, and
     each style has a different idea of how a name should be set. The face and
     size already followed the skin; the slant, the weight and the tracking did
     not, so every skin wore the same italic regardless of whether italic suited
     it — a console labels its build upright and heavy, a magazine sets its
     masthead in italic serif, and a brutalist one shouts in caps. */
  /* HOW AN ACTIVE CONTROL LOOKS. Every skin so far said the same thing — invert
     it, ink behind and paper in front — which is a perfectly good convention
     and the wrong one for a style whose whole language is light and depth. A
     neumorphic control that is ON should be PRESSED IN: the same surface, lit
     from the other side.

     Three tokens rather than one, because inverting needs a background and a
     text colour while pressing needs a shadow, and a skin should be able to use
     either without carrying the other. */
  --active-bg: var(--ink);
  --active-ink: var(--ink-inverse);
  --active-shadow: none;

  /* THE MOMENT OF PRESSING, which is not the same as being ON. A two-value
     switch like the merge-side control is always on one side or the other, so
     a permanent pressed look would say nothing; what it wants is feedback while
     the finger is down. Flat skins nudge it half a pixel, and the neumorphic
     one sinks it into the surface, which is what pressing means there. */
  --canvas-pattern: radial-gradient(circle at 1px 1px, var(--canvas-dot) 1px, transparent 0);
  --canvas-pattern-size: 20px 20px;

  /* HOW QUICKLY A CONTROL ANSWERS. Only the controls: the fades on the domain
     overlay, the canvas notice and the present-mode stage are about giving a
     reader time to notice something appearing, which is a matter of legibility
     rather than of style, and they stay where they are.

     A brutalist interface should snap — no easing, no duration, the state is
     simply the other one now — while a neumorphic surface takes its time
     because a soft material does. That difference is felt rather than seen,
     which is why it is worth having and why it was invisible while every skin
     shared one number. */
  /* THE HELP DOCUMENT is the only extended prose in the program, and it was set
     the same way under every skin: one size, one leading, one measure. That is
     the part of a style that a reader spends the longest inside, and the part
     where the differences between styles were originally worked out — a
     magazine sets its text open and a terminal sets it tight, and neither is a
     decoration on top of the words.

     The reading FACE is deliberately not here. Body text set in the display
     serif at thirteen pixels was tried at v256 and abandoned: at that size a
     serif shows none of its character and reads as merely old. */
  /* THE READING FACE, separate from the chrome face. The help text inherited
     the interface one, which is right for most skins and wrong where the chrome
     is set in something built for labels: the brutalist skin would have put a
     page of prose in Arial Black, and a display weight is exhausting at
     paragraph length. Defaults to the chrome face, so a skin that has no
     opinion needs to say nothing.

     A serif for the body was tried across the whole interface at v256 and
     abandoned — at label sizes it showed none of its character — but that
     argument was about nine-pixel labels. At fourteen pixels over several
     paragraphs a serif does what it is for, which is why the editorial skin
     takes one here and takes none anywhere else. */
  --doc-face: var(--ui-sans);
  --doc-size: 13.5px;
  --doc-leading: 1.75;

  --ease-control: 0.12s ease;

  --press-transform: translateY(0.5px);
  --press-shadow: none;

  --brand-style: italic;
  --brand-weight: var(--fw-label);
  --brand-track: 0;
  --brand-case: none;

  --ui-display: var(--ui-sans);
  --fs-display: 15px;
  /* The violation notice, set by the reader from the View menu. Every size
     inside that panel is in em, so this one moves all of them. */
  --fs-notice: 13px;

  --sp-1: 2px;    /* hairline padding inside the smallest chips */
  --sp-2: 4px;    /* tight control padding, vertical */
  --sp-3: 6px;    /* standard control padding, vertical */
  --sp-4: 8px;    /* standard control padding, horizontal; panel gutters */
  --sp-5: 12px;   /* panel padding */
  --sp-6: 16px;   /* dialog padding */
  --sp-7: 24px;   /* dialog outer margins */

  --r-control: 2px;
  --r-surface: 4px;
  --bg: #FAF8F4;
    --bg-canvas: #fefdfb;
    --bg-toolbar: #FAF8F4;
    --bg-soft: #F5F2EB;
    --surface: #ffffff;
    --surface-strong: #1B1A17;
    --stash-bg: #F4F0E5;
    --ink: #1B1A17;
    --ink-soft: #5C5A53;
    --ink-faint: #9B9890;
    --ink-inverse: #ffffff;
    --line: #E5E1D7;
    --line-strong: #C9C4B6;
    --canvas-dot: #E5E1D7;
    --trace-grey: #888888;
    /* The ink the tree is drawn in on screen. An export uses PAPER_INK
       instead, which is this same value frozen as a literal. */
    --tree-line: #2C2A26;
    --tree-ink-strong: #1B1A17;
    --accent: #B5471F;
    --accent-strong: #9c3d1a;
    --accent-wash: #FBEFE8;
    --accent-tint: rgba(181, 71, 31, 0.07);
    --accent-tint-soft: rgba(181, 71, 31, 0.04);
    --accent-tint-strong: rgba(181, 71, 31, 0.35);
    --glass: rgba(252, 251, 249, 0.88);
    --glass-strong: rgba(252, 251, 249, 0.94);
    --glass-soft: rgba(255, 255, 255, 0.40);
    --hover-tint: rgba(0, 0, 0, 0.02);
    --backdrop: rgba(20, 18, 15, 0.34);
    --shadow: rgba(44, 42, 38, 0.10);
    --shadow-strong: rgba(0, 0, 0, 0.28);
    --scroll-thumb: rgba(120, 110, 95, 0.30);
    --scroll-thumb-hover: rgba(80, 70, 55, 0.55);
    --scroll-thumb-active: rgba(60, 50, 40, 0.75);
    --badge: #d33;
    --danger-bg: #fee;
    --danger-ink: #900;
    --danger-line: #faa;
    --warn-bg: rgba(255, 250, 245, 0.97);
    --warn-line: rgba(176, 106, 90, 0.45);
    --warn-ink: #8A4B26;
    --err-bg: rgba(255, 244, 242, 0.95);
    --err-line: rgba(192, 57, 43, 0.40);
    --err-ink: #922B21;
    --ok-ink: #2F6F4E;
    --serif: 'Fraunces', Georgia, serif;
    --tree-serif: Georgia, 'Times New Roman', serif;
    --sans: 'Inter', system-ui, sans-serif;
    --mono: 'JetBrains Mono', monospace;
  }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  html, body { height: 100%; overflow: hidden; }
  /* SVG text in the tree uses Georgia. Georgia's default zero is an
     oldstyle figure (smaller, oval, sits low) which looks cramped in
     superscript and can be confused with lowercase 'o'. Force lining
     figures so 0 has full cap-height like in academic papers. */
  svg text {
    font-variant-numeric: lining-nums;
  }
  body {
    font-family: var(--ui-sans);
    background: var(--bg);
    color: var(--ink);
    font-size: 14px;
    -webkit-font-smoothing: antialiased;
  }
  .app {
    display: grid;
    grid-template-rows: 36px 48px 1fr;
    height: 100vh;
  }

  .menubar {
    display: flex;
    align-items: center;
    padding: 0 var(--sp-6);
    gap: 18px;
    background: var(--bg-canvas);
    border-bottom: var(--bd-hair) solid var(--line);
  }
  .brand {
    font-family: var(--ui-display);
    font-style: var(--brand-style);
    font-size: var(--fs-display);
    font-weight: var(--brand-weight);
    letter-spacing: var(--brand-track);
    text-transform: var(--brand-case);
    /* The wordmark is longer than the old one; keep it on a single line and
       out of the flex squeeze so a narrow window compresses the spacer
       instead of the name. */
    flex: none;
    white-space: nowrap;
  }
  .divider {
    width: 1px;
    height: 14px;
    background: var(--line);
  }
  .menu-item {
    font-size: 13px;
    color: var(--ink-soft);
    cursor: pointer;
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--r-control);
  }
  .menu-item:hover { background: var(--line); color: var(--ink); }
  .menubar-spacer { flex: 1; }
  .doc-name {
    font-size: 12px;
    color: var(--ink-faint);
    font-style: italic;
  }
  /* Once the tree has a real name it is no longer a placeholder, so it drops
     the faint italic treatment. */
  .doc-name.doc-name-set {
    color: var(--ink-soft);
    font-style: normal;
  }
  .lang-btn {
    font-family: var(--ui-sans);
    font-size: 11px;
    color: var(--ink-soft);
    cursor: pointer;
    padding: var(--sp-1) var(--sp-4);
    margin-left: 12px;
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong);
    user-select: none;
    letter-spacing: 0.04em;
    min-width: 28px;
    text-align: center;
  }
  .lang-btn:hover { background: var(--line); color: var(--ink); }

  .menu-dropdown {
    position: absolute;
    top: 36px;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    box-shadow: var(--shadow-float);
    padding: var(--sp-1);
    min-width: 180px;
    z-index: 30;
    /* Don't let it extend past the viewport. The script also caps this with
       a more accurate value at open time. */
    max-height: calc(100vh - 60px);
    overflow-y: auto;
  }
  .menu-dropdown-item {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--sp-2) var(--sp-4);
    font-size: 12px;
    color: var(--ink);
    border-radius: var(--r-control);
    cursor: pointer;
    user-select: none;
  }
  .menu-dropdown-item:hover { background: var(--bg); }
  /* pointer-events is the part that makes this honest. Greying an item that
     still responds to a click is worse than not greying it, because the reader
     is told the action is unavailable and then it happens anyway. */
  .menu-dropdown-item.disabled {
    color: var(--ink-faint);
    cursor: default;
    opacity: 0.55;
    pointer-events: none;
  }
  .menu-dropdown-item.disabled:hover { background: transparent; }
  .menu-dropdown-item.danger:hover { background: var(--danger-bg); color: var(--danger-ink); }
  /* Two rows of swatches inside a menu. Not menu items — they do not highlight
     on hover as a row, because the target is the chip, not the line. */
  .node-color-row {
    display: flex; align-items: center; gap: 5px;
    padding: var(--sp-2) var(--sp-5) var(--sp-2) var(--sp-5);
  }
  .node-color-caption {
    font-size: 11.5px; color: var(--ink-soft);
    width: 42px; flex: none;
  }
  /* Inside the arrow popup the swatch row sits among rows whose labels are 9px
     mono, uppercase, letter-spaced. The shared builder prints an ordinary
     11.5px caption, which read as a different control dropped into the middle
     of the panel. Same row, same label. */
  .arrow-popup .node-color-caption {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    width: 46px;
  }
  .arrow-popup .node-color-row { padding: var(--sp-2) 0; gap: 5px; }

  .node-color-chip {
    width: 15px; height: 15px; border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong);
    cursor: pointer; flex: none;
  }
  .node-color-chip:hover { outline: 2px solid var(--accent); outline-offset: 1px; }
  .node-color-chip.is-on { outline: 2px solid var(--accent); outline-offset: 1px; }
  /* The colour wheel, wearing the same conic icon the annotation toolbar uses.
     The native input is laid over it at zero opacity so the whole swatch is the
     target and the icon stays the same under every skin. */
  .node-color-wheel {
    position: relative; overflow: hidden;
    width: 15px; height: 15px; flex: none; cursor: pointer;
    border: var(--bd-hair) solid var(--line-strong); border-radius: var(--r-control);
    margin-left: 3px;
    background: conic-gradient(from 0deg, red, yellow, lime, cyan, blue, magenta, red);
  }
  .node-color-wheel input[type="color"] {
    position: absolute; inset: 0; width: 100%; height: 100%;
    opacity: 0; padding: 0; border: none; background: none; cursor: pointer;
  }
  .node-color-wheel:hover,
  .node-color-wheel.is-on { outline: 2px solid var(--accent); outline-offset: 1px; }
  /* ONE BUTTON, not a row of swatches. Seven chips sitting permanently beside
     B / I / U made the bar read as a palette with some text controls attached,
     when colour is used far less often than any of them. The bar under the
     letter carries the last colour used, so the button says what it will do. */
  .fmt-color-btn { position: relative; padding-bottom: 5px; }
  .fmt-color-bar {
    position: absolute; left: 5px; right: 5px; bottom: 3px; height: 3px;
    border-radius: var(--r-control); background: var(--ink);
  }
  .fmt-color-menu {
    position: fixed;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    box-shadow: var(--shadow-panel);
    padding: var(--sp-4); z-index: 1000; width: 176px;
  }
  .fmt-color-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; }
  .fmt-color {
    width: 18px; height: 18px; border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong); cursor: pointer;
  }
  .fmt-color:hover { outline: 2px solid var(--accent); outline-offset: 1px; }
  .fmt-color.is-default {
    background: linear-gradient(135deg,
      transparent 44%, var(--ink-faint) 44%, var(--ink-faint) 56%, transparent 56%);
  }
  .fmt-custom-row {
    display: flex; align-items: center; gap: 7px;
    margin-top: 8px; padding-top: 8px; border-top: var(--bd-hair) solid var(--line);
    font-size: 11.5px; color: var(--ink-soft);
  }

  .format-bar.disabled .fmt-color-btn { pointer-events: none; }
  /* The first chip clears the override rather than setting a colour, so it
     shows the absence of one instead of a colour that could be picked. */
  .node-color-chip.is-default {
    background: linear-gradient(135deg,
      transparent 44%, var(--ink-faint) 44%, var(--ink-faint) 56%, transparent 56%);
  }
  /* ONE LOOK FOR A PRINTED SHORTCUT, in every menu: the item's own face,
     faded, set off to the right. It was monospaced and small in most menus
     and plain in the Edit menu, and the author chose the plain one. */
  .menu-dropdown-item .shortcut {
    font-family: inherit;
    font-size: inherit;
    opacity: 0.5;
    margin-left: 18px;
    float: right;
  }
  .menu-dropdown-section:first-child {
    border-top: 0;
    margin-top: 0;
    padding-top: var(--sp-2);
  }
  /* A HEADING AFTER AN EXPLICIT DIVIDER draws no rule of its own: eight menus
     already insert one by hand, and two lines a few pixels apart look like a
     mistake rather than a stronger break. Handled here rather than by deleting
     those eight elements, because a divider is also used BETWEEN items with no
     heading after it, and because the next menu to insert one by hand should
     not have to know about this. */
  .menu-dropdown-divider + .menu-dropdown-section {
    border-top: 0;
    margin-top: 0;
    padding-top: var(--sp-2);
  }
  /* A SUBMENU opens to the right of its item on hover, or on a click for a
     pointer that cannot hover. The dropdown that holds submenus must not
     clip them: it is short and never needs to scroll. */
  .menu-dropdown.menu-has-submenus { overflow: visible; }
  .menu-dropdown-item.has-submenu { position: relative; }
  .menu-dropdown-item.has-submenu > .menu-submenu-caret {
    position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
    opacity: 0.55; font-size: 0.85em;
  }
  .menu-submenu {
    display: none;
    position: absolute;
    left: calc(100% - 2px);
    top: -6px;
    min-width: 220px;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    box-shadow: var(--shadow-float);
    padding: var(--sp-1);
    z-index: 31;
  }
  .menu-dropdown-item.has-submenu:hover > .menu-submenu,
  .menu-dropdown-item.has-submenu.is-open > .menu-submenu { display: block; }
  .menu-dropdown-divider {
    height: 1px;
    background: var(--line);
    margin: 3px 2px;
  }
  /* ---- Chinese typography for the small-caps label style ----------------
     Every rule below is a label or section heading set at 9-10px, faint, and
     uppercase. That is tuned for Latin, where capitals stay readable at that
     size and the caps themselves signal "label". Chinese gets no signal from
     text-transform and 9px glyphs are too small and too pale to read, so the
     same rules leave a Chinese UI with headings weaker than the text beneath
     them. Under data-lang="zh" they get a larger size, more contrast, and no
     letter-spacing (which only harms CJK). English is untouched. */
  html[data-lang="zh"] .menu-dropdown-section,
  html[data-lang="zh"] .stash-pane-header,
  html[data-lang="zh"] .tree-legend-title,
  html[data-lang="zh"] .movement-popup-section-label,
  html[data-lang="zh"] .popup-banner {
    font-size: 11.5px;
    color: var(--ink-soft, #8A857C);
    font-weight: var(--fw-strong);
    letter-spacing: 0;
  }
  html[data-lang="zh"] .popup .field-label,
  html[data-lang="zh"] .arrow-popup-row-label,
  html[data-lang="zh"] .arrow-popup-mini-label,
  html[data-lang="zh"] .movement-popup-section-label,
  html[data-lang="zh"] .pc-speed-label,
  html[data-lang="zh"] .annotation-toolbar .ann-tb-label,
  html[data-lang="zh"] .head-toggle .ht-label {
    font-size: 11px;
    letter-spacing: 0;
  }
  html[data-lang="zh"] .binding-toggle,
  html[data-lang="zh"] .popup .word-line-add {
    font-size: 11.5px;
    letter-spacing: 0;
  }

  /* Explanatory line under a menu item. Some options cannot be understood
     from a name alone; showing the two resulting shapes is more direct than
     describing them. */
  .menu-dropdown-sub { padding-left: 30px; }
  /* WITH THE MASTER SWITCH OFF, a sub-item keeps its tick but stops doing
     anything, and an untouched tick reads as `this is in effect`. Greyed, the
     tick still records the choice — it comes back when islands are switched on
     again — while saying plainly that it is not doing anything now. */
  .menu-dropdown-sub.is-inactive { opacity: 0.42; }
  /* The 32px here is an INDENT, not a rhythm step: it aligns this line with the
     text of the menu items above it, past their icon column. Snapping it to the
     scale would break that alignment to gain a consistency it is not part of. */
  .menu-dropdown-hint {
    font-size: 11px;
    line-height: 1.55;
    color: var(--ink-soft, #8A857C);
    padding: 0 var(--sp-5) var(--sp-4) 32px;
    max-width: 300px;
  }
  .menu-dropdown-hint code {
    font-family: ui-monospace, Menlo, monospace;
    font-size: 11px;
    color: var(--ink, #2c2a26);
  }

  /* A HEADING CARRIES ITS OWN RULE, so every menu with sections gets the
     separation without anyone remembering to insert a divider element — which
     is how File and View came to have them while Movement and Analysis, the two
     with the most headings, did not. The line goes ABOVE the heading: what it
     separates is the group that just ended from the one about to start. */
  .menu-dropdown-section {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    border-top: var(--bd-line) solid var(--line);
    margin-top: var(--sp-2);
    padding: var(--sp-3) var(--sp-4) var(--sp-1);
    user-select: none;
  }
  .menu-dropdown-slider-row {
    padding: var(--sp-1) var(--sp-4) var(--sp-2);
  }
  .menu-dropdown-slider {
    width: 100%;
    cursor: pointer;
    height: 4px;
  }
  /* Tree spacing UI — slider with label on left and value on right */
  .menu-dropdown-spacing-row {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: var(--sp-1) var(--sp-4);
  }
  .menu-dropdown-spacing-label {
    flex: 0 0 60px;
    font-size: 10px;
    color: var(--ink-soft);
  }
  .menu-dropdown-spacing-row .menu-dropdown-slider {
    flex: 1;
  }
  .menu-dropdown-spacing-value {
    flex: 0 0 36px;
    text-align: right;
    font-size: 10px;
    color: var(--ink-soft);
    font-family: var(--mono);
  }
  .menu-dropdown-presets-row {
    display: flex;
    gap: 4px;
    padding: var(--sp-2) var(--sp-4) var(--sp-3);
  }
  .menu-dropdown-preset-btn {
    flex: 1;
    height: 22px;
    padding: 0 var(--sp-2);
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong);
    background: var(--surface);
    color: var(--ink);
    font-family: var(--ui-sans);
    font-size: 10.5px;
    cursor: pointer;
  }
  .menu-dropdown-preset-btn:hover { background: var(--bg-toolbar); }
  .menu-dropdown-item .check {
    display: inline-block;
    width: 12px;
    color: var(--ink);
    margin-right: 4px;
    flex-shrink: 0;
    font-size: 11px;
  }
  .menu-dropdown-item .label-with-check {
    display: flex;
    align-items: center;
    flex: 1;
  }
  /* On short viewports (laptop screens), tighten further. */
  @media (max-height: 720px) {
    .menu-dropdown { padding: var(--sp-1); min-width: 170px; }
    .menu-dropdown-item { padding: var(--sp-1) var(--sp-3); font-size: 11px; }
    .menu-dropdown-section { font-size: 8.5px; padding: var(--sp-1) var(--sp-3) var(--sp-1); letter-spacing: var(--label-track); }
    .menu-dropdown-divider { margin: 2px 2px; }
    .menu-dropdown-slider-row { padding: var(--sp-1) var(--sp-3) var(--sp-2); }
  }

  .toolbar {
    display: flex;
    align-items: center;
    padding: 0 var(--sp-6);
    gap: 10px;
    background: var(--bg-toolbar);
    border-bottom: var(--bd-hair) solid var(--line);
    position: relative;
  }
  .btn {
    height: 30px;
    padding: 0 var(--sp-5);
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong);
    background: var(--surface);
    color: var(--ink);
    font-family: var(--ui-sans);
    font-size: 13px;
    font-weight: var(--fw-label);
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    transition: all var(--ease-control);
  }
  .btn:hover:not(:disabled) { background: var(--bg); border-color: var(--ink-faint); }
  .btn:active:not(:disabled) { transform: var(--press-transform); box-shadow: var(--press-shadow); }
  .btn:disabled { opacity: 0.35; cursor: not-allowed; }
  .btn-icon {
    width: 30px;
    padding: 0;
    justify-content: center;
  }
  .btn-add {
    background: var(--surface);
    color: var(--ink);
  }
  .btn-merge {
    background: var(--accent);
    color: var(--ink-inverse);
    border-color: var(--accent);
  }
  .btn-merge:hover:not(:disabled) {
    background: var(--accent-strong);
    border-color: var(--accent-strong);
  }
  .btn-project {
    background: var(--surface);
    color: var(--accent);
    border-color: var(--accent);
  }
  .btn-project:hover:not(:disabled) {
    background: var(--accent-wash);
  }
  .btn-annotation {
    background: var(--surface);
    color: var(--ink-soft);
    border-color: var(--line-strong);
  }
  .btn-annotation:hover:not(:disabled) {
    background: var(--bg);
    color: var(--ink);
    border-color: var(--ink);
  }
  .annotation-btn-wrap {
    position: relative;
    display: inline-block;
  }
  .annotation-menu {
    position: absolute;
    top: calc(100% + 4px);
    left: 0;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    box-shadow: var(--shadow-panel);
    padding: var(--sp-2);
    z-index: 1000;
    min-width: 140px;
  }
  .annotation-menu-item {
    padding: var(--sp-3) var(--sp-4);
    font-size: 12px;
    color: var(--ink);
    border-radius: var(--r-control);
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .annotation-menu-item:hover { background: var(--bg); }
  .annotation-menu-item .glyph {
    width: 18px;
    text-align: center;
    font-family: var(--tree-serif);
    color: var(--ink-soft);
  }

  /* When user has armed an annotation tool, the canvas shows a crosshair
     cursor to indicate "click to drop". */
  .canvas-area.annotation-arming { cursor: crosshair !important; }
  .canvas-area.annotation-arming * { cursor: crosshair !important; }

  /* Annotation visual layer (foreign objects rendered as absolutely-
     positioned divs above the SVG). */
  .annotation-layer {
    position: absolute;
    inset: 0;
    pointer-events: none;       /* let SVG-tree clicks through where empty */
    z-index: 5;
  }
  .annotation-item {
    position: absolute;
    pointer-events: auto;
    cursor: move;
    user-select: none;
    transform-origin: 50% 50%;   /* rotate/scale around visual center */
  }
  .annotation-item .ann-content {
    width: 100%;
    height: 100%;
    box-sizing: border-box;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .annotation-item.type-text .ann-content {
    font-family: var(--tree-serif);
    color: var(--ink);
    white-space: nowrap;
    padding: var(--sp-1) var(--sp-3);
  }
  /* Endpoint types: the wrapper div is just a bbox holding the SVG.
     The SVG <path>/<line> uses pointer-events:stroke (set inline) so
     only the actual line is clickable, not the bbox empty area. */
  .annotation-item.type-arrow,
  .annotation-item.type-line {
    pointer-events: none;
    cursor: default;
  }
  .annotation-item.type-arrow .ann-content,
  .annotation-item.type-line .ann-content {
    pointer-events: none;
    overflow: visible;
  }
  .annotation-item.type-arrow svg,
  .annotation-item.type-line svg {
    pointer-events: none;
  }
  .annotation-item.type-arrow svg *,
  .annotation-item.type-line svg * {
    pointer-events: auto;
    cursor: move;
  }
  .annotation-item.editing {
    cursor: text;
  }
  .annotation-item.editing .ann-content {
    outline: 1px dashed var(--accent);
    background: var(--glass);
    overflow: visible;  /* let text grow past the box while typing */
  }
  .annotation-item.editing [contenteditable="true"] {
    outline: none;
    min-width: 12px;
  }

  /* Selection overlay (border + handles). One layer per selected annotation. */
  .annotation-selection {
    position: absolute;
    pointer-events: none;
    transform-origin: 50% 50%;
    border: var(--bd-line) solid var(--accent);
    box-sizing: border-box;
  }
  .annotation-handle {
    position: absolute;
    width: 9px;
    height: 9px;
    background: var(--surface);
    border: var(--bd-line) solid var(--accent);
    border-radius: var(--r-control);
    pointer-events: auto;
    box-sizing: border-box;
  }
  .annotation-handle.h-nw { left: -5px; top: -5px; cursor: nwse-resize; }
  .annotation-handle.h-n  { left: 50%; top: -5px; transform: translateX(-50%); cursor: ns-resize; }
  .annotation-handle.h-ne { right: -5px; top: -5px; cursor: nesw-resize; }
  .annotation-handle.h-e  { right: -5px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
  .annotation-handle.h-se { right: -5px; bottom: -5px; cursor: nwse-resize; }
  .annotation-handle.h-s  { left: 50%; bottom: -5px; transform: translateX(-50%); cursor: ns-resize; }
  .annotation-handle.h-sw { left: -5px; bottom: -5px; cursor: nesw-resize; }
  .annotation-handle.h-w  { left: -5px; top: 50%; transform: translateY(-50%); cursor: ew-resize; }
  .annotation-rotate-handle {
    position: absolute;
    left: 50%;
    top: -28px;
    width: 11px;
    height: 11px;
    margin-left: -6px;
    background: var(--surface);
    border: var(--bd-line) solid var(--accent);
    border-radius: 50%;
    pointer-events: auto;
    cursor: grab;
  }
  .annotation-rotate-handle::after {
    /* short line connecting the rotate handle to the box */
    content: '';
    position: absolute;
    left: 50%;
    top: 100%;
    width: 1px;
    height: 18px;
    background: var(--accent);
    transform: translateX(-50%);
  }
  .annotation-rotate-handle:active { cursor: grabbing; }

  /* Endpoint-mode selection: no bbox border, just round handles at each
     endpoint of the line/arrow. */
  .annotation-selection.endpoint-mode { border: none; }
  .annotation-endpoint-handle {
    position: absolute;
    width: 11px;
    height: 11px;
    margin-left: -6px;
    margin-top: -6px;
    background: var(--surface);
    border: 1.5px solid var(--accent);
    border-radius: 50%;
    pointer-events: auto;
    cursor: move;
    box-sizing: border-box;
  }
  .annotation-curve-handle {
    position: absolute;
    width: 9px;
    height: 9px;
    margin-left: -5px;
    margin-top: -5px;
    background: var(--accent);
    border: var(--bd-line) solid var(--surface);
    border-radius: 50%;
    pointer-events: auto;
    cursor: ns-resize;
    box-sizing: border-box;
    opacity: 0.6;
  }
  .annotation-curve-handle:hover { opacity: 1; }

  /* Marquee (rubber-band) selection rectangle drawn while the user drags
     across empty canvas to box-select annotations. */
  .annotation-marquee {
    position: absolute;
    border: var(--bd-line) solid var(--accent);
    background: var(--accent-tint);
    pointer-events: none;
    z-index: 50;
    box-sizing: border-box;
  }

  /* Transient snap guide line drawn while the user is dragging an
     annotation near an alignment edge. */
  .annotation-snap-guide {
    position: absolute;
    background: var(--badge);
    pointer-events: none;
    z-index: 40;
    opacity: 0.85;
  }

  /* Multi-select: hide handles, keep just the dotted-style border. The
     selection overlay still uses the accent color but visually different
     from single-select so users see "one is primary". */
  .annotation-selection.multi-mode {
    border-style: dashed;
    border-color: var(--accent);
  }

  /* Contextual toolbar below a selected annotation. Holds whatever
     controls apply to the annotation's type (font size, direction,
     stroke, color, fill, etc). */
  .annotation-toolbar {
    position: absolute;
    display: flex;
    gap: 2px;
    align-items: center;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    padding: var(--sp-1) var(--sp-2);
    box-shadow: var(--shadow-panel);
    pointer-events: auto;
    z-index: 10;
    font-family: var(--ui-sans);
    font-size: 11px;
    color: var(--ink);
    white-space: nowrap;
  }
  .annotation-toolbar .ann-tb-btn {
    width: 26px;
    height: 26px;
    border: none;
    background: transparent;
    color: var(--ink);
    font-size: 15px;
    line-height: 1;
    border-radius: var(--r-control);
    cursor: pointer;
    padding: 0;
    font-family: var(--tree-serif);
  }
  .annotation-toolbar .ann-tb-btn:hover { background: var(--bg); }
  .annotation-toolbar .ann-tb-btn.active {
    background: var(--accent);
    color: var(--ink-inverse);
  }
  .annotation-toolbar .ann-tb-btn.bold-btn { font-weight: 700; }
  .annotation-toolbar .ann-tb-btn.italic-btn { font-style: italic; }
  .annotation-toolbar .ann-tb-divider {
    width: 1px;
    height: 18px;
    background: var(--line);
    margin: 0 2px;
  }
  .annotation-toolbar select.ann-tb-select {
    height: 24px;
    border: var(--bd-hair) solid var(--line);
    border-radius: var(--r-control);
    background: var(--surface);
    font-family: var(--ui-sans);
    font-size: 11px;
    color: var(--ink);
    padding: 0 var(--sp-2);
    cursor: pointer;
  }
  .annotation-toolbar .ann-tb-color {
    position: relative;
    width: 26px;
    height: 26px;
    border-radius: var(--r-control);
    cursor: pointer;
    border: var(--bd-line) solid var(--line);
    overflow: hidden;
  }
  .annotation-toolbar .ann-tb-color .swatch {
    position: absolute;
    inset: 4px;
    border-radius: var(--r-control);
  }
  /* "no fill" diagonal red strike for the none-fill swatch */
  .annotation-toolbar .ann-tb-color.no-fill .swatch {
    background: var(--surface);
  }
  .annotation-toolbar .ann-tb-color.no-fill::after {
    content: '';
    position: absolute;
    left: 0; right: 0;
    top: 50%;
    height: 1px;
    background: var(--badge);
    transform: rotate(-30deg);
  }
  .annotation-toolbar input[type="color"] {
    width: 0; height: 0;
    border: none;
    padding: 0;
    visibility: hidden;
    position: absolute;
  }
  .annotation-toolbar .ann-tb-label {
    font-size: 10px;
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    margin: 0 4px 0 2px;
  }
  .btn-present {
    background: var(--ink);
    color: var(--ink-inverse);
    border-color: var(--ink);
  }
  .btn-present:hover:not(:disabled) {
    background: var(--surface-strong);
    border-color: var(--surface-strong);
  }
  .toolbar-spacer { flex: 1; }

  .format-bar {
    display: flex;
    align-items: center;
    gap: 1px;
    padding: var(--sp-1);
    border-radius: var(--r-surface);
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    height: 30px;
    /* Fixed position pinned to the right side of the toolbar so it stays
       put regardless of how long the merge-hint text is. Sits well to
       the left of the head-toggle / Present button area. */
    position: absolute;
    right: 320px;          /* leave wide gap before head-toggle + Present */
    top: 50%;
    transform: translateY(-50%);
    z-index: 5;
  }
  .format-bar.disabled { opacity: 0.4; pointer-events: none; }
  .fmt-btn {
    width: 26px;
    height: 26px;
    border: none;
    background: transparent;
    border-radius: var(--r-control);
    color: var(--ink);
    font-size: 13px;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    font-family: var(--tree-serif);
    transition: background var(--ease-control);
  }
  .fmt-btn:hover { background: var(--bg-soft, #F2EFE8); }
  .fmt-btn.active { background: var(--active-bg); color: var(--active-ink); box-shadow: var(--active-shadow); }
  .fmt-btn.bold { font-weight: 700; }
  .fmt-btn.italic { font-style: italic; }
  .fmt-btn.underline { text-decoration: underline; }
  .fmt-btn.strike { text-decoration: line-through; }
  .fmt-btn.sup { font-size: 11px; vertical-align: super; }
  .fmt-btn.sub { font-size: 11px; vertical-align: sub; }
  .fmt-divider { width: 1px; height: 16px; background: var(--line); margin: 0 2px; }

  .head-toggle {
    display: flex;
    align-items: center;
    gap: 8px;
    height: 30px;
    padding: 0 var(--sp-5);
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line-strong);
    background: var(--surface);
    cursor: pointer;
    user-select: none;
  }
  .head-toggle:hover { background: var(--bg); border-color: var(--ink-faint); }
  /* A switch, so it answers to being pressed. Two values rather than on and
     off, which is why the feedback is momentary rather than a state. */
  .head-toggle:active { transform: var(--press-transform); box-shadow: var(--press-shadow); }
  .head-toggle .ht-label {
    font-size: 10px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
  }
  .head-toggle .ht-value {
    font-size: 12px;
    font-weight: var(--fw-label);
    color: var(--ink);
    font-family: var(--mono);
  }
  .head-toggle .ht-arrow {
    font-size: 11px;
    color: var(--ink-faint);
  }

  .merge-hint {
    font-size: 11px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    margin-left: 4px;
    letter-spacing: 0.02em;
    max-width: 240px;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  .canvas-wrap {
    position: relative;
    display: flex;
    overflow: hidden;
  }
  .stash-pane {
    /* Width is set dynamically (via inline style + persisted preference);
       border on the right is replaced by the splitter element. */
    flex: 0 0 220px;
    width: 220px;
    background: var(--stash-bg);
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  /* Draggable splitter between stash pane and canvas. The element
     itself is a wider invisible hit area (so the user doesn't need
     pixel-perfect aim); a 1px line is drawn in the middle via a
     pseudo-element. On hover/drag the line fattens into an accent-
     colored bar — a clear affordance without crowding the layout
     when idle. */
  .pane-splitter {
    flex: 0 0 7px;
    width: 7px;
    cursor: col-resize;
    user-select: none;
    position: relative;
    z-index: 6;
    /* Match the canvas background so the transparent hit area doesn't
       reveal the surrounding (warmer) body color as a faint band. */
    background: var(--bg-canvas);
  }
  .pane-splitter::before {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    /* Sit flush against the stash pane (the splitter's left edge) so
       the visible 1px line aligns with where the panes actually meet,
       rather than floating in the middle of the hit area. */
    left: 0;
    width: 1px;
    background: var(--line-strong);
    transition: width 120ms ease-out, background 120ms ease-out;
  }
  .pane-splitter:hover::before,
  .pane-splitter.dragging::before {
    width: 4px;
    background: var(--accent);
    border-radius: var(--r-control);
  }
  /* While dragging, prevent iframes/text from intercepting the drag and
     also give the cursor system-wide. */
  body.pane-resizing,
  body.pane-resizing * {
    cursor: col-resize !important;
    user-select: none !important;
  }
  .stash-pane-header {
    flex: 0 0 auto;
    padding: var(--sp-4) var(--sp-5) var(--sp-4);
    border-bottom: var(--bd-hair) solid var(--line);
    font-family: var(--ui-sans);
    font-size: 10px;
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    user-select: none;
    background: var(--glass-soft);
  }
  .stash-pane-scroll {
    flex: 1 1 auto;
    overflow-y: auto;
    overflow-x: hidden;
    padding: var(--sp-5) 0 var(--sp-7);
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
  }
  .stash-pane-scroll::-webkit-scrollbar { width: 8px; }
  .stash-pane-scroll::-webkit-scrollbar-track { background: transparent; }
  .stash-pane-scroll::-webkit-scrollbar-thumb {
    background: var(--scroll-thumb);
    border-radius: var(--r-control);
    background-clip: padding-box;
    border: var(--bd-line) solid transparent;
    min-height: 24px;
  }
  .stash-pane-scroll::-webkit-scrollbar-thumb:hover {
    background: var(--scroll-thumb-hover);
    background-clip: padding-box;
    border: var(--bd-line) solid transparent;
  }
  .stash-pane-empty {
    padding: var(--sp-5);
    font-size: 11px;
    color: var(--ink-faint);
    font-style: italic;
    text-align: center;
    user-select: none;
  }
  .stash-pane-svg {
    display: block;
    width: 100%;
  }
  .canvas-area {
    flex: 1 1 auto;
    position: relative;
    background: var(--bg-canvas);
    overflow: auto;   /* scroll bars when tree exceeds viewport */
    /* Firefox: thin, two-tone amber-on-bg, matches the rest of the UI */
    scrollbar-width: thin;
    scrollbar-color: var(--scroll-thumb) transparent;
  }
  /* Webkit / Blink (Chrome / Edge / Safari): a slim, rounded, semi-
     transparent thumb that darkens on hover. Idle = subtle; active =
     clearly visible. Matches the warm amber palette by using a desaturated
     brown rather than the OS default cool gray. */
  .canvas-area::-webkit-scrollbar {
    width: 10px;
    height: 10px;
    background: transparent;
  }
  .canvas-area::-webkit-scrollbar-track {
    background: transparent;
  }
  .canvas-area::-webkit-scrollbar-thumb {
    background: var(--scroll-thumb);
    border-radius: var(--r-surface);
    /* Inset border via background-clip — gives the thumb a "floating"
       look with a transparent gap around it instead of touching the
       edge. */
    background-clip: padding-box;
    border: var(--bd-bold) solid transparent;
    min-height: 32px;
    min-width: 32px;
  }
  .canvas-area::-webkit-scrollbar-thumb:hover {
    background: var(--scroll-thumb-hover);
    background-clip: padding-box;
    border: var(--bd-bold) solid transparent;
  }
  .canvas-area::-webkit-scrollbar-thumb:active {
    background: var(--scroll-thumb-active);
    background-clip: padding-box;
    border: var(--bd-bold) solid transparent;
  }
  /* Hide the bottom-right corner square (where vertical + horizontal
     scrollbars meet) — defaults to a flat gray which clashes with the
     warm canvas. */
  .canvas-area::-webkit-scrollbar-corner {
    background: transparent;
  }
  /* Inner container — its size is set to max(viewport, tree bounds + padding)
     so the SVG and annotation layer can grow beyond the viewport and the
     user can scroll to see the whole tree. The background dot grid lives
     here so it tiles across the entire scrollable area, not just the
     viewport. */
  .canvas-content {
    position: relative;
    min-width: 100%;
    min-height: 100%;
    /* Its own variable, not --line. The two have opposite requirements: a
       hairline border has to be seen, and the dot grid has to be almost
       unseen. Sharing one made the high-contrast skin, which needs dark
       borders, print a dark dot every 20px behind the tree. */
    /* THE PATTERN ITSELF IS A TOKEN, not only its colour. A skin could recolour
       the dots but not replace them, so a style whose ground is a fan or a
       ruled sheet had nowhere to put it. The dot grid remains the default, so
       every existing skin is unaffected. */
    background-image: var(--canvas-pattern);
    background-size: var(--canvas-pattern-size);
  }
  .canvas-svg {
    display: block;
    cursor: default;
    /* Width/height set dynamically by JS to fit the rendered tree */
  }
  /* Persistent overlay just for the binding-domain frame, so it can fade in
     AND out (the main SVG is rebuilt each render, which can't animate removal).
     Sits under the main tree SVG (drawn first, same coords), never intercepts
     pointer events, and its single frame group transitions opacity. */
  .gc-overlay-svg {
    position: absolute;
    left: 0;
    top: 0;
    pointer-events: none;
    overflow: visible;
  }
  .gc-overlay-frame {
    opacity: 0;
    transition: opacity 0.3s ease;
  }
  .gc-overlay-frame.shown {
    opacity: 1;
  }
  /* Empty-state guidance. A single faint line used to be the only thing a
     new user was told, while the program has menus, two locality definitions
     and a shelf of shortcuts. This gives the three steps that actually get a
     tree on screen, and points at Help for the rest. */
  .canvas-hint {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    text-align: left;
    font-size: 13px;
    line-height: 1.85;
    color: var(--ink-soft, #8A857C);
    user-select: none;
    background: var(--glass);
    border: var(--bd-line) solid var(--line, #e6e1d8);
    border-radius: var(--r-control);
    padding: var(--sp-7) var(--sp-7);
    max-width: 380px;
  }
  .canvas-hint b { color: var(--ink, #2c2a26); font-weight: var(--fw-strong); }
  .canvas-hint .ch-title {
    font-family: var(--ui-display); font-size: var(--fs-display); font-weight: var(--fw-strong); color: var(--ink, #2c2a26);
    margin-bottom: 10px; display: block;
  }
  .canvas-hint kbd {
    font-family: ui-monospace, Menlo, monospace;
    font-size: 11.5px;
    border: var(--bd-line) solid var(--line, #e6e1d8);
    border-radius: var(--r-control);
    padding: var(--sp-1) var(--sp-2);
    background: var(--surface);
    color: var(--ink, #2c2a26);
  }
  .canvas-notice {
    position: absolute;
    bottom: 16px;
    left: 50%;
    transform: translateX(-50%);
    font-size: 11px;
    color: var(--accent);
    font-style: italic;
    pointer-events: none;
    user-select: none;
    background: var(--glass);
    padding: var(--sp-2) var(--sp-5);
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--accent-tint-strong);
    transition: opacity 0.4s ease;
    opacity: 1;
    max-width: 80%;
    text-align: center;
  }
  .canvas-notice.fading {
    opacity: 0;
  }

  /* Binding Theory violation notice — sits at the TOP of the canvas (distinct
     from the bottom transient canvas-notice), stays until the violation is
     resolved, and reads as a red warning banner. */
  /* Legend for the Governing Category ingredients. The three roles are shown
     by node colour on the tree; this explains what each colour means, instead
     of hanging tags off the nodes (which crowded the tree and, next to a wide
     word, looked as if they labelled the neighbouring node). */
  .tree-legend {
    position: absolute;
    bottom: 14px;   /* left set by layoutOverlay */
    font-size: 11.5px;
    line-height: 1.7;
    pointer-events: none;
    user-select: none;
    background: var(--glass-strong);
    padding: var(--sp-4) var(--sp-5);
    border-radius: var(--r-control);
    border: var(--bd-hair) solid var(--line);
    box-shadow: var(--shadow-panel);
    z-index: 6;
  }
  .tree-legend-title {
    font-size: 10px;
    letter-spacing: var(--label-track);
    text-transform: uppercase;
    color: var(--ink-soft);
    margin-bottom: 3px;
  }
  .tree-legend-section + .tree-legend-section {
    margin-top: 8px; padding-top: 8px; border-top: var(--bd-line) solid var(--line);
  }
  .tree-legend-row { display: flex; align-items: center; gap: 7px; white-space: nowrap; }
  .tree-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
  /* Verdict line under the colour key, used when probing PRO: the interesting
     fact there is whether a governor exists at all, which no colour can say. */
  /* NUMBERED LINES WRAP UNDER THEIR TEXT, not under their number. A hanging
     indent is what makes a numbered list read as a list once any item runs past
     one line — without it the second line starts under the (1) and the numbers
     stop being a column. */
  .tree-legend-note-line { margin-top: 3px; }
  /* HANGING INDENT ONLY WHERE THERE IS SOMETHING TO HANG. A numbered item wants
     its wrapped lines under the text rather than under the (1), and that is what
     this was written for. Applied to every note line it put a stray indent on
     the second line of ordinary prose, which has no number for it to align
     against — an indent that looks like a mistake, because it is one. */
  .tree-legend-note-numbered {
    padding-left: 1.6em;
    text-indent: -1.6em;
  }
  .tree-legend-note {
    margin-top: 4px;
    padding-top: 4px;
    border-top: var(--bd-hair) solid var(--line);
    /* WIDE ENOUGH TO SAY ONE CLAUSE PER LINE. At 260px the numbered items broke
       after a few words each, which turned two statements into five ragged
       lines and buried the numbering that was added to make them countable.
       Capped against the viewport as well: the legend floats over the canvas at
       the bottom left, and a note that grows without limit covers the tree it
       is annotating. */
    max-width: min(380px, 42vw);
    white-space: normal;
    color: var(--ink-soft);
  }
  .tree-legend-note.is-violation { color: #C0392B; }

  /* Transient inline notice — replaces blocking alert() for anything the user
     merely needs to be told. Sits at the top of the canvas like the binding
     notice, fades itself out. */
  .app-notice {
    position: absolute;
    top: 14px;   /* left / transform set by layoutOverlay */
    font-size: 12.5px;
    line-height: 1.5;
    padding: var(--sp-4) var(--sp-6);
    border-radius: var(--r-control);
    max-width: 70%;
    text-align: center;
    z-index: 60;
    pointer-events: none;
    user-select: none;
    background: var(--warn-bg);
    color: var(--warn-ink);
    border: var(--bd-hair) solid var(--warn-line);
    box-shadow: var(--shadow-panel);
    opacity: 0;
    transition: opacity 0.18s ease;
  }
  .app-notice.shown { opacity: 1; }

  /* BELOW THE MENUS AND THE POPUPS. This notice is a standing report on the
     tree — it is there for as long as the violation is — while a dropdown or a
     node popup is what the reader is using at this moment. At z-index 40 it sat
     over both and covered the very entries someone had opened the menu to read.
     A transient message that must be seen regardless is a different thing and
     keeps its own layer (.app-notice, 60). */
  .binding-notice {
    position: absolute;
    top: 14px;   /* left / transform set by layoutOverlay */
    /* ONE NUMBER SETS THE WHOLE PANEL. Sizes inside are given in em so they
       follow it: a reader who cannot make out the subscript on a trace turns
       one dial rather than finding that the node got bigger and the island
       label did not. Default raised from 11.5px, which was legible for the
       words and not for the subscripts, and those are what a reader is
       matching against the tree. */
    font-size: var(--fs-notice, 13px);
    line-height: 1.45;
    color: var(--err-ink);
    /* THE WHOLE PANEL IS THE HANDLE. It began taking no pointer events at all,
       so a click went through to the tree underneath — which was the right
       answer while the panel could not be moved and might be sitting over the
       node you needed. It can be moved now, and a handle limited to the
       heading means half of a small panel does not respond to a drag, which
       is what a reader tries first. Grab it anywhere. */
    pointer-events: auto;
    cursor: grab;
    user-select: none;
    background: var(--err-bg);
    padding: var(--sp-3) var(--sp-5);
    border-radius: var(--r-surface);
    border: var(--bd-hair) solid var(--err-line);
    box-shadow: var(--shadow-panel);
    /* A FLOOR AS WELL AS A CEILING. Shrink-to-fit made the box the width of its
       longest row, so a short report came out as a stub barely wider than the
       two words in it and read as a fragment rather than a panel. Capped at the
       viewport too, so the floor cannot push it off a narrow canvas. */
    min-width: min(240px, 100%);
    max-width: 70%;
    text-align: center;
    z-index: 15;
  }
  /* THREE KINDS OF INFORMATION, THREE TREATMENTS. What the island is, which
     principle it violates, and which node — run together in one typeface these
     read as a sentence to be parsed rather than three facts to be picked out.
     Every colour here is a theme variable: the palette is a skin's business,
     and the contrast gate recomputes it per skin. */
  /* THE HANDLE. The notice itself takes no pointer events, so the rows stay
     transparent to clicks and a node under them is still reachable; the heading
     is the one part that accepts a pointer, which is where a floating panel is
     grabbed anyway. */
  .binding-notice-head {
    display: flex;
    align-items: baseline;
    justify-content: center;
    flex-wrap: wrap;
    gap: 4px 8px;
  }
  .binding-notice.is-dragging { box-shadow: var(--shadow-float); }
  .binding-notice.is-dragging { cursor: grabbing; }
  .binding-notice-mark {
    width: 1.05em;
    height: 1.05em;
    flex: none;
    align-self: center;
  }
  .binding-notice-text { font-weight: 500; }
  /* THE IDLE STATE IS NOT A FAULT. When nothing is switched on the panel says
     so in the legend's ink on the legend's ground rather than in the error
     pair: red would assert a problem where the program has looked for none. */
  .binding-notice.notice-quiet { color: var(--ink); background: var(--glass-strong); }
  .binding-notice-dismiss {
    margin-left: var(--sp-3);
    cursor: pointer;
    opacity: 0.6;
    font-size: 1.15em;
    line-height: 1;
  }
  .binding-notice-dismiss:hover { opacity: 1; }
  /* Monospaced because that is what the canvas uses for a node, so the eye can
     carry the name from here to the tree without re-reading it. */
  /* THE NODE IS THE THING TO LOOK FOR, so it is set apart from the text beside
     it — but OUTLINED rather than filled. --err-line is the panel border color
     at forty percent, which as a fill is a solid mid-red block on a pale panel:
     it stood out by being heavier than everything around it, which is not the
     same as being legible. The outline is the same color the panel border and
     the divider already use, so the chip belongs to the same drawing.
     Monospaced because that is what the canvas uses for a node — the eye
     carries the name from here to the tree without re-reading it. */
  .binding-notice-node {
    font-family: var(--mono);
    font-size: 0.94em;
    color: var(--err-ink);
    border: var(--bd-hair) solid var(--err-line);
    border-radius: var(--r-chip, 4px);
    padding: var(--sp-1) var(--sp-3);
    flex: none;
  }
  .binding-notice-tag { opacity: 0.85; }
  .notice-stacked { text-align: left; }
  .notice-stacked .binding-notice-head { justify-content: flex-start; }
  .binding-notice-rows {
    margin-top: var(--sp-2);
    padding-top: var(--sp-2);
    border-top: var(--bd-hair) solid var(--err-line);
    /* SEPARATED, BECAUSE THE ROWS CARRY FILLS. Text rows are told apart by their
       line boxes alone, but a filled chip is nearly as tall as its line, so two
       of them on consecutive rows meet edge to edge and read as one shape. The
       gap is between the rows rather than padding inside them, so the rule above
       stays where it is. */
    display: flex;
    flex-direction: column;
    gap: var(--sp-3);
  }
  .binding-notice-row {
    display: flex;
    align-items: baseline;
    gap: 8px;
    white-space: nowrap;
  }
  /* A ROW THAT OPENS. The caret says there is more; the pointer says it can
     be had with a click. The explanation below it is the one sentence the
     star's tooltip shows, wrapped, in the panel's own ink a shade lighter so
     the row above it stays the thing to read first. */
  .binding-notice-item { display: flex; flex-direction: column; gap: var(--sp-1); }
  /* Two blocks on the panel -- the islands crossed, the rules broken -- are
     told apart by a small label, in the panel's own ink. */
  .binding-notice-section {
    font-family: var(--ui-sans);
    font-size: 0.72em;
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    opacity: 0.7;
    margin-top: var(--sp-3);
  }
  .binding-notice-row { cursor: pointer; }
  .binding-notice-caret.is-blank { visibility: hidden; }
  .binding-notice-caret {
    width: 0.85em;
    height: 0.85em;
    flex: none;
    align-self: center;
    opacity: 0.8;
    transition: transform 0.12s ease;
  }
  .binding-notice-row.is-open .binding-notice-caret { transform: rotate(90deg); }
  /* A VARIABLE IN THE SENTENCE -- the reader's own word for a node -- is set
     in the node chip's face so it reads as a name, not as prose. */
  .binding-notice-var {
    font-family: var(--mono);
    font-size: 0.94em;
    padding: 0 0.3em;
    border: var(--bd-hair) solid var(--err-line);
    border-radius: var(--r-chip, 4px);
    white-space: nowrap;
  }
  .binding-notice-detail {
    white-space: normal;
    font-size: 0.95em;
    line-height: 1.4;
    opacity: 0.9;
    padding-left: calc(1em + 8px);
    max-width: 56ch;
  }
  /* Two explanations on one row -- an island described, then the principle
     -- are two paragraphs, not one run of text. */
  .binding-notice-detail > div + div { margin-top: var(--sp-3); }

  /* Curve drag handle: small dot at the bezier midpoint that lets the user
     pull the curve up or down to dodge nodes. Hidden until the arrow is
     hovered or its group is being edited (highlighted). */
  .arrow-curve-handle {
    opacity: 0;
    transition: opacity 0.15s ease;
    pointer-events: auto;
  }
  .movement-arrow:hover .arrow-curve-handle,
  .movement-arrow-highlight .arrow-curve-handle {
    opacity: 1;
  }
  .arrow-curve-handle:hover { fill: var(--accent); }
  .arrow-curve-handle.dragging {
    opacity: 1 !important;
    fill: var(--accent);
  }

  /* legacy class kept for the title text inside the pane header */
  .stash-label {
    pointer-events: none;
    user-select: none;
  }

  /* Stash card delete button — hidden by default, revealed on card hover */
  .stash-delete-btn {
    opacity: 0;
    transition: opacity 0.12s ease;
  }
  .stash-card:hover .stash-delete-btn {
    opacity: 1;
  }
  .stash-delete-btn:hover circle {
    fill: var(--tree-ink-strong);
  }
  .stash-delete-btn:hover line {
    stroke: var(--surface);
  }

  .selection-order-badge {
    position: absolute;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--ink);
    color: var(--ink-inverse);
    font-size: 10px;
    font-weight: var(--fw-label);
    font-family: var(--mono);
    display: flex;
    align-items: center;
    justify-content: center;
    pointer-events: none;
    border: 1.5px solid var(--bg-canvas);
  }

  .popup {
    position: absolute;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    padding: var(--sp-5) var(--sp-4) var(--sp-4);
    display: flex;
    gap: 8px;
    align-items: flex-end;
    box-shadow: var(--shadow-float);
    z-index: 20;
    /* Cap height so the popup never extends past the viewport. The script also
       caps this with a more accurate value at open time. */
    max-height: calc(100vh - 60px);
    overflow-y: auto;
  }
  /* Arrow popup — vertical layout, different from the inline-row popups */
  .popup.arrow-popup {
    flex-direction: column;
    align-items: stretch;
    gap: 7px;
    min-width: 280px;
  }

  /* Leaf popup v2: a vertical shell whose first tier is the original
     horizontal row (label / word / actions) and whose second tier is a
     connected "binding drawer" flush beneath it, same width. */
  .popup.leaf-popup-v2 {
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 0;
    overflow-y: auto;
  }
  .popup.leaf-popup-v2 .popup-banner { padding: var(--sp-4) var(--sp-5) 0; }
  .popup-main-row {
    display: flex;
    gap: 8px;
    align-items: flex-end;
    padding: var(--sp-4) var(--sp-5) var(--sp-4);
  }
  /* Right-hand action column: BINDING toggle on top, action buttons below. */
  /* THE THREE CONTROLS HERE ANSWER TO THE THREE ROWS OF THE WORD FIELD beside
     them: BINDING to + LINE ABOVE, SCOPE to the word input, the action row to
     + LINE BELOW. The gaps are deliberately unequal because the rows they line
     up with are: the word input is 34px where a button is 30, so the four it is
     taller by are carried by the gap BELOW scope. Measured in a browser rather
     than reasoned about — the tops come out at 256 / 294 / 338 either way, and
     one equal gap cannot put them there. A hidden BINDING toggle (a label that
     cannot carry a binding annotation) costs nothing: the column is anchored at
     the bottom, so what remains keeps its place. */
  .popup-action-col {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 8px;
  }
  .popup-action-col .popup-action-row { margin-top: 6px; }
  .popup-action-row {
    display: flex;
    gap: 8px;
    align-items: flex-end;
  }
  .binding-toggle {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    font-size: 11px;
    letter-spacing: var(--label-track);
    text-transform: uppercase;
    color: var(--ink-soft, #6b6b6b);
    padding: var(--sp-1) var(--sp-1);
    align-self: flex-start;
  }
  /* HOVER USED TO ERASE THE LABEL. The rule here was written for the quiet
     text toggle .binding-toggle asks to be — soft ink going to full ink on
     hover — but .popup button paints the control solid ink with an inverse
     label instead, and .popup button:hover swaps the fill to --surface-strong,
     which in most skins IS the ink. So the label took the colour of the ground
     it sits on. Measured over all twelve skins: 1.00 to 1.91, and 1.00 is the
     same colour twice.
     The label stays inverse now and the FILL lightens, which is the ordinary
     affordance for a solid button. Same measurement, same twelve skins: 6.69
     to 12.63. Specific enough to beat .popup button:hover, which sets the fill. */
  .popup button.binding-toggle:hover {
    background: var(--ink-soft);
    color: var(--ink-inverse);
  }
  .binding-toggle-caret { display: inline-block; transition: transform var(--ease-control); }
  /* The connected second tier. A top divider makes it read as an extension of
     the same popup rather than a separate panel. */
  .popup-binding-drawer {
    display: flex;
    gap: 16px;
    align-items: flex-end;
    padding: var(--sp-4) var(--sp-5) var(--sp-5);
    border-top: var(--bd-hair) solid var(--line);
    background: var(--bg-soft, #faf9f7);
    border-radius: 0 0 var(--r-surface) var(--r-surface);
  }
  .arrow-popup-row {
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .arrow-popup-row-label {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    width: 46px;
    flex-shrink: 0;
  }
  .arrow-popup-buttons {
    display: flex;
    gap: 3px;
    flex: 1;
  }
  .arrow-popup-btn {
    flex: 1;
    padding: var(--sp-1) var(--sp-3);
    font-size: 10px;
    background: var(--surface);
    color: var(--ink);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    cursor: pointer;
    font-family: inherit;
  }
  /* A ROW OF BUTTONS IS ONE CONTROL, so only its outer corners are rounded and
     the shared edges are drawn once. Individually rounded pills with a gap
     between them say these are separate things, which they are not: exactly one
     of them is on at a time. The borders overlap by a pixel so a shared edge
     does not read as two lines. */
  .arrow-popup-buttons { display: flex; }
  .arrow-popup-buttons .arrow-popup-btn { border-radius: 0; margin-left: -0.5px; }
  .arrow-popup-buttons .arrow-popup-btn:first-child {
    border-radius: var(--r-control) 0 0 var(--r-control); margin-left: 0;
  }
  .arrow-popup-buttons .arrow-popup-btn:last-child {
    border-radius: 0 var(--r-control) var(--r-control) 0;
  }
  .arrow-popup-buttons .arrow-popup-btn.active { position: relative; z-index: 1; }

  .arrow-popup-btn:hover { background: var(--bg); }
  .arrow-popup-btn.active {
    background: var(--active-bg);
    color: var(--active-ink);
    box-shadow: var(--active-shadow);
    border-color: var(--ink);
  }
  .arrow-popup-label-input {
    flex: 1;
    padding: var(--sp-1) var(--sp-3);
    font-size: 11px;
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    font-family: inherit;
    background: var(--surface);
    color: var(--ink);
    outline: none;
  }
  .arrow-popup-label-input:focus { border-color: var(--ink); }
  .arrow-popup-quickbtns {
    display: flex;
    gap: 3px;
    margin-left: 52px;
  }
  .arrow-popup-quickbtn {
    padding: var(--sp-1) var(--sp-3);
    font-size: 10px;
    background: var(--bg);
    color: var(--ink-soft);
    border: var(--bd-hair) solid var(--line);
    border-radius: var(--r-control);
    cursor: pointer;
    font-family: var(--mono);
  }
  .arrow-popup-quickbtn:hover {
    background: var(--surface);
    border-color: var(--line-strong);
    color: var(--ink);
  }
  .arrow-popup-actions {
    display: flex;
    gap: 5px;
    border-top: var(--bd-hair) solid var(--line);
    padding-top: 6px;
    margin-top: 2px;
  }
  .arrow-popup-actions button {
    flex: 1;
    padding: var(--sp-1) var(--sp-3);
    font-size: 10px;
    background: var(--surface);
    color: var(--ink);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    cursor: pointer;
    font-family: inherit;
  }
  .arrow-popup-actions button:hover { background: var(--bg); }

  /* Movement popup (extends arrow-popup) */
  .popup.movement-popup { min-width: 340px; max-width: 400px; }
  .movement-popup-section-label {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    margin-top: 3px;
    border-top: var(--bd-hair) solid var(--line);
    padding-top: 4px;
  }
  .movement-popup-section-label:first-of-type {
    border-top: none;
    padding-top: 0;
    margin-top: 0;
  }
  .movement-popup-slider-value {
    font-family: var(--mono);
    font-size: 10px;
    color: var(--ink-soft);
    min-width: 28px;
    text-align: right;
  }
  .popup .rich-input.movement-label-input {
    flex: 1;
    min-height: 22px;
    padding: var(--sp-1) var(--sp-3);
    font-size: 11px;
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    background: var(--surface);
    font-family: var(--mono);
  }
  .arrow-popup-checkbox-row {
    padding-left: 52px;
  }
  .arrow-popup-checkbox-label {
    display: flex;
    align-items: center;
    gap: 5px;
    font-size: 10px;
    color: var(--ink-soft);
    cursor: pointer;
    user-select: none;
  }
  .arrow-popup-checkbox-label input[type="checkbox"] {
    margin: 0;
    cursor: pointer;
  }
  .arrow-popup-compact-row {
    gap: 5px;
  }
  .arrow-popup-compact-row input[type="range"] {
    flex: 1;
    min-width: 40px;
    height: 4px;
  }
  .arrow-popup-mini-label {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    flex-shrink: 0;
  }
  /* Make sliders inside popup rows compact too */
  .popup.arrow-popup input[type="range"] { height: 4px; }
  /* On short viewports, tighten popup further */
  @media (max-height: 720px) {
    .popup { padding: var(--sp-4) var(--sp-4) var(--sp-3); }
    .popup.arrow-popup { gap: 5px; min-width: 260px; }
    .arrow-popup-row { gap: 5px; }
    .arrow-popup-row-label { font-size: 8.5px; width: 42px; letter-spacing: 0.03em; }
    .arrow-popup-btn { padding: var(--sp-1) var(--sp-2); font-size: 9.5px; }
    .arrow-popup-actions { padding-top: 4px; margin-top: 1px; }
    .arrow-popup-actions button { padding: var(--sp-1) var(--sp-2); font-size: 9.5px; }
    .arrow-popup-checkbox-row { padding-left: 47px; }
    .arrow-popup-checkbox-label { font-size: 9.5px; }
    .arrow-popup-mini-label { font-size: 8.5px; }
    .popup.movement-popup { min-width: 320px; }
    .popup .rich-input.movement-label-input { min-height: 20px; padding: var(--sp-1) var(--sp-3); font-size: 10.5px; }
    .movement-popup-slider-value { font-size: 9.5px; min-width: 26px; }
    .arrow-popup-quickbtns { margin-left: 47px; gap: 2px; }
    .arrow-popup-quickbtn { padding: var(--sp-1) var(--sp-2); font-size: 9.5px; }
  }
  .popup .field {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .popup .field-label {
    font-size: 9px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    padding-left: 2px;
  }
  .popup input {
    padding: var(--sp-3) var(--sp-4);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    font-family: var(--tree-serif);
    font-size: 14px;
    color: var(--ink);
    outline: none;
  }
  .popup .rich-input {
    padding: var(--sp-3) var(--sp-4);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
    /* In edit mode use a serif that clearly distinguishes 0 from o.
       Georgia (default tree font) makes them nearly identical, which is
       a real problem when labelling X⁰, N⁰ etc. Constantia (Windows)
       and Iowan Old Style (Mac) both have distinctive zeros. Falls
       back to Georgia if neither is installed. */
    font-family: Constantia, 'Iowan Old Style', 'Iowan Old Style W04', var(--tree-serif);
    font-size: 14px;
    color: var(--ink);
    outline: none;
    min-height: 28px;
    line-height: 1.4;
    white-space: pre;
    overflow-x: auto;
    font-variant-numeric: lining-nums;
    font-feature-settings: "ss01", "zero";
  }
  .popup .rich-input:focus { border-color: var(--ink); }
  .popup .rich-input.label-input { width: 100px; }
  .popup .rich-input.word-input { width: 150px; font-style: italic; }
  .popup .rich-input.word-input.upright { font-style: normal; }
  .popup .rich-input.word-input.main-word {
    background: var(--accent-tint-soft);
  }
  .popup .rich-input.merge-input { width: 160px; }
  .popup .rich-input:empty:before {
    content: attr(data-placeholder);
    color: var(--ink-faint);
  }
  .popup .rich-input sub { font-size: 0.7em; vertical-align: sub; }
  .popup .rich-input sup { font-size: 0.7em; vertical-align: super; }
  .popup input.label-input { width: 90px; }
  .popup input.word-input { width: 140px; font-style: italic; }
  .popup input.merge-input { width: 150px; }
  .popup input:focus { border-color: var(--ink); }

  /* multi-line word editing */
  .popup .word-lines {
    display: flex;
    flex-direction: column;
    gap: 4px;
  }
  .popup .word-line-row {
    display: flex;
    align-items: center;
    gap: 6px;
  }
  .popup .word-line-remove {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    border: var(--bd-hair) solid var(--line-strong);
    background: var(--surface);
    color: var(--ink-soft);
    font-size: 11px;
    line-height: 1;
    padding: 0;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
  }
  .popup .word-line-remove:hover {
    background: var(--ink);
    color: var(--ink-inverse);
    border-color: var(--ink);
  }
  .popup .word-line-add {
    align-self: flex-start;
    background: transparent;
    border: var(--bd-hair) dashed var(--line-strong);
    color: var(--ink-soft);
    font-family: var(--ui-sans);
    font-size: 10px;
    padding: var(--sp-2) var(--sp-4);
    border-radius: var(--r-control);
    cursor: pointer;
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    margin-top: 2px;
  }
  .popup .word-line-add:hover {
    border-color: var(--ink);
    color: var(--ink);
    background: var(--hover-tint);
  }
  .popup .main-word-row {
    display: flex;
    align-items: stretch;
    gap: 4px;
    align-self: flex-start;
  }
  .popup .main-word-row .rich-input.main-word {
    flex: 0 0 auto;
  }
  .popup .null-insert-btn {
    background: transparent !important;
    border: var(--bd-hair) solid var(--line-strong) !important;
    color: var(--ink-soft) !important;
    font-family: Constantia, 'Iowan Old Style', var(--tree-serif) !important;
    font-size: 16px !important;
    padding: 0 var(--sp-4) !important;
    border-radius: var(--r-control) var(--r-control);
    cursor: pointer;
    min-width: 28px;
    line-height: 1;
  }
  .popup .null-insert-btn:hover {
    border-color: var(--ink) !important;
    color: var(--ink) !important;
    background: var(--hover-tint) !important;
  }
  .popup button {
    padding: 0 var(--sp-5);
    border-radius: var(--r-control);
    border: none;
    background: var(--ink);
    color: var(--ink-inverse);
    font-size: 12px;
    font-weight: var(--fw-label);
    cursor: pointer;
    height: 30px;
  }
  /* A BUTTON IN THE POPUP THAT IS ON. The triangle switch had only its label
     to say so — the word changed from off to on and the control looked
     identical — which is a gap under every skin, not merely the one that
     presses its switches in. */
  .popup button.active {
    background: var(--active-bg);
    color: var(--active-ink);
    box-shadow: var(--active-shadow);
  }
  .popup button:hover { background: var(--surface-strong); }
  .popup button.btn-secondary {
    background: var(--surface);
    color: var(--ink);
    border: var(--bd-hair) solid var(--line-strong);
  }
  .popup button.btn-secondary:hover { background: var(--bg); }
  /* The bubble shell, shared by the delete confirmation and the filename
     prompt. Narrow, vertical, sized by its sentence rather than by a form.
     overflow is visible because the tail hangs outside the box; .popup sets it
     to auto, which would clip the tail off. */
  .popup.bubble {
    flex-direction: column;
    align-items: stretch;
    gap: 9px;
    min-width: 224px;
    max-width: 264px;
    padding: var(--sp-5) var(--sp-5) var(--sp-4);
    border-radius: var(--r-control);
    overflow: visible;
  }
  .popup.bubble .confirm-msg {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--ink);
  }
  /* THE TAIL. Without it the bubble is a box that happens to be near a node;
     with it the bubble points, which is the whole reason for not using a system
     dialog. A rotated square rather than a border triangle, so the hairline
     border and the surface both come from the skin and need no second colour. */
  .popup.bubble .confirm-tail {
    position: absolute;
    width: 9px;
    height: 9px;
    background: var(--surface);
    border-left: var(--bd-hair) solid var(--line-strong);
    border-top: var(--bd-hair) solid var(--line-strong);
    transform: rotate(45deg);
    margin-left: -4.5px;
  }
  .popup.bubble.tail-up .confirm-tail { top: -5px; }
  .popup.bubble.tail-down .confirm-tail { bottom: -5px; transform: rotate(225deg); }
  .popup.bubble.tail-none .confirm-tail { display: none; }
  .popup.bubble .bubble-input {
    width: 100%;
    height: 28px;
    padding: 0 var(--sp-4);
    font-size: 12.5px;
    font-family: var(--ui-sans);
    color: var(--ink);
    background: var(--bg-canvas);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-control);
  }
  .popup.bubble .bubble-input:focus { outline: none; border-color: var(--accent); }
  .popup.bubble .confirm-actions {
    display: flex;
    gap: 4px;
    justify-content: flex-end;
    align-items: center;
  }
  .popup.bubble .confirm-actions button {
    height: 26px;
    padding: 0 var(--sp-5);
    font-size: 11.5px;
    border-radius: var(--r-control);
  }
  /* Cancel is quiet. Two buttons of equal weight make the reader stop and
     compare them; the safe choice does not need to compete for attention, it
     needs to be easy to hit, and Escape hits it too. */
  .popup.bubble button.btn-quiet {
    background: transparent;
    border: none;
    color: var(--ink-soft);
  }
  .popup.bubble button.btn-quiet:hover { background: var(--hover-tint); color: var(--ink); }
  /* The destructive choice is the filled one, and the focused one, so Enter
     takes it — the bubble is small enough that reaching for the mouse to
     answer it would cost more than it saves. */
  .popup button.danger-solid {
    background: var(--accent);
    color: var(--ink-inverse);
    border: var(--bd-hair) solid var(--accent);
  }
  .popup button.danger-solid:hover { background: var(--accent-strong); border-color: var(--accent-strong); }
  .popup button.btn-secondary.danger {
    color: var(--accent);
    border-color: var(--accent-tint-strong);
  }
  .popup button.btn-secondary.danger:hover {
    background: var(--accent);
    color: var(--ink-inverse);
    border-color: var(--accent);
  }
  .popup-banner {
    position: absolute;
    top: -22px;
    left: 0;
    font-size: 10px;
    color: var(--ink-faint);
    font-family: var(--ui-sans);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
    background: var(--bg-canvas);
    padding: var(--sp-1) var(--sp-3);
    border-radius: var(--r-control);
  }

  .err-bar {
    display: none;
    background: var(--danger-bg);
    color: var(--danger-ink);
    padding: var(--sp-3) var(--sp-5);
    font-family: var(--mono);
    font-size: 12px;
    border-bottom: var(--bd-line) solid var(--danger-line);
  }
  /* ============================================================ */
  /* PRESENT MODE                                                   */
  /* ============================================================ */
  .present-overlay {
    position: fixed;
    top: 0; left: 0; right: 0; bottom: 0;
    background: var(--bg-canvas);
    z-index: 100;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .present-stage {
    flex: 1;
    width: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }
  .present-stage svg {
    max-width: 90vw;
    max-height: 80vh;
  }
  .present-stage .present-node {
    opacity: 0;
    transform: translateY(8px);
    transition: opacity 0.3s ease-out, transform 0.3s ease-out;
  }
  .present-stage .present-node.visible {
    opacity: 1;
    transform: translateY(0);
  }
  /* Mover-copy nodes stay hidden until the movement animation completes,
     even if their step has been revealed. The animation fades them in. */
  .present-stage .present-node.visible.movement-pending {
    opacity: 0;
  }
  /* A node the resting element is currently drawn over. It is taken back
     rather than hidden: still legible, but plainly behind, and restored the
     instant the element moves on. The transition matches the movement fades
     so the change does not read as a separate event. */
  .present-stage .present-node.visible.covered-by-mover {
    opacity: 0.22;
  }
  /* Adjunction target_origs (the head label rendered at the wrapper's
     non-mover child position) also stay hidden until the movement animation
     reveals them — the wrapper appears as a single head first, then the
     animation grows out the two branches with target_orig on one side and
     the mover sliding into the other. */
  .present-stage .present-node.visible.adj-target-pending {
    opacity: 0;
  }
  /* THE CAPTION BAR: what the step did, the string so far, and the verdicts
     that arise at it, above the controls and in the same surface. */
  .present-caption {
    position: fixed;
    bottom: 84px;
    /* Centred by auto margins and sized to its content: with left at 50%
       and a transform, a fixed box shrinks to fit HALF the viewport, and
       the sentences wrapped in a card a third as wide as they could be. */
    left: 0;
    right: 0;
    margin: 0 auto;
    width: max-content;
    max-width: min(720px, 90vw);
    /* Capped, and scrolling inside past the cap: a card can never take the
       whole screen, whatever a step reports. */
    max-height: 40vh;
    overflow-y: auto;
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    padding: var(--sp-3) var(--sp-5);
    box-shadow: var(--shadow-float);
    z-index: 110;
    font-family: var(--ui-sans);
    font-size: 14px;
    line-height: 1.45;
    color: var(--ink);
  }
  .present-caption:empty { display: none; }
  /* WHERE THE CARD GOES, by a class on the overlay (applyPresentCaptionLayout).
     bottom: under the tree, in room reserved once for the tallest card of the
     run, so the tree neither moves nor is covered. side: a column on the
     right. float: over the tree, translucent until the pointer rests on it. */
  .present-overlay.cap-bottom .present-stage { box-sizing: border-box; padding-bottom: var(--present-reserve, 120px); }
  .present-overlay.cap-bottom .present-stage svg { max-height: calc(100vh - var(--present-reserve, 120px) - 24px); }
  .present-overlay.cap-side .present-stage { box-sizing: border-box; padding-right: calc(min(380px, 32vw) + 48px); }
  .present-overlay.cap-side .present-stage svg { max-width: calc(100vw - min(380px, 32vw) - 96px); }
  .present-overlay.cap-side .present-caption {
    left: auto; right: 24px; bottom: auto; top: 50%; transform: translateY(-50%);
    margin: 0; width: min(380px, 32vw); max-width: min(380px, 32vw); max-height: 80vh;
  }
  .present-overlay.cap-float .present-caption { opacity: 0.72; transition: opacity 0.2s ease; }
  .present-overlay.cap-float .present-caption:hover { opacity: 1; }
  .present-caption .pc-cap-step { font-weight: 500; }
  /* A LABEL AND A WORD, TOLD APART: the label in small capitals in the
     interface face, the word in the mono chip the panel uses for a variable. */
  .present-caption .pc-cap-label { font-weight: 700; letter-spacing: 0.02em; }
  /* THE STAGE THE DERIVATION IS IN under GB: a folder tab standing on the
     top-left corner of the card, its bottom edge on the border, at every
     step. It sat across the border once and ran into the first line. */
  .present-caption .pc-cap-stage { position: absolute; bottom: 100%; left: var(--sp-4); transform: translateY(var(--bd-hair)); padding: 0 var(--sp-2); background: var(--surface); border: var(--bd-hair) solid var(--line-strong); border-bottom: none; border-radius: var(--r-control) var(--r-control) 0 0; font-size: 0.74em; font-weight: 600; letter-spacing: var(--label-track); text-transform: uppercase; line-height: 1.7; white-space: nowrap; }
  .present-caption .pc-cap-yield { opacity: 0.75; font-family: var(--mono); font-size: 0.92em; margin-top: 2px; }
  /* THE VERDICT BLOCK: set off from the step by a rule, laid out as two
     columns -- the tag, then the sentence -- so tags line up and sentences
     wrap under themselves. The sentence is in the body ink; only the tag
     carries the verdict colour, an island tag in the neutral chip. */
  .present-caption .pc-cap-verdicts {
    margin-top: var(--sp-3);
    padding-top: var(--sp-3);
    border-top: var(--bd-hair) solid var(--line);
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: var(--sp-3);
    row-gap: var(--sp-2);
    align-items: baseline;
  }
  .present-caption .pc-cap-verdicts-head { grid-column: 1 / -1; font-size: 0.74em; text-transform: uppercase; letter-spacing: var(--label-track); opacity: 0.7; }
  .present-caption .pc-cap-tag {
    justify-self: start;
    font-size: 0.82em;
    font-weight: 600;
    line-height: 1.6;
    padding: 0 var(--sp-2);
    border-radius: var(--r-control);
    color: var(--err-ink);
    border: var(--bd-hair) solid var(--err-line);
    white-space: nowrap;
  }
  .present-caption .pc-cap-tag.is-island { color: var(--ink); border-color: var(--line-strong); opacity: 0.85; }
  .present-caption .pc-cap-text { line-height: 1.5; }
  .present-caption .binding-notice-var { font-family: var(--mono); font-size: 0.94em; padding: 0 0.3em; border: var(--bd-hair) solid var(--line-strong); border-radius: var(--r-chip, 4px); }
  /* WHAT THE COLOURS MEAN, one row per colour on the presentation, in the
     canvas legend's own dots; shown while the colour is on the tree. */
  .present-caption .pc-cap-legend { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin-top: var(--sp-2); font-size: 0.84em; opacity: 0.8; }
  .present-caption .pc-cap-legend-row { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
  .present-caption .pc-cap-legend-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
  /* THE STAR, as the canvas draws it: the serif face, 600 weight, four fifths
     of the word size, anchored at its end beside the label; it fades in. */
  .present-stage .present-star { fill: var(--err-ink); font-family: Georgia, 'Times New Roman', serif; font-size: 13px; font-weight: 600; transition: opacity 0.35s ease; }
  .present-controls {
    position: fixed;
    bottom: 24px;
    left: 50%;
    transform: translateX(-50%);
    background: var(--surface);
    border: var(--bd-hair) solid var(--line-strong);
    border-radius: var(--r-surface);
    padding: var(--sp-3) var(--sp-4);
    display: flex;
    align-items: center;
    gap: 8px;
    box-shadow: var(--shadow-float);
    z-index: 110;
    transition: opacity 0.3s ease;
  }
  .present-controls.idle {
    opacity: 0.2;
  }
  .present-controls:hover {
    opacity: 1;
  }
  .pc-btn {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    border: none;
    background: transparent;
    color: var(--ink);
    cursor: pointer;
    font-size: 14px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .pc-btn:hover:not(:disabled) { background: var(--bg); }
  .pc-btn:disabled { opacity: 0.3; cursor: not-allowed; }
  .pc-btn.primary {
    background: var(--ink);
    color: var(--ink-inverse);
    width: 40px;
    height: 40px;
    font-size: 14px;
  }
  .pc-btn.primary:hover { background: var(--surface-strong); }
  .pc-progress {
    font-family: var(--mono);
    font-size: 11px;
    color: var(--ink-faint);
    padding: 0 var(--sp-4);
    min-width: 50px;
    text-align: center;
  }
  .pc-divider {
    width: 1px;
    height: 20px;
    background: var(--line);
  }
  .pc-speed {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 0 var(--sp-4);
  }
  .pc-speed-label {
    font-family: var(--ui-sans);
    font-size: 10px;
    color: var(--ink-faint);
    text-transform: uppercase;
    letter-spacing: var(--label-track);
  }
  .pc-speed-slider {
    -webkit-appearance: none;
    appearance: none;
    width: 80px;
    height: 4px;
    border-radius: var(--r-control);
    background: var(--line);
    outline: none;
    cursor: pointer;
  }
  .pc-speed-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--ink);
    cursor: pointer;
    border: none;
  }
  .pc-speed-slider::-moz-range-thumb {
    width: 14px;
    height: 14px;
    border-radius: 50%;
    background: var(--ink);
    cursor: pointer;
    border: none;
  }
  .pc-speed-value {
    font-family: var(--mono);
    font-size: 10px;
    color: var(--ink);
    min-width: 30px;
    text-align: right;
  }
  /* Labeled-bracket export dialog */
  .dialog-overlay {
    position: fixed; inset: 0; z-index: 1000;
    background: var(--backdrop);
    display: flex; align-items: center; justify-content: center;
  }
  .dialog-box {
    background: var(--bg-canvas, #fff);
    width: min(720px, 92vw); max-height: 86vh;
    border-radius: var(--r-surface); box-shadow: var(--shadow-modal);
    display: flex; flex-direction: column; overflow: hidden;
    font-family: Georgia, 'Times New Roman', serif;
  }
  /* A dialog of controls, not of reading, is set in the interface face. */
  .dialog-box.dialog-ui { font-family: var(--ui-sans); }
  .dialog-head {
    display: flex; align-items: center; justify-content: space-between;
    padding: var(--sp-5) var(--sp-6); border-bottom: var(--bd-hair) solid var(--line, #e5e2dc);
  }
  /* A LINK IN A DIALOG. There was no rule for one, so it fell to the browser
     default of #0000EE — fine on paper at 9.2 against the ground, and between
     1.9 and 2.1 on the four dark skins, which is to say invisible. The accent
     is the right colour because every skin has already had it checked against
     its own canvas. */
  .dialog-box a { color: var(--accent); }
  .dialog-box a:hover { color: var(--accent-strong); }

  .dialog-title { font-family: var(--ui-display); font-size: var(--fs-display); color: var(--ink, #2c2a26); }
  .dialog-close {
    border: none; background: none; font-size: 22px; line-height: 1;
    cursor: pointer; color: var(--ink-soft, #888); padding: 0 var(--sp-2);
  }
  .dialog-close:hover { color: var(--ink, #2c2a26); }
  .brackets-controls { display: flex; gap: 14px; padding: var(--sp-5) var(--sp-6) var(--sp-3); flex-wrap: wrap; }
  .brackets-seg { display: inline-flex; border: var(--bd-line) solid var(--line-strong, #d5d0c8); border-radius: var(--r-control); overflow: hidden; }
  .brackets-seg .seg-btn {
    border: none; background: var(--bg-soft, #faf9f7); cursor: pointer;
    padding: var(--sp-2) var(--sp-5); font-size: 12px; font-family: inherit; color: var(--ink, #2c2a26);
  }
  .brackets-seg .seg-btn + .seg-btn { border-left: var(--bd-line) solid var(--line-strong, #d5d0c8); }
  .brackets-seg .seg-btn.active { background: var(--active-bg); color: var(--active-ink); box-shadow: var(--active-shadow); }
  .brackets-output {
    margin: 8px 18px; padding: var(--sp-5) var(--sp-5); min-height: 180px; resize: vertical;
    font-family: 'JetBrains Mono', Menlo, Consolas, monospace; font-size: 13px; line-height: 1.6;
    border: var(--bd-line) solid var(--line-strong, #d5d0c8); border-radius: var(--r-surface);
    background: var(--bg-soft, #faf9f7); color: var(--ink, #2c2a26);
    white-space: pre; overflow: auto;
  }
  .dialog-foot { display: flex; gap: 10px; justify-content: flex-end; align-items: center; padding: var(--sp-4) var(--sp-6) var(--sp-6); }
  .brackets-input { background: var(--bg-canvas); color: inherit; }
  .brackets-report {
    max-height: 150px; overflow-y: auto; margin: 0 18px;
    font-size: 12px; line-height: 1.6;
  }
  .br-line { padding: var(--sp-1) 0; }
  .br-err  { color: var(--err-ink); }
  .br-warn { color: var(--warn-ink); }
  .br-ok   { color: var(--ok-ink); }
  .br-note { color: var(--ink-soft); }
  .brackets-caution {
    margin: 0 18px 8px; font-size: 11.5px; line-height: 1.6; color: var(--ink-soft);
  }
  /* The fix buttons sit OUTSIDE the report, which scrolls: with several
     warnings they were pushed below the fold and had to be hunted for. */
  .brackets-actions {
    display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 18px 0;
  }
  .brackets-actions:empty { display: none; }
  .br-fix { padding: var(--sp-1) var(--sp-5); font-size: 11.5px; }
  .brackets-wordhint {
    margin-right: auto; max-width: 62%;
    font-size: 12px; line-height: 1.4; font-style: normal;
    color: var(--ink-soft, #8a857c); text-align: left;
  }
  .dialog-btn {
    border: var(--bd-line) solid var(--line-strong, #d5d0c8); background: var(--bg-canvas, #fff);
    border-radius: var(--r-control); padding: var(--sp-3) var(--sp-6); font-size: 13px; cursor: pointer;
    font-family: inherit; color: var(--ink, #2c2a26);
  }
  .dialog-btn.primary { background: var(--ink); color: var(--ink-inverse); border-color: var(--ink); }
  .dialog-btn:hover { opacity: 0.88; }
