/* ============================================================
   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; }
/* The pane and the inspector split the workbench evenly (item f930eb1f,
   wayfair, clawson@veridian.info): "the left-hand master table consumes well
   over half the viewport width... the detail panel is squeezed into roughly
   the right 40%". That happened with every column shown AND with the column
   picker untouched — the column-aware floor below (also item f930eb1f) only
   helps once a user opens the picker, so it cannot be the whole fix: on any
   viewport under ~1930px wide the pane's min-width, not its 46% flex-basis,
   was what actually sized it. max-width caps each side at 50% regardless of
   what the row content needs; the row's own min-width requirement (the calc()
   below) moves to the .tw-list-inner wrapper instead, so a pane narrower than
   that scrolls its rows horizontally rather than either blanking the title or
   shoving the inspector past its cap. */
.tw-list-pane {
  --tw-c-id: 102px; --tw-c-flags: 58px; --tw-c-assignee: 110px; --tw-c-lastrun: 76px;
  --tw-c-lastrunby: 100px; --tw-c-prefmode: 102px; --tw-c-result: 76px;
  flex: 1 1 50%; max-width: 50%; min-width: 0; overflow: auto; border-radius: var(--radius-lg);
  background: var(--bg-card); border: 1px solid var(--border-light); box-shadow: var(--shadow);
  scrollbar-width: thin; scroll-padding-top: var(--tw-lhead-h, 27px);
}
/* min-width has to cover the row's FIXED columns plus a readable title, and so
   must be recomputed whenever one of those widths changes (item 192317b9). It
   is summed here rather than stated as one literal (item f930eb1f) because the
   number is not a constant: the picker can hide seven of the eight fixed
   cells, and a single literal keeps charging for columns that are no longer
   drawn. --tw-c-* is one term per HIDEABLE column — its own width plus the
   10px row gap it takes with it — declared above on .tw-list-pane (where the
   picker's hide rules below can zero the matching term) and read here, where
   they inherit down for free: a CSS custom property cascades to descendants
   unless an element overrides it, and this wrapper overrides none of them.
   Every term is pinned to its column's declared width by
   test_tests_v2_columns.js; they cannot drift.

   The base 136px is what no picker state can remove: 14 checkbox + its 10px
   gap + the row's 42px side padding + 2px border + 68px of title. Left too low
   the title cell computes to ZERO and the row overflows — blanking the one
   column the picker deliberately refuses to hide, by geometry rather than by
   choice. With all eight cells shown the sum is 760px, exactly the literal it
   replaces (widened from 660px when Preferred Mode added an 8th fixed column);
   hiding Last Run, Last Run By and Preferred Mode now returns 278px to the
   inspector instead of spending all of it on the title.

   Living on the INNER wrapper rather than the pane (item f930eb1f) means a
   pane capped below this sum by the 50% split above scrolls the wrapper
   sideways instead of shrinking it — the title still never blanks, and on any
   split wide enough to give the pane more room than the sum needs, the
   wrapper (width:100%) stretches with it, so the title gets that extra room
   BEFORE anything truncates.

   scroll-padding-top stops scrollIntoView() — the ArrowUp/ArrowDown row walk —
   parking the row it just selected underneath the sticky column header. */
