/* ============================================================
   Tests workbench v2 — tw- namespaced component layer.
   Loads AFTER app.css / theme-luxe.css / theme-dark.css and
   consumes their tokens; declares NO globals and touches no
   selector the chrome or the v1 workbench uses, so the two
   layouts coexist and the switch is instant.
   ============================================================ */
#tests-v2-root {
  --tw-pass: var(--pass, #3cc69a);
  --tw-fail: var(--fail, #c1153a);
  --tw-partial: var(--partial, #faa938);
  --tw-pending: #9a9b98;
  --tw-inprog: var(--accent, #4180f2);
  --tw-nt: #b6b7b4;
  --tw-coral: var(--create, #f55819);
  --tw-coral-hover: linear-gradient(135deg, #ff7a42, #f5581c);
  --tw-ink: #1d2554;
  --tw-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --tw-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  display: flex; flex-direction: column;
  /* Fill the content-area viewport (header 56px + content padding 24px×2)
     so the list and inspector scroll internally, not the page. */
  height: calc(100vh - var(--header-height, 56px) - 48px);
  min-height: 420px;
}
/* shared tokens for the overlay layers appended to <body> */
.tw-pop-layer, .tw-modal-layer {
  --tw-pass: var(--pass, #3cc69a); --tw-fail: var(--fail, #c1153a);
  --tw-partial: var(--partial, #faa938); --tw-pending: #9a9b98;
  --tw-inprog: var(--accent, #4180f2); --tw-nt: #b6b7b4;
  --tw-coral: var(--create, #f55819);
  --tw-coral-hover: linear-gradient(135deg, #ff7a42, #f5581c);
  --tw-ink: #1d2554; --tw-ease: cubic-bezier(0.22, 1, 0.36, 1);
  --tw-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
  font-family: var(--font, 'Montserrat', sans-serif); color: var(--text);
}
[data-theme="dark"] .tw-pop-layer, [data-theme="dark"] .tw-modal-layer,
[data-theme="dark"] #tests-v2-root { --tw-ink: #4a5694; }

/* spec-gen proposal banner */
.tw-proposal-banner {
  display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
  border: 1.5px solid var(--accent); border-radius: var(--radius);
  padding: 10px 14px; margin-bottom: 12px; background: var(--bg-secondary);
  flex-shrink: 0;
}

/* ---------- command bar ---------- */
.tw-cmdbar {
  display: flex; align-items: center; gap: 10px; padding: 10px 14px;
  background: var(--bg-card); border: 1px solid var(--border-light);
  border-radius: var(--radius-lg); box-shadow: var(--shadow); position: relative; z-index: 5;
  flex-shrink: 0;
}
.tw-ctl {
  display: inline-flex; align-items: center; gap: 7px; padding: 8px 13px;
  border: 1px solid var(--border); border-radius: 999px; font-weight: 600; font-size: 12.5px;
  background: var(--bg-card); color: var(--text); white-space: nowrap; cursor: pointer;
  font-family: inherit; transition: all .2s var(--tw-ease);
}
.tw-ctl:hover { border-color: rgba(37,51,120,.22); background: var(--bg-hover); }
.tw-caret { font-size: 9px; opacity: .55; }
.tw-ctl-label { color: var(--text-muted); font-weight: 500; }
.tw-badge {
  min-width: 17px; height: 17px; padding: 0 5px; border-radius: 999px; background: var(--tw-ink); color: #fff;
  font-size: 10px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center;
}
.tw-ctl.on { border-color: var(--tw-ink); background: var(--tw-ink); color: #fff; }
.tw-ctl.on .tw-ctl-label { color: rgba(255,255,255,.65); }
.tw-search { position: relative; display: flex; align-items: center; min-width: 150px; flex: 0 1 220px; }
.tw-search svg { position: absolute; left: 12px; color: var(--text-muted); pointer-events: none; }
.tw-search input {
  width: 100%; padding: 8px 12px 8px 33px; border: 1px solid var(--border); border-radius: 999px;
  background: var(--bg-secondary); color: var(--text); font-family: inherit; font-size: 13px;
  transition: all .2s var(--tw-ease);
}
.tw-search input:focus { background: var(--bg-card); border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }

/* the status spine — suite health that IS the status filter */
.tw-spine { flex: 1; display: flex; height: 26px; border-radius: 999px; overflow: hidden; min-width: 120px;
  box-shadow: inset 0 0 0 1px var(--border-light); }
/* flex-basis:auto + min-width:max-content = grow in proportion to the count,
   but never shrink past the segment's own "Pass 7" label. */
.tw-spine-seg { position: relative; display: flex; align-items: center; justify-content: center;
  gap: 4px; flex-basis: auto; min-width: max-content; border: 0; padding: 0 9px; cursor: pointer;
  transition: filter .15s var(--tw-ease), opacity .2s var(--tw-ease); }
.tw-spine-seg:hover { filter: brightness(1.06); }
.tw-spine-seg .tw-seg-lbl, .tw-spine-seg .tw-seg-n {
  font-size: 10px; font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.28);
  white-space: nowrap; pointer-events: none;
}
.tw-spine-seg .tw-seg-lbl { letter-spacing: .02em; }
.tw-spine-seg .tw-seg-n { font-family: var(--tw-mono); opacity: .85; }
.tw-spine-seg:hover .tw-seg-n, .tw-spine-seg.on .tw-seg-n { opacity: 1; }
/* Overflow rungs, applied by fitSpine() in tests-v2.js — see the comment there.
   min-width:max-content above means a spine too narrow for every segment's own
   label CLIPS the tail ones out of existence instead of squeezing them, so a
   status the user needs to see (a lone Fail) can vanish. Give up detail in a
   fixed order rather than whole segments: the word first, then the number. */
.tw-spine.compact .tw-spine-seg { padding: 0 6px; }
.tw-spine.compact .tw-spine-seg .tw-seg-lbl { display: none; }
/* Last rung: let the segments shrink again. Counts go with the width, which is
   the pre-7d2a3162 look — illegible, but every status still holds its band and
   its colour, and the tooltip/aria-label still name it. */
.tw-spine.micro .tw-spine-seg { min-width: 4px; padding: 0; }
.tw-spine.micro .tw-spine-seg .tw-seg-n { display: none; }
.tw-spine.filtered .tw-spine-seg:not(.on) { opacity: .28; }
.tw-spine-seg.on { box-shadow: inset 0 0 0 2px rgba(255,255,255,.75); }
.tw-cmd-actions { display: flex; align-items: center; gap: 8px; }

/* buttons */
.twb {
  display: inline-flex; align-items: center; gap: 7px; padding: 8px 15px; border-radius: 999px;
  font-weight: 700; font-size: 12.5px; border: 0; cursor: pointer; font-family: inherit;
  color: var(--text); background: none; white-space: nowrap; transition: all .2s var(--tw-ease);
}
.twb-ghost { border: 1px solid var(--border); background: var(--bg-card); }
.twb-ghost:hover { border-color: rgba(37,51,120,.22); background: var(--bg-hover); }
.twb-cta { background: var(--tw-coral); color: #fff; box-shadow: 0 6px 16px -6px rgba(245,88,25,.55); }
.twb-cta:hover { background: var(--tw-coral-hover); box-shadow: 0 8px 22px -6px rgba(245,88,25,.6); }
.twb-icon { padding: 8px 11px; }
.twb-ink { background: var(--tw-ink); color: #fff; }
.twb-ink:hover { filter: brightness(1.12); }
.twb-danger { color: var(--tw-fail); border: 1px solid rgba(193,21,58,.3); background: var(--bg-card); }
.twb-danger:hover { background: rgba(193,21,58,.08); }
.twb-sm { padding: 5px 11px; font-size: 11.5px; }

/* quiet one-line active-filter summary (no pills) */
.tw-filter-summary {
  padding: 7px 18px 0; font-size: 11.5px; color: var(--text-secondary);
  display: flex; gap: 14px; align-items: center; flex-shrink: 0;
}
/* renderFilterBtn() takes this row down with the `hidden` attribute once no
   filter is active. Same author-vs-UA cascade as .tw-bulkbar[hidden] below —
   the display:flex above outranks the UA stylesheet's [hidden]{display:none},
   so without this the emptied row keeps its 7px top padding as a phantom gap. */
.tw-filter-summary[hidden] { display: none; }
.tw-filter-summary b { font-weight: 700; color: var(--text); }
.tw-clear-link { color: var(--accent); font-weight: 600; cursor: pointer; }
.tw-clear-link:hover { text-decoration: underline; }

/* ---------- split ---------- */
.tw-split { flex: 1; display: flex; gap: 16px; min-height: 0; padding: 14px 0 4px; }
.tw-list-pane {
  flex: 0 0 46%; min-width: 360px; overflow-y: auto; border-radius: var(--radius-lg);
  background: var(--bg-card); border: 1px solid var(--border-light); box-shadow: var(--shadow);
  scrollbar-width: thin;
}
.tw-inspector {
  flex: 1; min-width: 0; overflow-y: auto; border-radius: var(--radius-lg);
  background: var(--bg-card); border: 1px solid var(--border-light); box-shadow: var(--shadow);
  scrollbar-width: thin;
}

/* area group header */
.tw-area-head {
  position: sticky; top: 0; z-index: 3; display: flex; align-items: center; gap: 9px;
  padding: 9px 16px; background: var(--bg-card); backdrop-filter: blur(6px);
  border-bottom: 1px solid var(--border-light); cursor: pointer; user-select: none;
}
/* Per-area select ("all the YMS ones") — item ec06ee45. Reveal mirrors the row
   checkbox rule below: invisible at rest so the header keeps its weight, shown
   on hover, and pinned visible whenever it carries state (checked or the
   partial dash) or the pane is already in selecting mode. opacity — not
   visibility/display — because the box stays clickable while transparent,
   which is what lets a first click land on it. */
.tw-area-head .tw-area-sel { display: flex; align-items: center; flex-shrink: 0; cursor: pointer; }
.tw-area-head .tw-area-cb {
  width: 14px; height: 14px; accent-color: var(--tw-ink); opacity: 0; transition: opacity .12s;
  flex-shrink: 0; cursor: pointer; margin: 0;
}
.tw-area-head:hover .tw-area-cb, .tw-area-head .tw-area-cb:checked,
.tw-area-head .tw-area-cb:indeterminate, .tw-list-pane.selecting .tw-area-cb { opacity: 1; }
.tw-area-head .tw-twirl { font-size: 9px; color: var(--text-muted); transition: transform .2s var(--tw-ease); }
.tw-area-head.closed .tw-twirl { transform: rotate(-90deg); }
.tw-area-head h3 { font-size: 11px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; margin: 0; color: var(--text); }
.tw-area-head .tw-area-n { font-size: 10.5px; color: var(--text-muted); font-family: var(--tw-mono); }
.tw-area-head .tw-area-mini { display: flex; gap: 4px; margin-left: auto; align-items: center; }
.tw-area-mini i { display: block; width: 7px; height: 7px; border-radius: 50%; font-style: normal; }
/* .tw-area-run and .tw-rrun are GONE. Both were opacity:0 until their row or
   header was hovered, which is precisely why a tester could not find the
   watchable Run at all and fell through to the headless batch. Their scopes are
   now first-class rows in the one Run dialog below. */

/* rows */
/* The 26px of left padding is the lane the reorder cluster is positioned into
   (item 034679c8 — see .tw-rmove below). It is stated as the DEFAULT and taken
   away again from the rows that draw no cluster, rather than the other way
   round, so that the fallback is the safe one: a browser without :has() drops
   the rule below and every row keeps 26px, which is 14px more indent than a
   flat sort needs but nothing overlapping. Written as `:has()` in the positive
   direction, that same browser would give a cluster row 12px and paint the
   controls over its checkbox. */
.tw-row {
  display: flex; align-items: center; gap: 10px; padding: 8px 16px 8px 26px; cursor: pointer;
  border-bottom: 1px solid var(--border-light); transition: background .12s;
  position: relative;
}
/* A flat sort renders no `.tw-rmove` at all (see rowHtml's `movable`), and
   those rows already started at the group header's chevron. */
.tw-row:not(:has(.tw-rmove)) { padding-left: 12px; }
.tw-row:hover { background: var(--bg-hover); }
.tw-row.sel { background: var(--accent-soft); box-shadow: inset 3px 0 0 var(--accent); }
.tw-row.checked { background: rgba(65,128,242,.06); }
.tw-row input[type="checkbox"] {
  width: 14px; height: 14px; accent-color: var(--tw-ink); opacity: 0; transition: opacity .12s; flex-shrink: 0;
}
.tw-row:hover input, .tw-row input:checked, .tw-list-pane.selecting .tw-row input { opacity: 1; }
/* 92px is NOT slack here, however much of it an 8-character hex id leaves
   empty. Item 034679c8 narrowed this to 64px and had to put it back: the font
   measures 6.32px per character, and while wayfair's ids are 7-9 characters
   (44.3-56.9px), the fleet also runs 599 mclane rows on `UNT-REC-290F2E`
   (14 characters, 88.5px) and daikin's `4.01.01.00_HARDEN`, and a user may type
   their own up to TEST_ID_MAX = 64 (api/csv_import.py). Those schemes carry
   their discriminator in the SUFFIX, so clipping one does not abbreviate a row,
   it makes a screenful of them read alike. Measure against the fleet, not
   against the instance that filed the bug. */
.tw-row .tw-rid { font-family: var(--tw-mono); font-size: 10.5px; font-weight: 600; color: var(--text-secondary);
  flex-shrink: 0; width: 92px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-row .tw-rtitle { flex: 1; font-size: 12.5px; font-weight: 500; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-row .tw-rglyphs { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
.tw-rglyph { font-size: 11px; opacity: .8; cursor: help; }
/* 66px = the widest chip this can hold plus a hair. statusLabel() emits exactly
   seven strings and the longest, "Running", measures 65px; the 20px on top of
   that was the gap the reporter saw between the end of the title and the
   right-aligned metadata, and no label ever reached into it. Still a FIXED
   column, not a content width: `justify-content: flex-end` right-aligns the ink
   either way, but sizing to content makes the TITLE cell's right edge ragged
   across a mixed-status group (measured: 31px of spread over the seven, versus
   0 here). */
.tw-row .tw-rstatus { flex-shrink: 0; display: inline-flex; align-items: center; gap: 6px; width: 66px; justify-content: flex-end;
  font-size: 10.5px; font-weight: 700; }
/* Never shrink the dot. The cell used to carry 20px of slack that absorbed any
   overshoot; at 66px a reader running a minimum font size pushes "Running" to
   the edge, and a shrinkable dot deforms into an ellipse — the one thing in the
   row whose SHAPE is meaning — before the label does anything visible. */
.tw-rstatus i { width: 8px; height: 8px; flex-shrink: 0; border-radius: 50%; display: block; font-style: normal; }

/* Reorder controls within a group — item 7c627bc6. Revealed the same way as the
   row checkbox above, and for the same documented reason: opacity, never
   visibility/display. These are the ACCESSIBLE half of the feature — HTML5 drag
   fires from neither touch nor a keyboard — so they must stay in the
   accessibility tree and stay focusable while transparent, which display:none
   and visibility:hidden would both break. :focus-within paints the cluster the
   moment one is tabbed to, so a keyboard user can see what they are aiming at.
   Revealing the cluster must never shift the row — that was the whole point of
   reserving its width, and it still is.

   It is a VERTICAL column in the row's own left padding now (item 034679c8).
   Laid out in a line the three controls needed 53px of every row's width, all
   of it invisible until you hovered, which put the id 60px right of the group
   header's chevron and left the band of white the reporter was looking at.
   Out of flow it cannot shift anything at all — a stronger guarantee than the
   reservation gave — and stacked it needs 20px instead of 53px. The lane it
   sits in is `.tw-row`'s left padding above. `left: 3px` clears the 3px inset
   bar a selected row paints down its own left edge. */
.tw-row .tw-rmove { position: absolute; left: 3px; top: 0; bottom: 0; width: 20px;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  opacity: 0; transition: opacity .12s; }
.tw-row:hover .tw-rmove, .tw-row .tw-rmove:focus-within { opacity: 1; }
/* Full-width of the column, so the drag target is WIDER than the 11x12 the
   glyph used to occupy even though the column is shorter. */
.tw-row .tw-rgrip { cursor: grab; color: var(--text-muted); font-size: 9px; line-height: 1;
  width: 20px; height: 9px; padding: 0; user-select: none;
  display: flex; align-items: center; justify-content: center; }
.tw-row .tw-rgrip:active { cursor: grabbing; }
/* Targets sized here rather than by the label. `data-no-norm` on the
   cluster keeps js/ui-normalize.js off these two: its icon-only test reads a
   glyph set that does not include the ▲/▼ these carry, so it classified them as
   ordinary neutral buttons and dressed them at the §5.1 default — 34px tall
   with 15px of side padding, i.e. 42px wide EACH, a 96px cluster, and a row
   18px taller than any of its own content. Sizing the box here is what keeps
   the target off the glyph. The hover tint (the pill radius still comes from
   the system) is what makes each half read as separately clickable now that
   neither carries a border at rest.

   20x20 became 20x12 when the cluster went vertical (item 034679c8): the three
   controls share the row's 35.75px of height, and growing the row to keep the
   squares would have spent more space than the change gives back. This is the
   one real cost of that change and it is a shrink of an already-sub-24px
   target — the pointer alternatives (drag, and "↕ Sort by number" on the group
   header) are unchanged, and both buttons stay keyboard-reachable. */
.tw-row .tw-rup, .tw-row .tw-rdn {
  border: 0; background: none; cursor: pointer; padding: 0; line-height: 1;
  width: 20px; height: 12px; flex-shrink: 0;
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 7px; color: var(--text-muted); font-family: inherit;
  transition: background .12s, color .12s;
}
.tw-row .tw-rup:hover, .tw-row .tw-rdn:hover { color: var(--accent); background: var(--accent-soft); }
.tw-row .tw-rup:focus-visible, .tw-row .tw-rdn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.tw-row.tw-dragging { opacity: .45; }
/* Inset, so the marker cannot add height and shuffle the rows under the pointer
   mid-drag — which would make the drop land somewhere the user did not aim. */
.tw-row.tw-drop-above { box-shadow: inset 0 3px 0 0 var(--accent); }
.tw-row.tw-drop-below { box-shadow: inset 0 -3px 0 0 var(--accent); }

.tw-area-head .tw-area-seq { opacity: 0; font-size: 10.5px; font-weight: 700; color: var(--accent);
  padding: 3px 9px; border-radius: 999px; border: 0; background: none; cursor: pointer;
  font-family: inherit; transition: opacity .15s; }
.tw-area-head:hover .tw-area-seq, .tw-area-head .tw-area-seq:focus-visible { opacity: 1; }
.tw-area-head .tw-area-seq:hover { background: var(--accent-soft); }
.tw-load-more { display: block; width: 100%; text-align: center; padding: 10px; font-size: 11.5px; font-weight: 600;
  color: var(--accent); border: 0; background: none; cursor: pointer; font-family: inherit; }
.tw-load-more:hover { background: var(--bg-hover); }
.tw-list-empty { padding: 60px 30px; text-align: center; color: var(--text-secondary); }
.tw-list-empty .tw-big { font-size: 30px; margin-bottom: 10px; }
.tw-list-empty h4 { font-size: 14px; font-weight: 700; color: var(--text); margin: 0 0 6px; }
.tw-list-empty p { font-size: 12px; line-height: 1.6; max-width: 380px; margin: 0 auto 14px; }

/* status colors */
.tw-c-pass { background: var(--tw-pass); } .tw-c-fail { background: var(--tw-fail); }
.tw-c-pending { background: var(--tw-pending); } .tw-c-partial { background: var(--tw-partial); }
.tw-c-in-progress { background: var(--tw-inprog); } .tw-c-blocked { background: #7a0f26; }
.tw-c-not-testable { background: var(--tw-nt); }
.tw-t-pass { color: var(--tw-pass); } .tw-t-fail { color: var(--tw-fail); } .tw-t-pending { color: var(--tw-pending); }
.tw-t-partial { color: #d98b13; } .tw-t-in-progress { color: var(--tw-inprog); } .tw-t-blocked { color: #7a0f26; }
.tw-t-not-testable { color: var(--tw-nt); }

/* ---------- inspector ---------- */
.tw-insp-empty { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;
  color: var(--text-muted); gap: 10px; padding: 40px; text-align: center; }
.tw-insp-empty .tw-big { font-size: 34px; opacity: .5; }
.tw-insp-empty p { font-size: 12.5px; line-height: 1.6; max-width: 300px; }
.tw-kbd { font-family: var(--tw-mono); font-size: 10px; background: var(--bg-secondary); border: 1px solid var(--border);
  border-bottom-width: 2px; border-radius: 5px; padding: 1px 6px; }

.tw-insp-head { padding: 18px 22px 0; }
.tw-insp-id-row { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
.tw-insp-id { font-family: var(--tw-mono); font-size: 12px; font-weight: 700; color: var(--text-secondary);
  background: var(--bg-secondary); padding: 3px 10px; border-radius: 7px; }
.tw-status-chip {
  display: inline-flex; align-items: center; gap: 7px; padding: 4px 12px; border-radius: 999px;
  font-size: 11px; font-weight: 700; cursor: pointer; border: 1px solid transparent;
  font-family: inherit; transition: all .15s var(--tw-ease);
}
.tw-status-chip:hover { filter: brightness(.97); box-shadow: var(--shadow-sm); }
.tw-status-chip i { width: 8px; height: 8px; border-radius: 50%; display: block; font-style: normal; }
.tw-s-pass { background: rgba(60,198,154,.16); color: #157a5b; } .tw-s-fail { background: rgba(193,21,58,.10); color: var(--tw-fail); }
.tw-s-pending { background: rgba(103,104,101,.10); color: var(--text-secondary); } .tw-s-partial { background: rgba(250,169,56,.14); color: #b06d05; }
.tw-s-in-progress { background: var(--accent-soft); color: var(--accent); }
.tw-s-blocked { background: rgba(193,21,58,.10); color: #7a0f26; } .tw-s-not-testable { background: rgba(103,104,101,.10); color: #8b8c89; }
[data-theme="dark"] .tw-s-pass { color: #5fe0b7; } [data-theme="dark"] .tw-s-partial { color: #ffc46b; }
.tw-insp-title { font-size: 16px; font-weight: 800; letter-spacing: -.01em; line-height: 1.35; margin-bottom: 4px; color: var(--text); }
.tw-insp-sub { font-size: 11.5px; color: var(--text-secondary); display: flex; gap: 14px; flex-wrap: wrap; }
.tw-insp-sub b { font-weight: 600; color: var(--text); }
.tw-insp-actions { display: flex; gap: 8px; margin-top: 14px; align-items: center; }

.tw-tabs { display: flex; gap: 2px; margin-top: 16px; border-bottom: 1px solid var(--border-light); padding: 0 22px; }
.tw-tab {
  padding: 9px 14px; font-size: 12px; font-weight: 700; color: var(--text-secondary);
  border: 0; background: none; cursor: pointer; font-family: inherit;
  border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color .15s;
}
.tw-tab:hover { color: var(--text); }
/* `.active` is the design-system name (design-review.css §6 folds all 25 tab
   variants onto it). This local rule is the fallback for that file's absence;
   it stays unprefixed so the shared rule keeps winning when both are loaded. */
.tw-tab.active { color: var(--text); border-bottom-color: var(--tw-ink); }
.tw-tab .tw-tab-n { font-size: 9.5px; font-family: var(--tw-mono); background: var(--bg-secondary);
  padding: 1px 6px; border-radius: 999px; margin-left: 5px; color: var(--text-muted); }
.tw-tab-body { padding: 18px 22px 30px; }

.tw-sect { margin-bottom: 20px; }
.tw-sect > h4 { font-size: 10.5px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted); margin: 0 0 8px; }
.tw-sect p { font-size: 12.5px; line-height: 1.65; color: var(--text); white-space: pre-wrap; word-break: break-word; margin: 0; }
.tw-meta-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.tw-meta-cell { background: var(--bg-secondary); border-radius: var(--radius-sm); padding: 9px 12px; }
.tw-meta-cell .tw-k { font-size: 9.5px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; color: var(--text-muted); }
.tw-meta-cell .tw-v { font-size: 12px; font-weight: 600; margin-top: 3px; color: var(--text); }

/* steps */
.tw-steps { counter-reset: twstep; }
.tw-step { display: flex; gap: 12px; padding: 7px 0; border-bottom: 1px dashed var(--border-light);
  font-size: 12.5px; line-height: 1.6; color: var(--text); }
.tw-step::before {
  counter-increment: twstep; content: counter(twstep, decimal-leading-zero);
  font-family: var(--tw-mono); font-size: 10px; font-weight: 700; color: var(--text-muted); padding-top: 3px; flex-shrink: 0;
}
.tw-step.exp::before { content: "✓"; color: var(--tw-pass); font-size: 12px; }

/* ---------- hardening provenance rail ---------- */
.tw-harden-rail {
  border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; margin-bottom: 20px;
  background: linear-gradient(180deg, rgba(65,128,242,.035), transparent 55%);
}
.tw-hr-track { display: flex; align-items: center; gap: 0; margin-bottom: 10px; }
.tw-hr-node { display: flex; align-items: center; gap: 7px; font-size: 10.5px; font-weight: 700; color: var(--text-muted); white-space: nowrap; }
.tw-hr-node .tw-nd { width: 9px; height: 9px; border-radius: 50%; background: var(--border); box-shadow: 0 0 0 3px rgba(37,51,120,.05); }
.tw-hr-node.done { color: var(--text); } .tw-hr-node.done .tw-nd { background: var(--tw-ink); }
.tw-hr-node.live { color: var(--accent); } .tw-hr-node.live .tw-nd { background: var(--accent); animation: tw-pulse 2s infinite; }
.tw-hr-node.gold { color: #157a5b; } .tw-hr-node.gold .tw-nd { background: var(--tw-pass); }
.tw-hr-link { flex: 1; height: 1.5px; background: var(--border); margin: 0 8px; min-width: 14px; }
.tw-hr-link.done { background: var(--tw-ink); }
@keyframes tw-pulse { 0%,100% { box-shadow: 0 0 0 3px rgba(65,128,242,.15);} 50% { box-shadow: 0 0 0 6px rgba(65,128,242,.06);} }
.tw-hr-blurb { font-size: 11.5px; color: var(--text-secondary); line-height: 1.55; }
.tw-hr-actions { display: flex; gap: 8px; margin-top: 10px; align-items: center; flex-wrap: wrap; }
.tw-hr-stamp { font-size: 11px; font-weight: 700; color: #157a5b; display: inline-flex; align-items: center; gap: 6px; }

/* harden diff */
.tw-diff { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.tw-diff-col { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; min-width: 0; }
.tw-diff-col > h5 { font-size: 10px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase; padding: 8px 12px; margin: 0; }
.tw-diff-col.was > h5 { background: var(--bg-secondary); color: var(--text-muted); }
.tw-diff-col.now > h5 { background: rgba(60,198,154,.16); color: #157a5b; }
.tw-diff-col ul { list-style: none; padding: 6px 12px 10px; margin: 0; max-height: 300px; overflow-y: auto; }
.tw-diff-col li { font-size: 11.5px; line-height: 1.6; padding: 5px 0; border-bottom: 1px dashed var(--border-light); word-break: break-word; color: var(--text); }
.tw-diff-col.was li { color: var(--text-secondary); }
.tw-unverified { border-left: 3px solid var(--tw-partial); background: rgba(250,169,56,.10); border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  padding: 10px 14px; margin-top: 12px; }
.tw-unverified h5 { font-size: 10.5px; font-weight: 800; color: #b06d05; margin: 0 0 5px; }
.tw-unverified li { font-size: 11.5px; line-height: 1.6; margin-left: 16px; color: var(--text); }

/* runs */
.tw-run-item { border: 1px solid var(--border-light); border-radius: var(--radius); padding: 12px 15px; margin-bottom: 12px;
  transition: box-shadow .2s var(--tw-ease); }
.tw-run-item:hover { box-shadow: var(--shadow); }
.tw-run-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tw-run-head .tw-mode { font-size: 10px; font-weight: 700; letter-spacing: .05em; text-transform: uppercase; color: var(--text-muted); }
.tw-run-head .tw-when { font-size: 11px; color: var(--text-muted); margin-left: auto; font-family: var(--tw-mono); }
.tw-run-notes { font-size: 11.5px; color: var(--text-secondary); line-height: 1.6; margin-top: 8px;
  max-height: 120px; overflow: hidden; position: relative; white-space: pre-wrap; word-break: break-word; }
.tw-run-notes.open { max-height: none; }
.tw-run-notes:not(.open)::after { content: ""; position: absolute; inset: auto 0 0; height: 40px;
  background: linear-gradient(transparent, var(--bg-card)); }
.tw-run-notes code { font-family: var(--tw-mono); font-size: 10.5px; background: var(--bg-secondary); padding: 1px 5px; border-radius: 4px; }
.tw-run-more { font-size: 11px; font-weight: 700; color: var(--accent); margin-top: 4px;
  border: 0; background: none; cursor: pointer; font-family: inherit; padding: 0; }
.tw-shots { display: flex; gap: 8px; margin-top: 10px; flex-wrap: wrap; }
.tw-shot { width: 88px; height: 58px; border-radius: 8px; object-fit: cover; object-position: top;
  border: 1px solid var(--border); cursor: zoom-in; transition: transform .18s var(--tw-ease), box-shadow .18s; }
.tw-shot:hover { transform: translateY(-2px) scale(1.03); box-shadow: var(--shadow-md); }
.tw-shot-ph { width: 88px; height: 58px; border-radius: 8px; background: var(--bg-secondary); border: 1px dashed var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 9.5px; color: var(--text-muted); text-align: center; padding: 4px; }
.tw-wms-note { font-size: 11px; color: var(--text-secondary); background: var(--bg-secondary); border-radius: var(--radius-sm);
  padding: 8px 12px; margin-top: 8px; line-height: 1.5; }

/* ---------- popovers ---------- */
.tw-pop-layer { position: fixed; inset: 0; pointer-events: none; z-index: 900; }
.tw-pop {
  position: absolute; pointer-events: auto; background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-lg); min-width: 230px; max-width: 360px;
  animation: tw-popin .18s var(--tw-ease); overflow: hidden;
}
@keyframes tw-popin { from { opacity: 0; transform: translateY(-5px) scale(.98); } }
.tw-pop-title { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; color: var(--text-muted);
  padding: 12px 16px 6px; }
.tw-pop-opt { display: flex; align-items: center; gap: 10px; width: 100%; text-align: left; padding: 9px 16px;
  font-size: 12.5px; font-weight: 600; color: var(--text); border: 0; background: none; cursor: pointer;
  font-family: inherit; transition: background .1s; }
.tw-pop-opt:hover { background: var(--bg-hover); }
.tw-pop-opt.on { background: var(--accent-soft); }
.tw-pop-opt .tw-n { margin-left: auto; font-family: var(--tw-mono); font-size: 10.5px; color: var(--text-muted); }
.tw-pop-opt .tw-hint { display: block; font-size: 10.5px; color: var(--text-secondary); font-weight: 400; margin-top: 2px; }
.tw-pop-opt .tw-check { width: 15px; text-align: center; color: var(--tw-ink); font-weight: 800; }
.tw-pop-sep { height: 1px; background: var(--border-light); margin: 6px 0; }
.tw-pop-foot { display: flex; gap: 8px; padding: 10px 14px; border-top: 1px solid var(--border-light); background: var(--bg-secondary); }
.tw-pop-foot .tw-spacer { flex: 1; }

/* filter popover */
.tw-fpop { width: 330px; max-width: 330px; }
.tw-fpop-body { max-height: 62vh; overflow-y: auto; padding-bottom: 4px; }
.tw-f-sect { padding: 4px 0 8px; }
.tw-f-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 7px 16px; text-align: left;
  font-size: 12.5px; font-weight: 600; color: var(--text); border: 0; background: none; cursor: pointer;
  font-family: inherit; border-radius: 0; transition: background .1s; }
.tw-f-row:hover { background: var(--bg-hover); }
.tw-f-row i { width: 9px; height: 9px; border-radius: 50%; display: block; font-style: normal; flex-shrink: 0; }
.tw-f-row .tw-n { margin-left: auto; font-family: var(--tw-mono); font-size: 10.5px; color: var(--text-muted); }
.tw-f-row .tw-check { width: 16px; height: 16px; border-radius: 5px; border: 1.5px solid var(--border);
  display: flex; align-items: center; justify-content: center; font-size: 10px; color: #fff; flex-shrink: 0;
  transition: all .12s var(--tw-ease); }
.tw-f-row.on .tw-check { background: var(--tw-ink); border-color: var(--tw-ink); }
.tw-f-row.dim { opacity: .45; }
.tw-f-select { margin: 2px 16px 8px; width: calc(100% - 32px); padding: 8px 12px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--bg-secondary); color: var(--text); font-weight: 600; font-family: inherit; font-size: 12.5px; }
.tw-f-toggle-track { width: 30px; height: 17px; background: var(--border); border-radius: 999px; position: relative;
  transition: background .15s var(--tw-ease); flex-shrink: 0; }
.tw-f-toggle-track::after { content: ""; position: absolute; top: 2px; left: 2px; width: 13px; height: 13px;
  background: #fff; border-radius: 50%; transition: left .15s var(--tw-ease); box-shadow: 0 1px 3px rgba(0,0,0,.25); }
.tw-f-row.on .tw-f-toggle-track { background: var(--tw-ink); }
.tw-f-row.on .tw-f-toggle-track::after { left: 15px; }

/* ---------- modals ---------- */
.tw-modal-layer { position: fixed; inset: 0; z-index: 950; display: none; }
.tw-modal-layer.show { display: flex; align-items: center; justify-content: center; background: rgba(23,26,63,.45);
  animation: tw-fade .2s var(--tw-ease); }
@keyframes tw-fade { from { opacity: 0; } }
.tw-modal { background: var(--bg-card); border-radius: var(--radius-lg); box-shadow: var(--shadow-lg);
  width: min(600px, 94vw); max-height: 86vh; overflow-y: auto; padding: 24px; animation: tw-mup .25s var(--tw-ease); }
@keyframes tw-mup { from { opacity: 0; transform: translateY(14px) scale(.98); } }
/* The Add/Edit Test dialog only (item f37aade7). Every other dialog here is a
   single column of fields, where 600px reads better than a wide measure; this
   one carries the step editor's two side-by-side text columns, and at 600px
   each column was under 200px wide. The 94vw floor is unchanged, so nothing
   about the narrow-screen layout moves. */
.tw-modal.tw-modal-steps { width: min(940px, 94vw); }
.tw-modal h3 { font-size: 16px; font-weight: 800; margin: 0 0 4px; color: var(--text); }
.tw-modal .tw-m-sub { font-size: 12px; color: var(--text-secondary); margin: 0 0 16px; line-height: 1.55; }
.tw-modal label.tw-fl { display: block; font-size: 10px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  color: var(--text-muted); margin: 14px 0 5px; }
.tw-modal input[type="text"], .tw-modal textarea, .tw-modal select {
  width: 100%; padding: 9px 12px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-secondary); color: var(--text); resize: vertical; font-family: inherit; font-size: 13px;
  box-sizing: border-box;
}
.tw-modal input:focus, .tw-modal textarea:focus { background: var(--bg-card); border-color: var(--accent); outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft); }
.tw-m-foot { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; padding-top: 14px;
  border-top: 1px solid var(--border-light); align-items: center; }
.tw-m-foot .tw-m-status { margin-right: auto; font-size: 11.5px; color: var(--text-secondary); }

/* ── The one Run dialog: What / How / Where ──────────────────────────────────
   A disabled option stays VISIBLE and keeps its reason on screen — that is the
   whole point of the surface. Never display:none a rule the user is entitled to
   understand, and never gate a reason behind a hover (this screen already
   learned that lesson from the two hover-only Run buttons this dialog
   replaced). Opacity is the dimming, so the text stays selectable and stays in
   the accessibility tree. */
.tw-run-sec { font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--text-muted); margin: 13px 0 6px; }
.tw-run-sec:first-of-type { margin-top: 8px; }
.tw-run-opt { display: grid; grid-template-columns: auto 1fr; gap: 1px 10px; align-items: baseline;
  padding: 7px 11px; border: 1.5px solid var(--border); border-radius: var(--radius);
  margin-bottom: 5px; cursor: pointer; transition: border-color .15s var(--tw-ease), background .15s var(--tw-ease); }
/* The action button must never scroll out of reach: this dialog is a decision,
   and a decision you cannot press is not one.

   It used to be a `position:sticky` footer inside the scrolling modal, and
   that is what clipped the last option: sticky keeps the footer in view by
   letting content run UNDERNEATH it, so "Run in the background" scrolled
   behind an opaque bar and lost its bottom border — the box looked sliced in
   half. A sticky element cannot reserve its own space.

   The fix is structural rather than more padding: `.tw-modal-split` makes the
   modal a flex column of head / scrolling body / footer, so the footer is a
   sibling of the scroll area instead of an overlay on top of it. Nothing can
   ever be drawn under it, and the body's scrollbar stops where the footer
   starts. */
.tw-modal.tw-modal-split { display: flex; flex-direction: column; padding: 0; overflow: hidden; }
.tw-modal-split > .tw-m-head { flex: 0 0 auto; padding: 24px 24px 0; }
.tw-modal-split > .tw-m-body { flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain;
  padding: 0 24px; }
/* The last option needs its own breathing room: a border that ends flush
   against the footer rule still reads as clipped even when it is not. */
.tw-modal-split > .tw-m-body > :last-child { margin-bottom: 10px; }
.tw-modal-split > .tw-m-foot { flex: 0 0 auto; margin: 0; padding: 14px 24px 18px;
  border-top: 1px solid var(--border-light); background: var(--bg-card); }
.tw-run-opt:hover:not(.tw-run-off) { border-color: var(--accent); background: var(--bg-hover); }
.tw-run-opt.on { border-color: var(--tw-ink); background: var(--accent-soft); }
.tw-run-opt input { margin: 0; grid-row: 1 / span 2; align-self: center; accent-color: var(--tw-ink); }
.tw-run-lbl { font-size: 13px; font-weight: 700; color: var(--text); }
.tw-run-why { font-size: 11.5px; color: var(--text-secondary); grid-column: 2; line-height: 1.4; }
.tw-run-off { cursor: not-allowed; opacity: .55; border-style: dashed; }
.tw-run-off .tw-run-why { color: var(--fail, #c1153a); }
.tw-gen-card { display: flex; gap: 12px; padding: 13px 15px; border: 1.5px solid var(--border); border-radius: var(--radius);
  cursor: pointer; margin-bottom: 9px; transition: all .15s var(--tw-ease); }
.tw-gen-card:hover { border-color: var(--accent); background: var(--bg-hover); }
.tw-gen-card.on { border-color: var(--tw-ink); background: var(--accent-soft); }
.tw-gen-card .tw-gi { font-size: 17px; }
.tw-gen-card b { font-size: 13px; display: block; margin-bottom: 2px; color: var(--text); }
.tw-gen-card span { font-size: 11.5px; color: var(--text-secondary); line-height: 1.5; }

/* ---------- bulk bar ---------- */
.tw-bulkbar {
  position: fixed; bottom: 26px; left: 50%; transform: translateX(-50%); z-index: 800;
  display: flex; align-items: center; gap: 6px; padding: 8px 10px 8px 18px;
  background: var(--tw-ink); color: #fff; border-radius: 999px; box-shadow: 0 16px 40px -8px rgba(29,37,84,.55);
  animation: tw-bup .28s var(--tw-ease); white-space: nowrap;
}
/* Item 718d8988: `hidden` is the ONLY way renderBulk() takes this bar down — it
   sets bar.hidden = true the moment the selection empties, and the shell ships
   the element pre-hidden before anything is selected. But `display: flex` above
   is an AUTHOR declaration and the [hidden]{display:none} it relies on lives in
   the User-Agent stylesheet, which every author rule outranks regardless of
   specificity. So the bar stayed painted at the bottom of the viewport — with
   the stale count it last rendered, since renderBulk() returns before rewriting
   its contents on the empty path — and the ✕ inside it could not close it
   either, because ✕ just clears the selection and re-enters the same hide.
   Restating it here is what makes the attribute mean anything. */
.tw-bulkbar[hidden] { display: none; }
@keyframes tw-bup { from { opacity: 0; transform: translateX(-50%) translateY(16px); } }
.tw-bulkbar .tw-bcount { font-size: 12px; font-weight: 800; margin-right: 8px; }
.tw-bulkbar button { padding: 7px 13px; border-radius: 999px; font-size: 11.5px; font-weight: 700;
  color: rgba(255,255,255,.92); border: 0; background: none; cursor: pointer; font-family: inherit;
  transition: background .12s; }
.tw-bulkbar button:hover { background: rgba(255,255,255,.13); }
.tw-bulkbar .tw-b-run { background: var(--tw-coral); }
.tw-bulkbar .tw-b-run:hover { background: var(--tw-coral-hover); }
.tw-bulkbar .tw-b-danger:hover { background: rgba(193,21,58,.55); }
.tw-bulkbar .tw-b-x { padding: 7px 10px; opacity: .7; }


/* lightbox */
.tw-lightbox { position: fixed; inset: 0; z-index: 990; background: rgba(23,26,63,.82); display: flex;
  align-items: center; justify-content: center; cursor: zoom-out; animation: tw-fade .2s; }
.tw-lightbox img { max-width: 92vw; max-height: 88vh; border-radius: var(--radius); box-shadow: var(--shadow-lg); }

/* ── Export dialog + field-mapping side-car ──────────────────────────────
   The ⋯ menu's six export buttons became one dialog (owner, 2026-08-17).

   Built to the Activate Design & Brand Guidelines rather than to this screen's
   own older visual language — the owner's note was that the modal "doesn't
   match the other screens". Concretely, that meant: pill buttons (999px) and
   700 weights, and the sidebar navy `--tw-ink` used for primary and selected
   states. The guidelines retire all three: 7px control radius, 600 weight, and
   ONE blue (`--accent`) for every action and selected state, with the dark
   navy reserved for the selected sidebar item and nowhere else.

   Single choices are labelled DROPDOWNS, not segmented rows or chips — the
   standing owner rule, and what every other form on the platform does.

   Tokens only, and only tokens that exist on main today (`--accent`,
   `--border`, `--bg-card`, `--text`, `--text-secondary`, `--text-muted`,
   `--radius-sm`, `--shadow-lg`), so this inherits the palette automatically
   when design-review.css lands. Two new local tokens are declared below WITH
   their dark counterparts — an undefined token silently paints its light
   fallback in dark mode, which has shipped as a real bug twice.
   Everything is scoped under .tw-modal-export so it cannot leak into the
   other modals this screen renders. */
#tests-v2-root, .tw-modal-export {
  /* Modal width tiers from the guidelines (560 / 720 / 900). This dialog is
     the middle tier: two columns of fields, no stepper. */
  --twx-modal-md: 720px;
  /* Neutral-button ink. --text is the near-navy body colour; outline buttons
     want the guidelines' quieter --edit-ink, which main does not define yet. */
  --twx-btn-ink: #374151;
  /* Helper text. NOT --text-muted: this text carries meaning (it is the only
     place the dialog explains what a choice does), and the dark theme's
     --text-muted measured 4.33:1 on #181d29 when this fork was written — under
     AA. The global token is now 5.48:1 there, so this fork is redundant and
     should be retired into var(--text-muted). Measured
     with the guidelines' own contrast scan, not eyeballed. */
  --twx-help-ink: #6b7280;
  /* Primary-button fill. NOT --accent: the dark theme lifts it to #5398f6,
     and white ink on that is 2.92:1 — the worst failure on the dialog. This
     is the guidelines' #2563eb, which carries white at 5.17:1 in BOTH themes,
     so the primary action stays legible either way. */
  --twx-primary: #2563eb;
}
[data-theme="dark"] #tests-v2-root, [data-theme="dark"] .tw-modal-export {
  --twx-btn-ink: #cbd2e0;
  --twx-help-ink: #a8adbd;   /* 7.4:1 on the dark card */
  --twx-primary: #2563eb;    /* white ink stays 5.17:1 — do not lift it */
}

.tw-modal.tw-modal-export { width: min(var(--twx-modal-md), 94vw); }

/* Header: title, a 40x40 close, one divider. */
.tw-modal-export .tw-xhead { position: relative; padding-bottom: 16px;
  border-bottom: 1px solid var(--border); margin-bottom: 4px; }
.tw-modal-export .tw-xhead h3 { font-size: 16px; font-weight: 600; }
.tw-modal-export .tw-xhead .tw-m-sub { margin-bottom: 0; }
.tw-xclose { position: absolute; top: -4px; right: 0; width: 40px; height: 40px;
  display: flex; align-items: center; justify-content: center; padding: 0;
  border: 0; border-radius: var(--radius-sm); background: none; cursor: pointer;
  font-family: inherit; font-size: 22px; line-height: 1; color: var(--text-secondary);
  transition: background var(--transition, .2s), color var(--transition, .2s); }
.tw-xclose:hover { background: var(--bg-hover); color: var(--text); }

/* Section eyebrow — 11px/600 micro-caps, 0.4px tracking, never more. */
.tw-modal-export .tw-xsect { font-size: 11px; font-weight: 600; letter-spacing: .4px;
  text-transform: uppercase; color: var(--text-secondary);
  margin: 24px 0 8px; }
.tw-modal-export .tw-xsect:first-child { margin-top: 16px; }

/* Checkbox rows: control + label + helper text on the spacing scale. */
.tw-xcheck { display: flex; gap: 12px; align-items: flex-start; padding: 8px 0;
  cursor: pointer; }
.tw-xcheck input { margin: 1px 0 0; accent-color: var(--accent); flex: 0 0 auto;
  width: 16px; height: 16px; }
.tw-xcheck-t { font-size: 14px; font-weight: 500; color: var(--text); line-height: 1.4; }
.tw-xhelp { display: block; font-size: 12px; font-weight: 400; color: var(--twx-help-ink);
  line-height: 1.45; margin-top: 2px; }
/* The always-on inclusion. Stated in full so the document's contents are
   legible — but visibly not a choice. */
.tw-xcheck.tw-xlocked { cursor: default; }
.tw-xcheck.tw-xlocked .tw-xcheck-t { color: var(--text-secondary); }

/* Two-column field grid. */
.tw-xgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.tw-xfield { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.tw-xlabel { font-size: 12px; font-weight: 600; color: var(--text-secondary); }
/* Controls: 34px min, card surface, EXPLICIT ink (a bare control inherits UA
   black and vanishes in dark mode), accent focus ring. */
.tw-modal-export .tw-xfield select {
  width: 100%; min-height: 34px; padding: 7px 11px; margin: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-card); color: var(--text);
  font-family: inherit; font-size: 13px; box-sizing: border-box; cursor: pointer;
}
.tw-modal-export .tw-xfield select:focus { border-color: var(--accent); outline: none;
  box-shadow: 0 0 0 3px var(--accent-soft); }

/* Buttons: one geometry, 7px radius, 600 weight, no shadows, no pills.
   34px in the body, 44px in the modal footer. */
.tw-xbtn { display: inline-flex; align-items: center; justify-content: center;
  min-height: 34px; padding: 7px 14px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--bg-card); color: var(--twx-btn-ink);
  font-family: inherit; font-size: 13px; font-weight: 600; cursor: pointer;
  white-space: nowrap; transition: background var(--transition, .2s),
  border-color var(--transition, .2s); }
.tw-xbtn:hover { background: var(--bg-hover); }
.tw-xbtn:focus-visible { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft); }
.tw-xbtn-lg { min-height: 44px; padding: 11px 20px; }
/* A button inside a column-flex field would stretch to the cell's full width
   and read as a text input. Buttons size to their label. */
.tw-xfield > .tw-xbtn { align-self: flex-start; }
.tw-xbtn-primary { background: var(--twx-primary); border-color: var(--twx-primary); color: #fff; }
.tw-xbtn-primary:hover { background: var(--twx-primary); filter: brightness(1.1); }
.tw-xbtn-primary:disabled { opacity: .6; cursor: default; filter: none; }

/* Footer: fixed, right-aligned, primary always visible. */
.tw-modal-export .tw-m-foot { gap: 12px; border-top: 1px solid var(--border); }

/* ── the per-step editor in the Create/Edit dialog (item 37a740f4) ──
   One input per step, numbered from its position — the tester never types or
   maintains the numbering. The number is a fixed-width badge and the arrows/✕
   are fixed-width too, so the inputs keep one left edge and the controls stay
   put while the list is being resequenced. */
.tw-step-rows { display: flex; flex-direction: column; gap: 8px; }
/* flex-start, not center (item 8e8fd8c3): a step with four expected results
   makes its row tall, and centring would float the step input and the ✕ in the
   middle of that height, away from the row they belong to. */
.tw-step-row { display: flex; align-items: flex-start; gap: 8px; }
.tw-step-n { flex: 0 0 30px; text-align: center; font-family: var(--tw-mono); font-size: 12px;
  font-weight: 700; color: var(--text-secondary); background: var(--bg-secondary);
  border-radius: var(--radius-sm); padding: 8px 0; }
/* Drag to reorder (item 2de596df). The BADGE is the grab handle, not the row —
   see the comment in tests-v2.js: a draggable row takes the pointer away from
   the step's own text boxes, so the tester could no longer select a word of it.
   user-select stops the gesture painting the digit blue on the way past. */
.tw-step-n { cursor: grab; user-select: none; -webkit-user-select: none; }
.tw-step-n:active { cursor: grabbing; }
/* The dragged row stays where it is and dims. The list deliberately does not
   reflow under the pointer — the drop line is the only thing that moves. */
.tw-step-row.tw-step-dragging { opacity: .45; }
/* The drop line lives in the 8px gap between rows, drawn as an OUTER box-shadow
   rather than a border so that showing it shifts nothing. */
.tw-step-row.tw-step-drop-before { box-shadow: 0 -2px 0 var(--accent); }
.tw-step-row.tw-step-drop-after { box-shadow: 0 2px 0 var(--accent); }
/* Overrides the dialog's full-width field rule above — a step input is one cell
   in a row, not a block. `flex-basis: 0` rather than auto so the step column and
   the expected column divide the row evenly instead of by their content. */
/* A textarea rather than an input since item f37aade7, so a long step is
   readable while it is being edited. `resize: vertical` is inherited from the
   dialog rule and `height` is set per box from its scrollHeight.
   `overflow: auto`, NOT hidden: the sizer clamps at a share of the viewport, so
   a very long value really does have more text than the box shows, and hiding
   the scrollbar would leave it unreachable with no affordance — the reported
   symptom, reintroduced. Below the clamp the box is exactly as tall as its text
   and no scrollbar appears. */
.tw-modal .tw-step-row textarea[data-si] {
  flex: 1 1 0; width: auto; min-width: 0; margin: 0; min-height: 34px; padding: 7px 11px;
  background: var(--bg-card); font-size: 13px; line-height: 1.45; overflow: auto; }
/* The expected-result column (item 8e8fd8c3) — same basis as the step input, and
   its own vertical stack because a step may expect several things. */
/* Slightly wider basis than the step input, not equal: this column also has
   to hold the per-result delete button, so at an equal basis the TEXT boxes
   come out visibly uneven (measured in Chromium: 229px of step against 166px
   of expected). 1.2 lands the two inputs within a couple of pixels. */
.tw-step-exp { flex: 1.2 1 0; min-width: 0; display: flex; flex-direction: column; gap: 5px; }
/* flex-start, not center: an expected result that wraps to three lines would
   otherwise float its ✕ in the middle of them. */
.tw-step-exp-row { display: flex; align-items: flex-start; gap: 5px; }
/* overflow:auto for the same reason as the step box above — a value held at the
   clamp has to stay reachable. */
.tw-modal .tw-step-exp-row textarea[data-ei] {
  flex: 1 1 0; width: auto; min-width: 0; margin: 0; min-height: 34px; padding: 7px 11px;
  background: var(--bg-card); font-family: var(--tw-mono); font-size: 11.5px;
  line-height: 1.45; overflow: auto; }
.tw-step-exp-add { align-self: flex-start; background: none; border: none; cursor: pointer;
  padding: 2px 4px; font-size: 11px; color: var(--text-secondary); }
.tw-step-exp-add:hover { color: var(--accent); text-decoration: underline; }
.tw-step-ops { display: flex; align-items: center; gap: 5px; }
/* The column headings above the rows — without them the second box is just an
   unexplained input, and its placeholder is gone the moment it is typed in. The
   38px indent clears the step-number badge and its gap. */
.tw-step-head { display: flex; gap: 8px; padding: 0 0 5px 38px; font-size: 10px;
  font-weight: 700; color: var(--text-secondary); text-transform: uppercase;
  letter-spacing: .5px; }
.tw-step-head span { flex: 1 1 0; min-width: 0; }
.tw-step-foot { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.tw-step-foot span { font-size: 11px; color: var(--text-secondary); line-height: 1.5; }
.tw-step-pair { font-size: 11px; color: var(--text-secondary); line-height: 1.5; margin: 6px 0 0; }
/* Carries the expand panel's refusals ("this JSON cannot be put back on one
   line"). It used to also carry the steps/expected pairing warning, which item
   8e8fd8c3 removed along with the flat list the pairing was against. */
.tw-step-warn { font-size: 11px; line-height: 1.5; margin: 8px 0 0; padding: 7px 10px;
  border-radius: var(--radius-sm); background: rgba(250,169,56,.1);
  border: 1px solid rgba(250,169,56,.45); color: #8a5200; }

/* One expected value lifted out of the one-per-line list for editing (item
   c597c259). It sits inline under the box rather than in a second dialog: the
   list behind it stays readable, and the value's line index — what Apply writes
   back through — cannot be invalidated by a stacked modal's own close path. */
.tw-exp-zoom { margin-top: 8px; padding: 10px 12px; background: var(--bg-secondary);
  border: 1px solid var(--border); border-radius: var(--radius); }
.tw-exp-zoom-label { font-size: 10px; font-weight: 600; color: var(--text-secondary);
  text-transform: uppercase; letter-spacing: .5px; margin-bottom: 6px; }
/* pre + normal wrapping: a laid-out payload keeps its indentation and scrolls
   sideways instead of re-wrapping, so what the user edits is what gets stored. */
.tw-modal .tw-exp-zoom textarea { width: 100%; margin: 0; font-family: var(--tw-mono);
  font-size: 11.5px; white-space: pre; overflow-wrap: normal; resize: vertical; }
.tw-exp-zoom-foot { display: flex; gap: 8px; justify-content: flex-end; margin-top: 8px; }

/* ── the field-mapping side-car ── */
/* The body's first child sits directly under the header's divider, so it owns
   the gap — without this the first column row reads as part of the header. */
.tw-map-rows { display: flex; flex-direction: column; gap: 8px; margin-top: 16px; }
.tw-map-row { display: flex; align-items: center; gap: 8px; }
.tw-map-key { flex: 0 0 140px; font-size: 12px; font-weight: 600;
  color: var(--text-secondary); background: var(--bg-secondary);
  border-radius: var(--radius-sm); padding: 7px 10px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The label input carries the header text, so it takes the slack — the arrows
   and ✕ must stay put as rows are added, or the list jitters while the user is
   resequencing it. */
.tw-modal-export .tw-map-row input[type="text"] {
  flex: 1 1 auto; min-width: 0; margin: 0; min-height: 34px; padding: 7px 11px;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-card); color: var(--text);
  font-family: inherit; font-size: 13px; box-sizing: border-box; }
.tw-modal-export .tw-map-row input[type="text"]:focus { border-color: var(--accent);
  outline: none; box-shadow: 0 0 0 3px var(--accent-soft); }
/* Icon buttons: 34px square in a dense row (the guidelines' row tier). */
.tw-xicon { flex: 0 0 auto; width: 34px; height: 34px; display: flex;
  align-items: center; justify-content: center; padding: 0;
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-card); color: var(--twx-btn-ink);
  font-family: inherit; font-size: 13px; cursor: pointer;
  transition: background var(--transition, .2s); }
.tw-xicon:hover:not(:disabled) { background: var(--bg-hover); }
.tw-xicon:disabled { opacity: .4; cursor: default; }
.tw-xicon:focus-visible { outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft); }
.tw-xicon-rm { color: var(--tw-fail, #c1153a); }
.tw-map-add { display: flex; align-items: center; gap: 8px; margin-top: 16px;
  padding-top: 16px; border-top: 1px solid var(--border-light); }
.tw-modal-export .tw-map-add select { margin: 0; max-width: 240px; min-height: 34px;
  padding: 7px 11px; border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--bg-card); color: var(--text); font-family: inherit; font-size: 13px; }

/* The Co-Pilot's captured report on the Runs tab. Preformatted: the agent
   writes a numbered narrative, and reflowing it destroys the structure that
   makes it readable. */
.tw-run-report { margin-top: 12px; border: 1px solid var(--border);
  border-radius: var(--radius-sm); background: var(--bg-secondary); overflow: hidden; }
.tw-run-report > summary { cursor: pointer; padding: 8px 12px; font-size: 13px;
  font-weight: 600; color: var(--text); list-style: none; user-select: none; }
.tw-run-report > summary::-webkit-details-marker { display: none; }
.tw-run-report > summary::before { content: '▸ '; color: var(--text-secondary); }
.tw-run-report[open] > summary::before { content: '▾ '; }
.tw-run-report > summary:hover { background: var(--bg-hover); }
.tw-run-report pre { margin: 0; padding: 0 14px 13px; font-size: 12px; line-height: 1.55;
  white-space: pre-wrap; word-break: break-word; color: var(--text);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  max-height: 460px; overflow-y: auto; }

/* responsive */
@media (max-width: 1100px) {
  .tw-split { flex-direction: column; }
  .tw-list-pane { flex: 1; min-width: 0; }
  .tw-inspector { display: none; }
  .tw-spine { display: none; }
}
@media (max-width: 620px) {
  /* The two-column field grid collapses before the labels start truncating,
     and the mapper's field-name chip gives up width first — the header-label
     input is the one control that must stay usable. */
  .tw-xgrid { grid-template-columns: 1fr; }
  .tw-map-key { flex-basis: 104px; }
}

/* ---------- background-run bar ----------
   Sits at the top of the Tests screen whenever the harness is holding a run for
   this environment. It is deliberately in the document flow rather than a
   floating toast: a background batch can run for many minutes, and the whole
   complaint it answers was that after the first toast there was nothing left on
   screen to look at. */
.tw-runbar {
  display: flex; flex-direction: column; gap: 8px;
  margin: 0 0 12px; padding: 0;
}
.tw-runbar[hidden] { display: none; }

/* The row is a COLUMN: a header line (dot / detail / actions) and, only when
   opened, the details panel underneath at full width. It used to be a single
   flex row with the Details button stacked inside the text column, which cost a
   whole line of height on every row for a control that belongs next to Cancel
   — reported from the Daikin instance. */
.tw-runrow {
  display: flex; flex-direction: column;
  padding: 10px 12px 10px 14px;
  background: var(--bg-card); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.tw-runrow > .tw-run-dot,
.tw-runrow > .tw-run-main,
.tw-runrow > .tw-run-actions { align-self: auto; }
/* The header line itself. `contents` would flatten the column, so the three
   pieces are laid out with a grid instead: dot, the text that flexes, actions
   that never shrink. */
.tw-runrow {
  display: grid;
  grid-template-columns: auto 1fr auto;
  grid-template-areas: "dot main actions" "detail detail detail";
  align-items: center; gap: 4px 12px;
}
.tw-runrow > .tw-run-dot { grid-area: dot; }
.tw-runrow > .tw-run-main { grid-area: main; }
.tw-runrow > .tw-run-actions { grid-area: actions; }
.tw-runrow > .tw-run-detail { grid-area: detail; }

/* Details sits WITH Cancel, so the pair reads as one control group and the row
   keeps its height whether or not the run is expandable. */
.tw-run-actions {
  display: flex; align-items: center; gap: 8px; flex: 0 0 auto;
}
.tw-run-dot {
  width: 8px; height: 8px; border-radius: 50%; flex: 0 0 8px;
  background: var(--tw-inprog);
}
/* A live run's dot breathes; a finished one holds still and takes the colour of
   its outcome, so the state is legible without reading a word. */
.tw-run-queued .tw-run-dot,
.tw-run-running .tw-run-dot { animation: tw-run-pulse 1.5s var(--tw-ease) infinite; }
/* Colour keys on the OUTCOME, never the lifecycle status. `status: done` only
   says the harness reached the end; a run whose every test failed is `done` too,
   and painting that green is what made a failing run read as a pass. */
.tw-out-passed .tw-run-dot { background: var(--pass); }
.tw-out-failed .tw-run-dot,
.tw-out-error .tw-run-dot { background: var(--fail); }
/* Finished having executed nothing is not a pass and not a failure — it is a
   run that did not happen, and it gets its own colour so it cannot hide as
   either. */
.tw-out-none-ran .tw-run-dot { background: var(--partial, #d97706); }
/* Could not be run here. Not a pass, and pointedly NOT a failure — nothing about
   it is a statement about the customer's system. */
.tw-out-not-runnable .tw-run-dot { background: var(--partial, #d97706); }
.tw-out-cancelled .tw-run-dot { background: var(--text-muted); }
@keyframes tw-run-pulse { 50% { opacity: .3; } }
@media (prefers-reduced-motion: reduce) {
  .tw-run-queued .tw-run-dot, .tw-run-running .tw-run-dot { animation: none; }
}

.tw-run-main { flex: 1 1 auto; min-width: 0; }
.tw-run-top { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.tw-run-top b { font-size: 12.5px; color: var(--text); }
.tw-run-phase { font-size: 11.5px; color: var(--text-secondary); }

.tw-run-meter {
  height: 4px; margin: 6px 0 5px; border-radius: 999px;
  background: var(--bg-secondary); overflow: hidden;
}
.tw-run-meter i {
  display: block; height: 100%; border-radius: 999px;
  background: var(--accent); transition: width .3s var(--tw-ease);
}
.tw-out-passed .tw-run-meter i { background: var(--pass); }
.tw-out-failed .tw-run-meter i,
.tw-out-error .tw-run-meter i { background: var(--fail); }
.tw-out-none-ran .tw-run-meter i { background: var(--partial, #d97706); }
.tw-out-not-runnable .tw-run-meter i { background: var(--partial, #d97706); }
.tw-out-cancelled .tw-run-meter i { background: var(--text-muted); }
/* A finished run with nothing to show still needs a visible bar, or "nothing
   ran" renders as an empty track that looks like it never started. */
.tw-out-none-ran .tw-run-meter i,
.tw-out-not-runnable .tw-run-meter i,
.tw-out-error .tw-run-meter i { width: 100% !important; opacity: .35; }

/* Whose run this is. Only ever rendered for somebody ELSE's run — the label
   exists to break the tie between two identical-looking rows, so putting one on
   every row would defeat it. */
.tw-run-who {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px; color: var(--text-secondary);
  background: var(--bg-secondary); border: 1px solid var(--border-light);
  padding: 1px 6px; border-radius: 999px; white-space: nowrap;
}
/* A run you did not start reads as somebody else's territory at a glance. */
.tw-runrow.tw-run-theirs { border-left: 3px solid var(--border-light); }

.tw-run-sub { font-size: 11px; color: var(--text-muted); }
.tw-run-err { color: var(--fail); }

.tw-run-cancel, .tw-run-dismiss,
.tw-suite-cancel, .tw-suite-dismiss { flex: 0 0 auto; font-size: 11.5px; }
.tw-run-cancel[disabled], .tw-suite-cancel[disabled] { opacity: .55; cursor: default; }

/* Durable suite runs reuse the whole row language above on purpose — the dot
   pulse keys on tw-run-queued/running and the colours on tw-out-*, so a suite
   row inherits the same honesty rules with no second set of selectors to
   drift. The only suite-specific need: its counts line ("9 pass · 3 fail · 2
   not runnable · 10 queued · 2 running · now: …") is long, so let it wrap
   instead of pushing the Cancel button off the row. */
.tw-suiterow .tw-run-sub { overflow-wrap: anywhere; }

/* A refused dispatch carries the server's full remedy — several sentences, not
   a status word — so it wraps for the same reason the suite counts do, and
   keeps its own line-height because it is meant to be READ, not glanced at.
   Sized above .tw-run-sub's 11px: this row is the only copy of the reason that
   exists anywhere in the product (item 8581d8c7). */
.tw-dispatch-err .tw-run-sub {
  overflow-wrap: anywhere;
  font-size: 12px; line-height: 1.5; margin-top: 2px;
  white-space: pre-line;
}

/* ---------- run bar: stage, reason and the details expander ---------- */
.tw-run-stage {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 10.5px; color: var(--text-secondary);
}
.tw-run-more {
  padding: 0; border: 0; background: none; cursor: pointer; white-space: nowrap;
  font-family: inherit; font-size: 11px; font-weight: 600; color: var(--accent);
}
.tw-run-more:hover { text-decoration: underline; }
.tw-run-detail { margin-top: 8px; }
.tw-run-detail[hidden] { display: none; }

/* One row per test — verdict beside the script id — with that test's
   transcript folded underneath it, closed until asked for. The panel used to
   be two lists (verdicts, then transcripts): the word you look for was never
   beside the script you look at, and three transcripts open at full height
   pushed the run bar itself off the screen. */
.tw-run-results { display: flex; flex-direction: column; gap: 3px; margin-bottom: 8px; }
/* A GRID, not a flex row: caret, verdict, script id, reason and line count each
   own a column, so the ids line up down the panel however long the verdict word
   or the reason is. Every cell is always emitted (empty when it has nothing to
   say) — with a grid, a skipped element silently promotes the next one into the
   wrong column. */
.tw-run-res {
  display: grid; grid-template-columns: 11px 72px auto minmax(0, 1fr) auto;
  align-items: baseline; gap: 8px; width: 100%; text-align: left;
  font-family: inherit; font-size: 11.5px; color: inherit; cursor: pointer;
  padding: 5px 8px; border: 0; border-radius: var(--radius-sm); background: var(--bg-secondary);
}
.tw-run-res:hover { background: var(--bg-hover, var(--bg-secondary)); }
.tw-run-res b { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.tw-run-res-caret {
  font-size: 10px; color: var(--text-secondary);
  transition: transform .12s ease; transform-origin: 45% 55%;
}
.tw-run-res.open .tw-run-res-caret { transform: rotate(90deg); }
.tw-run-res-note {
  color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tw-run-res-lines { color: var(--text-secondary); font-size: 10.5px; white-space: nowrap; }
.tw-run-res-v {
  font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  padding: 1px 5px; border-radius: 3px; text-align: center;
}
/* The run's own harness lines carry no verdict — the cell holds the column open
   so their label starts where every test's does. */
.tw-run-res.plain .tw-run-res-v { background: none; }
/* ...and its label is prose, not a script id, so it is not set in mono. */
.tw-run-res.plain b { font-family: inherit; font-weight: 700; }
.tw-run-res.ok .tw-run-res-v { background: var(--pass); color: #fff; }
.tw-run-res.bad .tw-run-res-v { background: var(--fail); color: #fff; }
.tw-run-res.warn .tw-run-res-v { background: var(--partial, #d97706); color: #fff; }
/* Still running: a neutral chip, never a colour that reads as a verdict. */
.tw-run-res.live .tw-run-res-v { background: var(--text-secondary); color: #fff; }

.tw-run-log {
  margin: 3px 0 0; padding: 10px 12px; max-height: 260px; overflow: auto;
  background: var(--bg-secondary); border: 1px solid var(--border-light);
  border-radius: var(--radius-sm);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 11px; line-height: 1.55; color: var(--text-secondary);
  white-space: pre-wrap; word-break: break-word;
}
.tw-run-log[hidden] { display: none; }

.tw-run-warn { color: var(--partial, #d97706); }

.tw-run-log-test { display: flex; flex-direction: column; }

/* ── Script tab: Data + Execution log sections (FR-ea027678) ── */

.tw-execlog {
  max-height: 380px; overflow: auto; display: flex; flex-direction: column; gap: 8px;
  padding: 12px; background: var(--bg-secondary);
  border: 1px solid var(--border-light); border-radius: var(--radius-sm);
}
.tw-el-msg { max-width: 92%; }
.tw-el-msg.ai { align-self: flex-start; }
.tw-el-msg.me { align-self: flex-end; }
.tw-el-msg.sys { align-self: center; opacity: .8; }
.tw-el-head { display: flex; gap: 8px; align-items: baseline; margin-bottom: 2px; }
.tw-el-msg.me .tw-el-head { flex-direction: row-reverse; }
.tw-el-who {
  font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
  color: var(--tw-ink);
}
.tw-el-msg.me .tw-el-who { color: var(--text-secondary); }
.tw-el-when { font-size: 9.5px; color: var(--text-muted); font-family: var(--tw-mono); }
.tw-el-text {
  font-size: 12px; line-height: 1.55; color: var(--text);
  background: var(--bg-card); border: 1px solid var(--border-light);
  border-radius: 10px; padding: 7px 10px;
  white-space: pre-wrap; word-break: break-word;
}
.tw-el-msg.me .tw-el-text { background: var(--bg-secondary); }
.tw-el-foot { margin-top: 6px; font-size: 10.5px; color: var(--text-muted); }