.tw-list-inner {
  min-width: calc(136px + var(--tw-c-id) + var(--tw-c-flags) + var(--tw-c-assignee)
    + var(--tw-c-lastrun) + var(--tw-c-lastrunby) + var(--tw-c-prefmode) + var(--tw-c-result));
}
.tw-inspector {
  /* 320px is a floor, not a target: enough for the detail head, tab strip and
     a Preconditions paragraph to wrap without turning every line into its own
     scroll of the viewport (the reported "...line ItemId" clip). Below it the
     pane would rather force .tw-split to overflow sideways than crush the
     inspector to something unreadable. */
  flex: 1 1 50%; max-width: 50%; min-width: 320px; 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 */
/* `top` is the column header's height, not 0 (item 192317b9): both are sticky
   inside the same scroller, so parking this at 0 would slide it UNDER the
   column header and the two would occupy the same strip. See .tw-lhead.
   The literal fallback is not decoration: an undefined custom property makes
   the whole declaration invalid, which silently degrades to `top: auto` — i.e.
   these headers would simply stop sticking, with nothing to see in the
   stylesheet. The suite pins the fallback to the declared height. */
.tw-area-head {
  position: sticky; top: var(--tw-lhead-h, 27px); 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; }
/* The age, promoted out of the glyph cluster into a column of its own (item
   192317b9). Fixed like .tw-rid and .tw-rstatus and for the same reason given
   there: rel() emits anything from "5m ago" to "Mar 3", and a content width
   would make this column's edges — and so the title cell's — ragged down the
   list.
   66px is set by the HEADER, not by the data: the widest string rel() can
   produce ("59m ago") measures 42px, but "LAST RUN" measures 57.3px in
   Montserrat and 58.4px in the system-ui fallback, so the 58px this first
   shipped at rendered "LAST R…" — a truncated header being precisely the thing
   the column labels exist to stop. Matching .tw-rstatus's 66px leaves ~8px of
   slack, which is what covers the font actually resolving to a fallback.
   `cursor: help` is inherited from nothing here — the cell carries a
   `title="Last run <exact timestamp>"`, and .tw-rglyph used to supply the
   affordance for it while the age lived in the glyph cluster. Without it the
   age reads as part of the row's click target.
   text-overflow matters because rel() is not guaranteed to return a short
   string: it falls back to returning its ARGUMENT verbatim when the timestamp
   will not parse, and a raw timestamp clipped mid-character with no ellipsis
   reads as a corrupted value rather than a truncated one. */
.tw-row .tw-rlast {
  width: 66px; flex-shrink: 0; text-align: right; cursor: help;
  font-family: var(--tw-mono); font-size: 9.5px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Who dispatched the latest run (item 4452e299) — "Triggered by:" on the Runs
   tab, now its own list column. 90px is set by the HEADER, the same way
   .tw-rlast's 66px is: "LAST RUN BY" is the widest string this cell's header
   ever shows, and the value itself (an email or a display name) is routinely
   longer than that and is exactly what overflow:ellipsis + the title tooltip
   below exist to shorten without hiding. NOT monospace like .tw-rlast — the
   value is a name/address, not a formatted timestamp, so the row's own font
   reads more naturally here. */
.tw-row .tw-rlastby {
  width: 90px; flex-shrink: 0; text-align: right; cursor: help;
  font-size: 9.5px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Preferred Mode (item cf6659a6): the run mode a test is expected to run in
   most often, and what the Run dialog preselects. 92px matches .tw-rid — the
   three values it ever holds (Autonomous/Guided/Manual) are shorter than that,
   so the width here is set by the HEADER text the same way its siblings are,
   and overflow:ellipsis + the title tooltip cover a fallback font that
   renders it wider. Not monospace — this is a word, not a formatted value. */
.tw-row .tw-rprefmode {
  width: 92px; flex-shrink: 0; text-align: right; cursor: help;
  font-size: 9.5px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Who is supposed to work on this test (item 960bf92d) — "it needs to be
   clear who is supposed to work on what" on a multi-tester suite. 100px by
   the same header-driven reasoning as .tw-rlastby above: "ASSIGNED TO"
   measures wider than "LAST RUN BY" in this font, and a name/email value is
   routinely longer still, which is exactly what the ellipsis + title tooltip
   exist to shorten without hiding. */
.tw-row .tw-rassignee {
  width: 100px; flex-shrink: 0; text-align: right; cursor: help;
  font-size: 9.5px; color: var(--text-muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tw-row .tw-rassignee-empty { opacity: .5; cursor: default; }

/* ---------- list column header (item 192317b9) ---------- */
/* Reported by clawson@veridian.info: the panel showed id / title / status
   icons / age / Pass-Fail with nothing saying which was which, and no way to
   drop the ones you did not want.
 *
 * The header's geometry is COPIED from .tw-row above, never approximated —
 * same display, same gap, same vertical rhythm — and each cell below repeats
 * its row cell's width declaration. tests/test_tests_v2_columns.js reads both
 * blocks back out of this file and fails if the two ever drift apart, which is
 * the only failure mode a column header really has.
 *
 * --tw-glyphs-w is shared by the flag cluster and its header so there is one
 * number, not two that must be kept equal by hand. It is a min-width on the
 * rows: a rare row carrying five or six glyphs grows past it rather than
 * clipping one (a missing bug or needs-attention glyph is lost information),
 * and because .tw-rlast and .tw-rstatus after it are FIXED, that growth is
 * absorbed by the flexible title — every column to the right stays aligned,
 * and the cluster stays right-aligned against the same edge as its header.
 * Its 48px is set by that header too: "STATUS" measures 44px, so 48 leaves the
 * same slack for a fallback font as .tw-rlast gets, while still holding the
 * three glyphs a row usually carries. */
.tw-list-pane { --tw-lhead-h: 27px; --tw-glyphs-w: 48px; }
.tw-lhead {
  position: sticky; top: 0; z-index: 4;
  display: flex; align-items: center; gap: 10px;
  /* 7 + 12 + 7 + 1px border = the 27px --tw-lhead-h that .tw-area-head sticks
     below. line-height is stated rather than inherited so that sum is a fact
     about this file and not about the reader's font. */
  padding: 7px 16px 7px 26px; line-height: 12px;
  background: var(--bg-card); border-bottom: 1px solid var(--border);
  font-size: 9.5px; font-weight: 800; letter-spacing: .07em; text-transform: uppercase;
  /* --text-secondary, not --text-muted: these labels are the whole point of the
     feature, and --text-muted (#94a3b8) is 2.56:1 on the card — a decorative
     floor. --text-secondary clears AA at this size. */
  color: var(--text-secondary); user-select: none;
}
/* The header's indent is written in the SAME DIRECTION as the row's, through
   the SAME :has() test, and that is load-bearing rather than tidy. .tw-row
   states 26px as the default and takes it away again for rows that draw no
   reorder cluster, precisely so a browser without :has() over-indents instead
   of painting controls over a checkbox (see the note above .tw-row). Written
   the other way round here — 12px by default, 26px added back via a class the
   JS emits — that same browser would keep every row at 26px while the header
   stayed at 12px, and every label would sit 14px left of its column on exactly
   the flat sorts this feature exists to label. Keying off the pane means the
   header cannot disagree with the rows it sits above: they are the rows whose
   presence the test is asking about. */
.tw-list-pane:not(:has(.tw-rmove)) .tw-lhead { padding-left: 12px; }
/* The row checkbox is a 14px flex item that never shrinks, so the header
   reserves the identical lane — otherwise every column below it is 24px out. */
.tw-lhead .tw-lh-cb { width: 14px; flex-shrink: 0; }
.tw-lh { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tw-lh-id { width: 92px; flex-shrink: 0; }
.tw-lh-title { flex: 1; min-width: 0; }
.tw-lh-flags { width: var(--tw-glyphs-w); flex-shrink: 0; text-align: right; }
.tw-lh-assignee { width: 100px; flex-shrink: 0; text-align: right; }
.tw-lh-lastrun { width: 66px; flex-shrink: 0; text-align: right; }
.tw-lh-lastrunby { width: 90px; flex-shrink: 0; text-align: right; }
.tw-lh-prefmode { width: 92px; flex-shrink: 0; text-align: right; }
.tw-lh-result { width: 66px; flex-shrink: 0; text-align: right; }
.tw-row .tw-rglyphs { min-width: var(--tw-glyphs-w); justify-content: flex-end; }

/* Hiding a column. One rule takes out the header cell and the row cells
   together, so the header cannot be showing a column the rows are not (or the
   reverse) — that is the whole reason visibility is a class on the pane
   instead of a branch inside rowHtml(). There is deliberately no
   `.tw-hide-title` rule: the title is pinned in COLUMNS, and leaving the rule
   out means even a hand-edited localStorage value cannot blank the column that
   says which test a row is. */
.tw-list-pane.tw-hide-id .tw-rid,      .tw-list-pane.tw-hide-id .tw-lh-id,
.tw-list-pane.tw-hide-flags .tw-rglyphs, .tw-list-pane.tw-hide-flags .tw-lh-flags,
.tw-list-pane.tw-hide-assignee .tw-rassignee, .tw-list-pane.tw-hide-assignee .tw-lh-assignee,
.tw-list-pane.tw-hide-lastrun .tw-rlast, .tw-list-pane.tw-hide-lastrun .tw-lh-lastrun,
.tw-list-pane.tw-hide-lastrunby .tw-rlastby, .tw-list-pane.tw-hide-lastrunby .tw-lh-lastrunby,
.tw-list-pane.tw-hide-prefmode .tw-rprefmode, .tw-list-pane.tw-hide-prefmode .tw-lh-prefmode,
.tw-list-pane.tw-hide-result .tw-rstatus, .tw-list-pane.tw-hide-result .tw-lh-result {
  display: none;
}
/* ...and stop reserving the width it took, so the pane's min-width above falls
   with it (item f930eb1f). Kept as a SEPARATE rule rather than folded into the
   display:none block: these set a property on the pane itself, while the block
   above sets one on its cells, and one selector list cannot do both. The pairing
   is machine-checked — every hideable column needs both halves, or the picker
   would take a column away and go on charging the inspector for it. */
.tw-list-pane.tw-hide-id { --tw-c-id: 0px; }
.tw-list-pane.tw-hide-flags { --tw-c-flags: 0px; }
.tw-list-pane.tw-hide-assignee { --tw-c-assignee: 0px; }
.tw-list-pane.tw-hide-lastrun { --tw-c-lastrun: 0px; }
.tw-list-pane.tw-hide-lastrunby { --tw-c-lastrunby: 0px; }
.tw-list-pane.tw-hide-prefmode { --tw-c-prefmode: 0px; }
.tw-list-pane.tw-hide-result { --tw-c-result: 0px; }

/* 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); }
/* Who DISPATCHED the run (item ac9ea9aa). Deliberately NOT .tw-mode, which
   uppercases and letter-spaces: this field renders an ACCOUNT ADDRESS, and
   case-folding it makes the string stop matching the account as it is stored and
   as it is shown everywhere else in the product — a reader copying it out, or
   checking it against the Users list, would be comparing a different string.
   Mono at 11px to match .tw-when, the row's other verbatim-data field. */
.tw-run-head .tw-runby { font-size: 11px; color: var(--text-muted); font-family: var(--tw-mono); }
.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; }
/* Item 372229cb — why the note above stops mid-word. Sits OUTSIDE .tw-run-notes
   on purpose: that box is clamped to 120px and fades its own tail out, so a
   marker placed inside it would be hidden by exactly the collapse the reader
   has to get past to see the cut in the first place. */
.tw-run-cut { font-size: 11px; color: var(--text-secondary); font-style: italic;
  line-height: 1.5; margin-top: 6px; padding-left: 10px;
  border-left: 2px solid var(--border); }
.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; }
/* The column picker (item 192317b9) reuses .tw-f-row for its checkbox rows, so
   it only needs its own width — five short labels want nothing like the filter
   popover's 330px. */
.tw-colpop { width: 244px; max-width: 244px; }
/* A pinned column's row is shown checked but inert. :disabled has to be said
   here because .tw-f-row sets its own cursor and hover. */
.tw-colpop .tw-f-row:disabled { cursor: default; }
.tw-colpop .tw-f-row:disabled:hover { background: none; }
.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; }
  /* max-width: none — the 50% split cap above exists to share width WITH the
     inspector; stacked, there is no inspector beside it to share with, and
     leaving the cap in place would strand the pane at half a full-width
     column for no reason. */
  .tw-list-pane { flex: 1; min-width: 0; max-width: none; }
  .tw-inspector { display: none; }
  .tw-spine { display: none; }
}
/* Below this the stacked pane can no longer hold every fixed column and a
   readable title (item 192317b9): the pane drops its min-width above, so at
   phone widths the title would be squeezed to nothing exactly as it was at the
   old 360px. Give up the least load-bearing columns instead of the most —
   Last Run, Last Run By (item 4452e299) and Preferred Mode (item cf6659a6)
   are the three a row can lose and still be identifiable. Header cell and row
   cells go together in ONE rule, the same pairing the picker's own hide rules
   use, so the header cannot be left labelling a column that is gone. */
@media (max-width: 560px) {
  .tw-list-pane .tw-rlast, .tw-list-pane .tw-lh-lastrun,
  .tw-list-pane .tw-rlastby, .tw-list-pane .tw-lh-lastrunby,
  .tw-list-pane .tw-rprefmode, .tw-list-pane .tw-lh-prefmode { 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); }

/* A label the renderer already shortened with UI.clip (item 372229cb). The
   character budget bounds the string; this bounds the BOX, for the pane widths
   where even the clipped text does not fit — and it ends in an ellipsis rather
   than a hard edge, so the reader is told either way. `max-width: 100%` keeps
   it from widening the row it sits in. The full text is on the element's own
   title=, so nothing here hides anything that is not still reachable. */
.tw-clip {
  display: inline-block; vertical-align: bottom; max-width: 100%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

.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); }

/* ── VPT plan inside the Run dialog ─────────────────────────────────────────
   One line per script: how many users run it, and the User Pool they log in
   from. The total line under it states the batch — or the reason it cannot
   start yet — in words; the Run button follows that line, never the reverse. */
.tw-vpt-plan { border: 1.5px solid var(--border); border-radius: var(--radius); padding: 6px 11px 9px; margin-bottom: 5px; }
.tw-vpt-row { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(120px, 220px); gap: 10px;
  align-items: center; padding: 6px 0; border-bottom: 1px solid var(--border-light); }
.tw-vpt-row:last-of-type { border-bottom: 0; }
.tw-vpt-name { font-size: 12.5px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tw-vpt-name b { font-family: var(--tw-mono); font-size: 11.5px; margin-right: 7px; color: var(--tw-ink); }
.tw-vpt-n { display: inline-flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--text-secondary); white-space: nowrap; }
.tw-vpt-n input { width: 66px; padding: 4px 7px; border: 1px solid var(--border); border-radius: 6px;
  font: inherit; font-size: 12.5px; color: var(--text); background: var(--bg-card); }
.tw-vpt-pool { min-width: 0; padding: 4px 7px; border: 1px solid var(--border); border-radius: 6px;
  font: inherit; font-size: 12px; color: var(--text); background: var(--bg-card); }
.tw-vpt-total { font-size: 11.5px; color: var(--text-secondary); margin-top: 8px; line-height: 1.4; }
.tw-vpt-total.tw-vpt-bad { color: var(--fail, #c1153a); }

/* Data Import rows (item 459160e5): a VPT script that runs a Data Set. The
   plan line keeps the pool row's grid so the two kinds align; the third cell
   names the data set instead of offering a pool. The editor block sits under
   the Data field and only shows for the VPT phase. */
.tw-vpt-row.tw-vpt-import .tw-vpt-n input { width: 76px; }
.tw-vpt-ds { min-width: 0; font-size: 12px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  padding: 4px 7px; border: 1px dashed var(--border); border-radius: 6px; background: var(--bg-secondary); }
.tw-e-import { display: flex; align-items: center; gap: 8px; margin-top: 10px; cursor: pointer; }
.tw-e-import input { margin: 0; }
.tw-e-import-fields { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px; align-items: center; margin-top: 6px; }
.tw-e-import-fields select { min-width: 0; padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px;
  font: inherit; font-size: 12.5px; color: var(--text); background: var(--bg-card); }
.tw-e-import-fields .tw-vpt-n input { width: 76px; }
/* VPT evidence on a history row: the Data Set run line and the error list. */
.tw-run-ds { max-height: none; }
.tw-run-errs ul { margin: 6px 0 0; padding-left: 18px; }
.tw-run-errs li { font-family: var(--tw-mono); font-size: 10.5px; line-height: 1.6; word-break: break-word; }
