:root{
  --bg:#0a0b0e;--panel:#13151b;--panel2:#181b22;--line:#262a33;--txt:#e8eaf0;--mut:#888fa0;--dim:#5b6172;
  --amber:#ffb454;--amber2:#ff9d2f;--green:#62d39a;--red:#ff6f6f;--blue:#7aa2ff;--mag:#ff2bb5;--retry:#c084fc;--blocked:#45c8d8;--gone:#ff8d6f;
  --selbar-bg:var(--amber);  /* multi-select bar is solid ORANGE with BLACK button outlines (owner 2026-07-18; was inverted grey) */
  --mono:"DM Mono",ui-monospace,Menlo,monospace;--body:"Hanken Grotesk",system-ui,sans-serif;--disp:"Bricolage Grotesque",var(--body);
}
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{background:var(--bg);color:var(--txt);font-family:var(--body);font-size:14px}
a{color:var(--blue)}
button{font-family:var(--body)}
.hidden{display:none!important}
.ic{width:14px;height:14px;display:block;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}

/* layout: top bar full width, below = main + activity */
#app{display:flex;flex-direction:column;height:100vh}
.top{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 16px;background:var(--panel);border-bottom:1px solid var(--dim);flex:none;flex-wrap:wrap}
.brand{font-family:var(--disp);font-weight:800;font-size:23px;line-height:29px;letter-spacing:-.03em;color:var(--txt);display:inline-flex;align-items:center}
.brand b{color:var(--amber)}
/* The header wordmark (and ONLY that one) is an <a href="/console"> since
   2026-08-04 — previously a <button> that called location.reload(). Strip the UA
   chrome back to plain text: the anchor needs text-decoration + colour resets
   that the button never did (a bare <a> would render blue and underlined via the
   global `a{color:var(--blue)}`). Scoped to .topmain because .brand is shared
   with the five gate/reset/signup cards, which render it as a <span> and must
   not pick up a pointer cursor. */
.topmain .brand{background:none;border:0;padding:0;cursor:pointer;text-align:left;text-decoration:none;color:var(--txt)}
/* Toolbar row: never wraps internally (keeps the account/person icon pinned
   top-right instead of dropping to a 2nd line). Grows to fill the row so the
   filter can size between its min/max; the filter is the ONLY item allowed to
   shrink (siblings are flex-shrink:0 below), so it resizes first when the
   window narrows. */
.tools{display:flex;align-items:center;gap:7px;flex-wrap:nowrap;flex:1 1 auto;min-width:0;justify-content:flex-end}
.tools>.btn,.tools>.acctmenu{flex-shrink:0}
.btn{font-size:12.5px;padding:7px 12px;border:1px solid var(--line);background:var(--panel2);border-radius:8px;color:var(--txt);cursor:pointer;white-space:nowrap;display:inline-flex;align-items:center;gap:6px}
.btn:hover{border-color:var(--dim)}
.btn:disabled{opacity:.45;cursor:default}
.btn:disabled:hover{border-color:var(--line)}
/* Top-menu buttons: lighter-grey outline by default, orange on hover. */
.tools .btn{border-color:var(--dim)}
.tools .btn:hover{border-color:var(--amber)}
.filteractions .btn:hover{border-color:var(--amber)}
.btn.primary{background:linear-gradient(180deg,var(--amber),var(--amber2));border:0;color:#1a1206;font-family:var(--disp);font-weight:700}
/* Label is #1a1206, the SAME near-black .btn.primary rests at one line above —
   so the button keeps one text colour whether it is idle or working, and only
   the background animates (owner, 2026-08-05). It was #fff until then. The
   colour is set here rather than left to inherit because `.btn.busy` is also
   worn by non-primary buttons, which would otherwise inherit `.btn`'s light
   var(--txt) onto this amber wave. */
.btn.primary.discovering,.btn.busy{background:linear-gradient(110deg,var(--amber2),var(--amber),#ffe0a3,var(--amber),var(--amber2));background-size:300% 100%;animation:wavebg 1.1s ease-in-out infinite;color:#1a1206;border-color:transparent;cursor:progress}
/* A BUSY button is disabled (every call site sets .disabled alongside the class,
   to block a double submit) — so without this it inherited `.btn:disabled
   {opacity:.45}` above and spent the whole job at 45% opacity: label and wave
   animation both washed out (owner, 2026-08-05: "make sure the button text is
   white. it's currently dimmed" — the label was never grey, the whole element
   was). Opacity dims the WHOLE element, so no `color` can undo it — the dimming
   itself has to go. Same intent as the .shotmodal :not(.busy):not(.fail) guards
   further down: a button that is working must not be painted like a dead one.
   Scoped to the busy state only, so every ordinary disabled button stays dimmed.
   EXCEPTION since 2026-08-08 (scan Stop button, spec §6): .btn.primary.discovering
   is enabled ON PURPOSE while a scan runs — Stop has to be clickable — so its
   `:disabled` compound above never actually matches for it; harmless (an unmatched
   selector is inert, not wrong) and left in place because every OTHER .btn.busy
   caller (Saving…, the shotmodal state machine) IS still genuinely disabled and
   still needs the undim. The wave animation reads as "activity", not "disabled",
   for both — see the cursor override just below for the other half of the tell. */
.btn.primary.discovering:disabled,.btn.busy:disabled{opacity:1}
/* A SCANNING button is a fixed 150px with its label centred (owner,
   2026-08-05), so it stops resizing under itself every time the page counter
   gains a digit. `.btn` is inline-flex with no justify-content, so the label
   would otherwise sit hard left in the wider box. Global box-sizing:border-box
   (line 7) makes 150px the OUTER width, padding included.
   Scoped to .discovering, NOT the .busy twin it shares every other rule with:
   .busy is worn by unrelated buttons ("Saving…", the shotmodal state machine)
   whose widths are set by their own layouts. 150px fits every label this state
   actually shows — longest is "Scanned 3,000 pages" at 147px, and the counter
   is pages FETCHED, bounded by fetchCap = max(maxPages,50)*3 (1500 at the live
   fleet cap). Past ~5 digits it would overflow rather than wrap, because .btn
   sets white-space:nowrap.
   `flex:none` is LOAD-BEARING, not decoration: this button lives in `.frow`
   (display:flex), so it is a flex item with the default `0 1 auto` — and a flex
   item SHRINKS below its declared width when the line is over-constrained. On a
   narrow modal that silently returns the button to content width and the fix
   evaporates exactly where the jitter is most visible. Measured while getting
   this wrong: 136px instead of 150px, with the rule matching and applied. Same
   family as the `.below` collision — a flex container quietly overriding a
   child's own sizing.
   `cursor:pointer` overrides the shared rule's `:progress` (2026-08-08, same
   specificity + later in the cascade wins) — this button is the scan Stop
   button now, clickable on purpose, and a wait cursor on a clickable control
   lies about it. `.btn.busy` (unaffected — different selector) keeps :progress. */
.btn.primary.discovering{width:150px;flex:none;justify-content:center;cursor:pointer}
.btn.fail{background:var(--red);border-color:var(--red);color:#3a0d0d;font-weight:700}
@keyframes wavebg{0%{background-position:0% 50%}100%{background-position:300% 50%}}
/* fade-in + roll-down reveal (discovery results) */
.reveal{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows 1s cubic-bezier(.22,1,.36,1),opacity 1s ease}
.reveal>*{overflow:hidden;min-height:0}
.reveal.show{grid-template-rows:1fr;opacity:1}
.btn.icon{padding:7px 9px}
.btn.on{border-color:var(--amber);color:var(--amber);background:rgba(255,180,84,.08)}
/* account dropdown (person icon → gated menu of URL-routed pages) */
.acctmenu{position:relative;display:inline-flex}
.acctmenu #t-account[aria-expanded="true"]{border-color:var(--amber);color:var(--amber);background:rgba(255,180,84,.08)}
.acctdrop{position:absolute;top:calc(100% + 6px);right:0;min-width:172px;z-index:60;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:5px;box-shadow:0 14px 34px -10px rgba(0,0,0,.7);display:flex;flex-direction:column;gap:1px}
/* Signed-in email header atop the account dropdown, with a lighter-grey rule. */
.acctemail{font-family:var(--mono);font-size:11px;color:var(--mut);padding:7px 11px 9px;margin-bottom:4px;border-bottom:1px solid var(--dim);word-break:break-all;max-width:230px}
.acctitem{text-align:left;font-size:13px;padding:8px 11px;border:0;border-radius:7px;background:transparent;color:var(--txt);cursor:pointer;white-space:nowrap;font-family:var(--body)}
.acctitem:hover{background:var(--panel2)}
.acctdivider{height:1px;background:var(--line);margin:5px 4px}

.below{display:flex;flex:1;min-height:0}
/* No top padding on the scroll container: position:sticky pins to the CONTENT edge
   (below top padding), which would leave a gap where rows peek above the pinned domain
   bar. Move the top gap onto #domains so it scrolls away instead. */
/* container-type:inline-size makes .main the query container the domain bar's
   pill group measures itself against (owner 2026-07-25). It lives HERE and not
   on #domains deliberately: #domains is a grid whose max-content tracks are
   MEANT to overflow into horizontal scroll, so it is not a stable width to
   query. inline-size containment applies to the box's own width only and does
   not disturb the sticky .domhead or the bottom-sticky #stickybars inside. */
.main{flex:1;min-width:0;overflow:auto;padding:0 16px 16px;container-type:inline-size;container-name:list}
#domains{padding-top:16px}
.filterbar{display:flex;align-items:center;gap:10px;flex-basis:100%}
.topmain{display:flex;align-items:center;gap:0;flex:0 0 auto;min-width:0;flex-wrap:wrap}
/* nowrap since 2026-08-09 (owner): when the row runs out of width the FILTER
   PILLS stack and the four links keep their single line. It used to wrap, and
   because .statusfilter was a default flex item (basis auto = its content
   width) the pills never gave way — instead the whole link block dropped to a
   line of its own. Measured before: the block dropped at 740px and the topbar
   grew 98 -> 128px while the pills sat on one row. */
.filteractions{display:flex;gap:8px;flex-basis:100%;flex-wrap:nowrap;align-items:center}
/* DEFAULT: two columns of two (Select all/none | Expand all/Collapse all),
   pushed to the RIGHT end of the row (owner 2026-08-04 for the side) — i.e.
   TWO lines tall, which is what a wide window shows.
   NARROW: one column of four. The switch is `.pillstack`, set by
   fitFilterActions() in app.js, and the trigger is the owner's own — "only when
   the filter pills start stacking" — NOT a width. It cannot be a width: the
   pill labels carry live counts ("no changes (1421)"), so the wrap point moves
   with the data, the same reason fitSelbar() measures instead of thresholding.
   flex:none: the links must never absorb the shrink, or they compress instead
   of the pills wrapping.
   margin-LEFT:auto is the mirror of the old margin-right:50px. It is now
   belt-and-braces — .statusfilter grows to fill the row, so there is no free
   space left for the auto margin to eat — kept because it is what right-aligns
   the block if that flex ever changes. */
.linkstack{display:flex;gap:18px;margin-left:auto;flex:none}
.linkstack>div{display:flex;flex-direction:column;gap:2px;align-items:flex-start}
/* One column of four. 2px both ways so the four are evenly spaced — this gap
   runs BETWEEN the pairs while .linkstack>div's runs WITHIN each, and any
   difference between the two reads as an odd 2+2 grouping. */
.filteractions.pillstack .linkstack{flex-direction:column;gap:2px}
/* Filter lives in the top-row .tools cluster; min 150 / max 500 (min lowered
   from 250, owner 2026-07-25), and it is the first (only) toolbar item to
   shrink as the window narrows (siblings are flex-shrink:0). */
.filterwrap{position:relative;display:inline-flex;align-items:center;flex:1 1 250px;min-width:150px;max-width:500px}
.filter{width:100%;padding:7px 30px 7px 11px;border:1px solid var(--amber);border-radius:9px;font-size:13px;background:var(--panel);color:var(--txt)}
.filter-x{position:absolute;right:6px;top:50%;transform:translateY(-50%);display:flex;align-items:center;justify-content:center;width:20px;height:20px;border:0;border-radius:6px;background:transparent;color:var(--mut);font-size:18px;line-height:1;cursor:pointer;padding:0}
.filter-x:hover{background:var(--panel2);color:var(--txt)}
.filter:placeholder-shown+.filter-x{display:none}
.filter::placeholder{color:var(--dim)}
/* The ONLY flexible item in .filteractions, and min-width:0 is the load-bearing
   half: a flex item's automatic minimum is its MIN-CONTENT size, so without it
   the pills refuse to shrink past their widest row and the row overflows
   instead of wrapping (the same automatic-minimum rule that makes .domname
   truncate). basis 0 rather than auto so the box is sized by the space left
   over, not by the pills — which is what makes them wrap first. */
.statusfilter{display:flex;gap:6px;flex-wrap:wrap;flex:1 1 0;min-width:0}
/* Below this the toolbar can no longer hold the filter beside the buttons, and
   .top's own wrap dropped the WHOLE cluster under the wordmark — Add site, the
   density button, the filter, Activity Log and the account icon together
   (measured: 625px with Activity Log VISIBLE, 525px without). Owner
   2026-08-09: move ONLY the filter.
   order:1 puts it after the account icon in FLEX order — DOM order is
   untouched, so tab order and the wide layout are unaffected — and a 100%
   basis then gives it a line of its own, leaving every button on line 1.
   .tools must wrap for that to be possible; it is nowrap by default.
   640 is keyed to the Activity-Log-VISIBLE number deliberately: that button is
   shown on this install, and a threshold set for the narrower cluster would let
   the real one drop before the query ever fired. With it hidden the filter
   takes its own line ~115px earlier than strictly necessary — one spare line,
   never a broken row. Re-measure if the cluster gains or loses a control. */
@media (max-width:640px){
  /* flex:1 1 0 is load-bearing and NOT tidiness. .top wraps, and a wrapping
     flex container assigns items to lines by their HYPOTHETICAL main size —
     flex-basis before any shrinking — so .tools at basis auto measures its full
     content (buttons + a 500px filter) and drops below the wordmark whole, no
     matter how far it could have shrunk. Zeroing the basis lets it share the
     wordmark's line and grow into what is left, which is what puts the buttons
     back on line 1; the filter then wraps to line 2 INSIDE .tools. Without
     this the change makes things worse: wordmark, then buttons, then filter. */
  .tools{flex-wrap:wrap;flex:1 1 0}
  .filterwrap{order:1;flex:1 1 100%;max-width:none}
  /* .tools is two lines tall here, and .top centres its children on the cross
     axis — which would hang the wordmark halfway down, level with the gap
     between the buttons and the filter (measured: 19px below the button row).
     Top-aligning puts it back on the button row (measured: 1px out). */
  .top{align-items:flex-start;position:relative}
  /* Taking the wordmark out of flow is what makes the filter FULL width (owner
     2026-08-09). In flow it shares line 1 with .tools, so .tools — and the
     filter's 100% basis inside it — starts at the wordmark's right edge and the
     input is ~113px short on the left. Absolute, .tools is the only in-flow
     item and spans the whole bar. Nothing collides: .tools is
     justify-content:flex-end, so the buttons stay hard right (~250px) while the
     wordmark holds the left (~101px) — verified down to 400px, far below the
     640px this query even starts at. Offsets mirror .top's own padding. */
  .topmain{position:absolute;left:16px;top:11px}
}

/* domains */
.dom{background:var(--panel);border:1px solid var(--dim);border-radius:11px;margin-bottom:var(--card-gap,10px);grid-column:1/-1;display:grid;grid-template-columns:subgrid}
/* Sticky domain bar: pins to the top of .main while its own URLs scroll past, then the
   next domain bar pushes it up (each .domhead sticks within its own .dom containing block). */
/* Columnized bars via SUBGRID (owner 2026-07-18, 3rd revision — supersedes the
   fixed 200px tracks): ONE track list lives on #domains and every domain bar,
   column-title row and URL row subscribes via grid-template-columns:subgrid.
   The four middle columns are max-content, so they EXPAND to the widest pill
   set anywhere on the page — pills never stack vertically — while width AND
   position stay identical across every domain (independent per-bar grids
   cannot share max-content sizing; that's the whole reason for subgrid).
   Cell content left-aligned. Track order (7 tracks; LAST CHECK column dropped
   into hover tooltips + a STATE pill column added left of CHANGES, owner
   2026-07-22; the DIFF column retired 2026-08-04 — the diff %% now rides ON the
   CHANGED pill, e.g. "2%% CHANGE"): [exp|chk] [num] [URL 1fr] [STATE] [CHANGES]
   [ISSUES] [icons]. STATE holds the uptime pill on the domain bar and the auto-added/
   auto-removed pill on URL rows; CHANGES was renamed from STATUS. The uniform
   13px horizontal padding on all three bar types offsets their first/last
   subgrid items identically, so cross-bar alignment survives it. Narrow
   viewports get a horizontal scroll (.main). Number precedes checkbox in
   tracks 1-2 (owner 2026-07-24, was checkbox-then-number) on BOTH URL rows
   (rowEl appends num before cb) and the column-title row (domChk moves after
   the num placeholder) — so the select-all checkbox stays aligned above each
   row's own checkbox; the domain bar's track 1 stays the expand triangle
   (no per-domain checkbox exists there). column-gap stays one uniform 10px
   across every track. */
/* ISSUES track (6th) is minmax(0,max-content) — not plain max-content — so once
   the flexible URL column hits its 200px min, the ISSUES column is the next to
   give: its pills wrap/stack (col-issues is flex-wrap:wrap) before the row falls
   into horizontal scroll (owner 2026-07-25: "stack ISSUES pills first"). */
/* ACTIONS track (7th) is sized to the DOMAIN bar's button count, which is the
   larger of the two clusters that share it (.domactions 7, .rowicons 4). One
   .iconbtn = 28px (14px .ic + 6px padding + 1px border per side, box-sizing:
   border-box) and the flex gap is 6px, so n buttons need n*28 + (n-1)*6:
   5 -> 164px, 6 -> 198px, 7 -> 232px.
   BUT the track is NOT the usable width. .dom (1px border) and .domhead (13px
   horizontal padding) are both subgrids, and per CSS Grid L2 a subgrid's own
   padding and border come out of its FIRST and LAST tracks — so .domactions
   actually gets track - 14px. That is the same rule that made .num overlap the
   checkbox (CLAUDE.md Traps, "Console CSS"), hitting the other end of the list.
   Measured, not derived (getBoundingClientRect, tmp/ harness on the real
   stylesheet, 700-1400px incl. the 1000px container breakpoint):
     track 176 + 6 buttons -> usable 162, slack -36  (overlaps ISSUES by 26px)
     track 212 + 6 buttons -> usable 198, slack   0  (flush)
     track 224 + 6 buttons -> usable 210, slack  12  <- shipped 2026-08-04
   224 delivers the 12px of slack the old 176px was aiming for (164 + 12) and
   never got: with 5 buttons it measured slack -2, i.e. the cluster has been
   spilling 2px over the ISSUES column since the DIFF column was retired. That
   is fixed here in passing.
   RE-MEASURED 2026-08-07 when the label button became the cluster's 7th (the
   label pill moved out of .namecell into .domactions):
     track 224 + 7 buttons -> usable 210, slack -22  (overlaps ISSUES)
     track 246 + 7 buttons -> usable 232, slack   0  (flush)
     track 258 + 7 buttons -> usable 244, slack  12  <- shipped
   258 holds the same +12 slack 224 gave six buttons. The tag button ships
   inside a .labelwrap (position:relative, the dropdown anchors to it), which is
   an EXTRA flex item — measured IN the cluster at 28px, i.e. it costs nothing;
   measuring the bare button would have measured something that doesn't ship.
   BOTH clusters are justify-content:flex-end, so an undersized track does NOT
   clip or scroll — it overflows LEFT, over the ISSUES column, which reads as a
   crowded row rather than as a bug. Adding a button here means re-measuring,
   and measuring the CLUSTER, not the track. */
#domains{display:grid;column-gap:var(--col-gap,10px);grid-template-columns:26px 26px minmax(var(--url-min,200px),1fr) max-content max-content minmax(0,max-content) var(--actions-w,258px)}
#domains > .empty{grid-column:1/-1}
.rows{grid-column:1/-1;display:grid;grid-template-columns:subgrid}
.domhead,.colhead,.row{grid-column:1/-1;display:grid;grid-template-columns:subgrid;align-items:center}
.domhead{padding:var(--bar-pad-y,12px) 13px;cursor:pointer;position:sticky;top:0;z-index:3;background:var(--panel);border-radius:10px 10px 0 0}
/* overflow:visible (not hidden): the label pill's dropdown now lives inside
   .namecell (2026-07-24, moved next to the URLs pill) and must be able to
   escape this box. The domain-name ellipsis below doesn't depend on this
   property — .domname's OWN overflow:hidden already gives it an automatic
   flex min-width of 0 per spec, which is what lets it shrink+truncate. */
.namecell{display:flex;align-items:center;gap:10px;min-width:0;overflow:visible}
.namecell .domname{overflow:hidden;text-overflow:ellipsis}
/* The domain bar drops its `.num` placeholder (owner 2026-07-25) so track 1 is
   the expand triangle. The site name used to span 2/4, i.e. it STARTED in the
   checkbox track; owner 2026-08-05 moved the site's select-all checkbox onto the
   bar (track 2, above the rows' own checkboxes) and the name onto TRACK 3, flush
   with the `.url` in the rows below. Pinning the span explicitly (rather than
   letting it auto-place) keeps the following cells on tracks 4-7: a definite-
   column item advances the auto-placement cursor.
   Both alignments survive the bars' 13px padding because tracks 2 and 3 are
   INTERIOR tracks — per CSS Grid L2 a subgrid's padding is subtracted from its
   first and last tracks only, and .domhead/.colhead/.row all carry the same
   13px anyway. */
.domhead .namecell{grid-column:3/4}
/* The "n URLs" + "label" pills never stack onto a second line (owner
   2026-07-25). `.domname` already ellipsis-truncates, so it is the flex item
   that gives — the trade is a shortened site name, not wrapped pills. Scoped to
   .namecell because mailAuthCard() reuses .pills in the Domain-settings modal
   and DOES want wrapping there. */
.namecell .pills{flex-wrap:nowrap}
.namecell .pills>*{flex:none}
/* The "N URLs" pill reads in the action buttons' glyph colour, not the muted
   pill default (owner 2026-08-09) — it is a fact about the site, not a dimmed
   aside. Scoped to the name cell so the muted default still applies to every
   other bare .pill; the border stays var(--dim), only the text moves. */
.namecell .pills .pill{color:var(--txt)}
.dom.collapsed .domhead{border-radius:10px}
.domname{font-family:var(--disp);font-weight:700;font-size:var(--name-font,14.5px);white-space:nowrap}
.pills{display:flex;gap:var(--pill-gap,6px);flex-wrap:wrap;align-items:center}
/* Domain-bar pill group (STATE / CHANGES / ISSUES), owner 2026-07-25.
   DEFAULT: `display:contents` — the three cells stay direct subgrid items in
   tracks 4/5/6 exactly as before (ISSUES stacks inside its own track when
   squeezed; see the flex-wrap rule below).
   NARROW (the container query — GENERATED at runtime, see below): ONLY the
   ISSUES cell drops to the domain bar's own second grid row, starting at track
   3 so it lines up with the URL column of the rows underneath. STATE and
   CHANGES stay on line 1 in tracks 4/5 (owner 2026-08-09) — until then the
   whole WRAPPER moved and took all three down together, which is why the
   wrapper still exists but is now never re-styled. `.domhead` subscribes to the
   parent's COLUMN template via subgrid but keeps its own implicit ROWS, which
   is what makes a grid-row:2 item legal here. The generated block must also pin
   .domactions, which otherwise auto-places into the vacated ISSUES track — see
   the ⚠ note beside containerCss() in uituning.js. */
.dompillgroup{display:contents}
/* STATE and CHANGES stay on one line. ISSUES does NOT any more (owner
   2026-08-09 — `.domhead .col-issues` was the third name in this list until
   then, and removing it is the whole fix).
   ISSUES is the only cell that can be squeezed: tracks 4/5 are max-content and
   the 1fr name track stops at --url-min, so when the bar runs out of room the
   ISSUES track is what shrinks — and a nowrap flex row does not shrink, it
   OVERFLOWS RIGHT, straight under .domactions, which is justify-content:
   flex-end and so never moves aside. That is the reported "buttons on top of
   the pills".
   Letting it wrap makes the pills stack inside their own column at exactly the
   width where they would otherwise have begun overlapping — self-adjusting,
   which is what a fixed breakpoint provably cannot be: the overlap width moves
   with the pill count (measured table in uituning.js). It now inherits
   flex-wrap:var(--issues-wrap,wrap) from .col-issues, so the density panel's
   wrap toggle governs the bars and the URL rows together. */
.domhead .col-state,.domhead .col-status{flex-wrap:nowrap}
/* The narrow-mode blocks themselves are NOT here (density panel, 2026-08-07):
   a container query's condition cannot read var(), so they are GENERATED by
   public/console/uituning.js containerCss() and injected as
   <style id="uitune-bp"> at boot + on change by applyUiTune() (app.js) — TWO
   blocks since the 2026-08-09 filter restructure: the threshold block
   (default 1300px — the measured collision table lives beside it) moves ONLY
   `.domhead .col-issues` to grid-row 2 and pins
   `.domactions{grid-column:-2/-1}`, and a stacked-card block at
   min(800px, threshold) reflows the whole bar into a flex card. (The
   2026-08-07 form — the wrapper re-styled to flex + `.namecell{grid-column:
   3/-2}` — is RETIRED; its measurement history is kept in uituning.js beside
   the template.) The injected style must exist even with NO stored settings.
   Editing the narrow-mode rules means editing containerCss(), never
   re-adding a static block here (test/uituning.test.js fails on any
   @container rule in this file). */
/* Pill FONT SIZE is unified at 11px across every domain-bar and URL-row pill
   family — .pill / .uptime-badge / .statusbar .seg / .scanpill(.big) /
   .dot / .flagbadge / .autopill (owner, 2026-08-07; was a spread
   of 9–10.5px). The families keep their own padding/radius: domain bar
   3px 9px / 999px, URL row 1px 6px / 6px (.flagbadge 1px 5px / 5px).
   Since the density panel (2026-08-07) the 11px lives in var(--pill-font,11px)
   and the paddings scale via var(--pill-scale,1) — every fallback must equal
   the uituning.js KNOBS default (pinned by test/uituning.test.js). */
.pill{font-family:var(--mono);font-size:var(--pill-font,11px);padding:calc(3px*var(--pill-scale,1)) calc(9px*var(--pill-scale,1));border-radius:999px;background:var(--panel2);color:var(--mut);border:1px solid var(--dim);white-space:nowrap}
.pill.status{letter-spacing:.1em}
.pill.ok{background:rgba(98,211,154,.14);color:var(--green);border-color:rgba(98,211,154,.35)}
.pill.changed{background:rgba(255,180,84,.14);color:var(--amber);border-color:rgba(255,180,84,.35)}
.pill.error{background:rgba(255,111,111,.14);color:var(--red);border-color:rgba(255,111,111,.35)}
.pill.pending{background:rgba(122,162,255,.14);color:var(--blue);border-color:rgba(122,162,255,.35)}
.pill.flag{background:rgba(255,43,181,.12);color:var(--mag);border-color:rgba(255,43,181,.35)}
.pill.warn{background:rgba(255,180,84,.14);color:var(--amber);border-color:rgba(255,180,84,.35)}
.domactions{display:flex;gap:6px;justify-content:flex-end}
.iconbtn{cursor:pointer;border:1px solid var(--dim);background:var(--panel2);border-radius:7px;padding:5px 6px;line-height:0;color:var(--txt)}
.iconbtn:hover{border-color:var(--amber)}
.iconbtn.danger:hover{border-color:var(--red);color:var(--red)}
/* A disabled icon button keeps pointer-events on (its data-tip is what
   explains why it's dead — see the .iconbtn.disabled comment below), so it
   CAN become the :hover target; without this it borrowed the amber hover
   above and looked clickable at exactly the moment its tip says otherwise. */
.iconbtn.disabled:hover{border-color:var(--dim)}

.colhead{padding:7px 13px;border-top:1px solid var(--line);background:rgba(127,127,127,.04)}
.ch{font-family:var(--mono);font-size:9.5px;letter-spacing:.14em;color:var(--mut)}
/* #fff is deliberate (not var(--txt)): these list markers sit on --panel and
   want to be brighter than the body text beside them. */
.lnum{font-family:var(--mono);font-size:11.5px;color:#fff;flex:none}
/* Sticky domheads all sit at z-index:3, so a LATER domain's bar paints over an
   open label dropdown in an earlier one (they're sibling stacking contexts and
   DOM order wins at equal z). Lift the bar that owns the open dropdown. */
.domhead:has(.labeldrop){z-index:6}
.rows{border-top:1px solid var(--line);overflow:hidden;border-radius:0 0 10px 10px}
.row{padding:var(--row-pad-y,9px) 13px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);font-size:var(--row-font,13px)}
.row:last-child{border-bottom:0}
.row:hover{background:color-mix(in srgb,var(--txt) 7%,transparent)}
.row.hl{background:rgba(255,180,84,.10)}
.row.sel .url{color:var(--amber)}
/* Row numbers: no trailing dot, centred between the card edge and the checkbox
   (owner 2026-07-25, actually delivered 2026-08-04).
   NO width HERE — that was the bug, and the old comment ("width:26px matches the
   track") was the stale assumption that hid it. `.row`/`.colhead` carry
   `padding:… 13px`, and per CSS Grid L2 a subgrid's own padding is subtracted
   from its FIRST and LAST tracks: track 1 is 26px on #domains but only 13px
   inside the row. A hardcoded 26px box therefore overflowed its track rightwards
   and OVERLAPPED the checkbox by 4px (measured: num 14→40, chk starts at 36),
   which is why the numbers looked right-aligned against them.
   Left auto, the grid's default justify-self:stretch fills whatever the track
   really is and text-align:center then centres inside it. Don't reintroduce a
   fixed width unless the row padding goes too.
   THE NEGATIVE MARGINS (owner 2026-08-05, "the number is still not centred
   between the left outline and the checkbox"): stretch alone gives the cell
   only 12px (26px track - 13px row padding - 1px .dom border), and a 2-digit
   number is already 15.6px at DM Mono 13px. Chrome pins overflowing centred
   text to the line box's LEFT edge, so the surplus grows RIGHTWARD only —
   measured from the row's left edge, glyphs vs the checkbox at x=35:
     "7"     15.1->22.9   gap  12.1
     "98"    13  ->28.6   gap   6.4
     "147"   13  ->36.4   gap  -1.4   <- overlapping
     "1024"  13  ->44.2   gap  -9.2   <- overlapping
   i.e. the wider the number the further right it goes, with the 13px of dead
   space on the left never used. That reads as right-aligned-against-the-
   checkbox, not as an overflow. So hand the cell the whole span the eye
   actually judges — card edge to checkbox — by cancelling the row padding on
   the left (-13px) and the column gap on the right (-10px): stretch then
   resolves to 35px (12 + 13 + 10), the box runs x=0..35 (flush to the checkbox,
   never over it — .chk owns track 2 from x=35) and text-align:center finally
   has room to centre. Measured after: "7" 13.6->21.4, "147" 5.8->29.2,
   "1024" 1.9->33.1 — symmetric at every width. The LEFT magic number is the
   row padding declared above (.row/.colhead padding 13px — change it and
   re-measure); the RIGHT side is DERIVED, calc(-1*var(--col-gap,10px)), so it
   follows the density panel's gap slider by construction. Track sizes are
   fixed px, so the negative margins cannot resize a track. */
.num{font-family:var(--mono);color:var(--mut);text-align:center;flex:none;margin-left:-13px;margin-right:calc(-1*var(--col-gap,10px))}
.url{font-family:var(--mono);color:var(--txt);text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:calc(var(--row-font,13px) - .5px)}
.url:hover{text-decoration:underline}
.right{display:flex;align-items:center;gap:14px;color:var(--mut);font-size:12px}
.col-status{display:flex;gap:6px;align-items:center;justify-content:flex-start}
.col-issues{display:flex;gap:calc(var(--pill-gap,6px) - 1px);align-items:center;justify-content:flex-start;flex-wrap:var(--issues-wrap,wrap)}
/* The domain bar's ISSUES cell is split into a scan-pill slot and the issue
   pills (app.js domainHeadEl). BOTH are display:contents by default, so the
   pills stay direct flex children of .col-issues and the wide bar renders
   exactly as it did before the split — the wrappers exist only so the stacked
   narrow layout can lift the scan slot onto the URLs/uptime/CHANGES line and
   give it a reserved width (see containerCss in uituning.js). */
.scanslot,.issuepills{display:contents}
.col-state{display:flex;gap:6px;align-items:center;justify-content:flex-start}
.urlcell{display:flex;align-items:center;gap:8px;flex:1;min-width:0;overflow:hidden}
/* URL-row status: a small outlined pill (matches .flagbadge/.scanpill), not a
   dot+text label — the leading circle (.dot::before) is retired. */
.dot{display:inline-flex;align-items:center;font-size:var(--pill-font,11px);font-family:var(--mono);letter-spacing:.06em;white-space:nowrap;padding:calc(1px*var(--pill-scale,1)) calc(6px*var(--pill-scale,1));border-radius:6px;border:1px solid}
.dot.ok{background:rgba(98,211,154,.14);color:var(--green);border-color:rgba(98,211,154,.4)}
.dot.changed{background:rgba(255,180,84,.14);color:var(--amber);border-color:rgba(255,180,84,.4)}
.dot.error{background:rgba(255,111,111,.14);color:var(--red);border-color:rgba(255,111,111,.4)}
.dot.pending{background:rgba(122,162,255,.14);color:var(--blue);border-color:rgba(122,162,255,.4)}
/* Bot-wall verdict (spec 2026-08-23): a SETTLED troubled state, like error —
   no pulse/animation, unlike retrying below, which is actively in-progress.
   Cyan (--blocked) is chosen so it can never be mistaken for any other status:
   no existing console colour occupies cyan/teal (the palette is otherwise
   amber/red/green/blue/magenta/violet + greys). Slate/grey was rejected — it
   collides with the dim-text greys (--mut #888fa0 / --dim #5b6172) and would
   read as disabled rather than a state; rust was rejected as too close to
   error's red. */
.dot.blocked{background:rgba(69,200,216,.14);color:var(--blocked);border-color:rgba(69,200,216,.4)}
.dot.gone{background:rgba(255,141,111,.14);color:var(--gone);border-color:rgba(255,141,111,.4)}
/* the no-answer family shares BLOCKED's cyan on purpose (owner pick, Task 8):
   "we didn't get your page" is exactly what that tone already means */
.dot.noanswer{background:rgba(69,200,216,.14);color:var(--blocked);border-color:rgba(69,200,216,.4)}
/* RETRYING (driven later by Task 8) must read as visually distinct from
   CHANGED's amber — a different hue (violet, --retry) PLUS a pulse, so the two
   never look identical even at a glance. */
.dot.retrying{background:rgba(192,132,252,.16);color:var(--retry);border-color:rgba(192,132,252,.45);animation:retrypulse 1.1s ease-in-out infinite}
@keyframes retrypulse{0%,100%{opacity:1}50%{opacity:.55}}
/* Domain status pills: one chip per non-ok state present. Restyled 2026-07-25
   to match the ISSUES pills (.pill) — separate rounded, gap-separated, bordered
   chips with the same tint recipe (rgba(...,.14) fill / rgba(...,.35) border /
   toned text) — instead of the old single overflow-clipped .statusbar capsule
   with panel-colored divider cuts. Wraps when the CHANGES column is tight. */
.statusbar{display:inline-flex;flex-wrap:wrap;gap:calc(var(--pill-gap,6px) - 1px);vertical-align:middle;align-items:center;font-family:var(--mono)}
.statusbar .seg{padding:calc(3px*var(--pill-scale,1)) calc(9px*var(--pill-scale,1));font-size:var(--pill-font,11px);border-radius:999px;white-space:nowrap;border:1px solid}
.statusbar .seg.ok{background:rgba(98,211,154,.14);color:var(--green);border-color:rgba(98,211,154,.35)}
.statusbar .seg.changed{background:rgba(255,180,84,.14);color:var(--amber);border-color:rgba(255,180,84,.35)}
.statusbar .seg.error{background:rgba(255,111,111,.14);color:var(--red);border-color:rgba(255,111,111,.35)}
.statusbar .seg.pending{background:rgba(122,162,255,.14);color:var(--blue);border-color:rgba(122,162,255,.35)}
/* Same cyan --blocked tone as .dot.blocked, so the domain-pill chip reads as
   the same state as the per-URL pill. */
.statusbar .seg.blocked{background:rgba(69,200,216,.14);color:var(--blocked);border-color:rgba(69,200,216,.35)}
.statusbar .seg.gone{background:rgba(255,141,111,.14);color:var(--gone);border-color:rgba(255,141,111,.4)}
/* the no-answer family shares BLOCKED's cyan on purpose (owner pick, Task 8):
   "we didn't get your page" is exactly what that tone already means */
.statusbar .seg.noanswer{background:rgba(69,200,216,.14);color:var(--blocked);border-color:rgba(69,200,216,.4)}
/* Same violet --retry tone as .dot.retrying, so the domain-pill chip reads as
   the same state as the per-URL pill. */
.statusbar .seg.retrying{background:rgba(192,132,252,.14);color:var(--retry);border-color:rgba(192,132,252,.35)}
.flagbadge{font-family:var(--mono);font-size:var(--pill-font,11px);padding:calc(1px*var(--pill-scale,1)) calc(5px*var(--pill-scale,1));border-radius:5px;background:rgba(255,43,181,.14);color:var(--mag);border:1px solid rgba(255,43,181,.4);white-space:nowrap}
.flagbadge.err{background:rgba(255,111,111,.14);color:var(--red);border-color:rgba(255,111,111,.4)}
/* Amber = the ADVISORY tier (2026-08-03), so the three severities the issues
   modal and the domain bar use — magenta content / red broken / amber advisory
   — all survive on the URL row too. Used by the per-page site-hygiene badges
   whose ISSUE_META cls is 'warn' (noindex, orphan page). */
.flagbadge.warn{background:rgba(255,180,84,.14);color:var(--amber);border-color:rgba(255,180,84,.4)}
/* Scan-state pills (owner 2026-08-07): on URL rows the geometry is a
   character-for-character copy of .dot above — PENDING/SCANNING read as
   siblings of the status pill next door, and NO ISSUES mirrors .dot.ok's
   NO CHANGE exactly. Blue stays the console's in-progress family (PENDING /
   CHECKING / BASELINING), not a severity — the magenta SCANNING status of the
   CHANGES column is retired along with the idea that an issue scan is a
   change state (it also masked real CHANGED/ERROR while a scan ran). */
.scanpill{display:inline-flex;align-items:center;font-size:var(--pill-font,11px);font-family:var(--mono);letter-spacing:.06em;white-space:nowrap;padding:calc(1px*var(--pill-scale,1)) calc(6px*var(--pill-scale,1));border-radius:6px;border:1px solid}
.scanpill.pending,.scanpill.scanning{background:rgba(122,162,255,.14);color:var(--blue);border-color:rgba(122,162,255,.4)}
.scanpill.clean{background:rgba(98,211,154,.14);color:var(--green);border-color:rgba(98,211,154,.4)}
/* Domain-bar geometry (owner 2026-08-04): on a domain bar the scan pill's
   neighbours are .pill (3px 9px / 999px radius), so the URL-row geometry above
   would read as a foreign element there. Font size no longer differs (unified
   at 11px, owner 2026-08-07) so .big stopped resetting it; letter-spacing
   resets to match the .statusbar segs, whose count-pill voice "N SCANNING"
   borrows — and the border alpha drops to their .35 (the .4 above is the
   .dot family's). */
.scanpill.big{letter-spacing:normal;padding:calc(3px*var(--pill-scale,1)) calc(9px*var(--pill-scale,1));border-radius:999px}
.scanpill.big.pending,.scanpill.big.scanning{border-color:rgba(122,162,255,.35)}
.scanpill.big.clean{border-color:rgba(98,211,154,.35)}
/* The URL list no longer has a DIFF column (retired 2026-08-04 — the diff % rides
   on the CHANGED pill now), so this survives ONLY for the screenshot modal's
   .shotmeta.pct, which extends it below. Don't delete it with the column. */
.pct{font-family:var(--mono);text-align:left;color:var(--txt)}
.rowicons{display:flex;gap:6px;justify-content:flex-end}

/* skriinsjot amber checkbox */
.chk{display:inline-flex;align-items:center;gap:9px;cursor:pointer;user-select:none}
.chk input{position:absolute;opacity:0;width:0;height:0}
.box{width:18px;height:18px;border-radius:6px;border:1.5px solid var(--amber);background:var(--panel2);display:grid;place-items:center;transition:.15s;flex:none}
.box svg{width:12px;height:12px;stroke:var(--bg);stroke-width:3.2;fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:0;transform:scale(.6);transition:.15s}
.chk input:checked+.box{background:var(--amber);border-color:var(--amber)}
.chk input:checked+.box svg{opacity:1;transform:scale(1)}
.chk:hover .box{border-color:var(--amber2);background:rgba(255,180,84,.5)}
.chk:hover input:checked+.box,.chk:hover .box.on,.chk:hover .box.mid{background:var(--amber)}
.box.xbox{border-color:var(--line);background:var(--panel2);color:var(--mut);font-size:12px;line-height:1;cursor:default}
.box.on,.box.mid{background:var(--amber);border-color:var(--amber)}
.box.on svg,.box.mid svg{opacity:1;transform:scale(1)}

/* selection bar */
/* Multi-select bar: amber border over a solid amber strip (--selbar-bg), which
   reads as a light band against the dark row list; counter + "Deselect all"
   left, action buttons dead-centered. */
/* Sticky wrapper: pins the manual-run bar (#runbar) + the selection bar (#selbar)
   to the bottom of the scrolling list. Both are static in normal flow INSIDE it
   (only the wrapper is sticky) so they never overlap — runbar stacks ABOVE selbar
   (DOM order), and either/both can be hidden independently. Empty (both hidden) =
   zero height, so no phantom gap under the list. */
.stickybars{position:sticky;bottom:0;z-index:5;display:flex;flex-direction:column}
/* position:relative makes the BAR the containing block for the absolutely
   centered .selbar-center — load-bearing twice over. (1) The group is centered
   on BOTH axes now; anchored to #stickybars instead (the pre-2026-08-05 state,
   via the sticky wrapper / an engine-dependent container-type quirk), top:50%
   would mis-center whenever the runbar stacks above the selbar. (2) No
   container-type here: nothing queries the bar's size since the 940px query
   died, and size containment is exactly the engine-divergent CB mechanism
   being retired. */
.selbar{position:relative;margin-top:14px;background:var(--selbar-bg);border:1.5px solid var(--amber);border-radius:11px;padding:10px 14px;display:flex;align-items:center;gap:10px;box-shadow:0 10px 30px rgba(0,0,0,.55)}
/* Run progress bar (SPEC §8): same amber-outlined strip as .selbar, stacked
   above it. One .runseg per active run: .runseg-txt label and optional
   .runseg-phase caption (e.g., "<Label> · <done>/<total>" or "<Label> · N pages
   found" when total is null), plus a × cancel and, on a queued run, a ▷
   start-now button (run-progress-granularity, 2026-08-06 evening; outline
   glyph since 2026-08-07 — see the .runseg-play comment below); multiple
   runs divided by .runbar-div. EVERY queued run gets a full segment now —
   the collapsed "N sites queued" chip was REMOVED (owner reversal of spec D2;
   CLAUDE.md carries the history) in favour of always showing the whole queue.
   Already wraps on narrow bars (flex-wrap+gap below predate this change);
   the multi-line COLLAPSE on top of that wrap — one line by default, a
   .runbar-caret to expand — is new, see the caret/.runbar-clamped rules
   below and applyRunbarCollapse() in app.js. Scheduled runs
   (trigger:'scheduled') lead with a clock icon; manual runs carry none, and
   that contrast is what makes the clock legible. */
.runbar{margin-top:14px;background:var(--selbar-bg);border:1.5px solid var(--amber);border-radius:11px;padding:10px 14px;display:flex;align-items:center;flex-wrap:wrap;gap:10px 14px;box-shadow:0 10px 30px rgba(0,0,0,.55)}
/* Collapsed to one line (applyRunbarCollapse sets max-height inline, measured
   off the real rendered content — see app.js — never a fixed px here). Scoped
   to this class rather than living on .runbar unconditionally so a bar that
   fits one line on its own is never at risk of clipping a stray box-shadow or
   focus ring; nothing needs to escape #runbar today (tooltips are appended to
   <body>, not here — CLAUDE.md), but there's no reason to clip more than the
   one case that needs it. */
.runbar-clamped{overflow:hidden}
/* min-width:0 is REQUIRED, not decoration: a flex item's default min-width is
   auto, so a long caption ("Recrawling X · Checked 812 page(s), 4,910 link(s)
   — 12 broken so far") would grow past the bar and push .runseg-x off screen
   rather than ellipsising. */
.runseg{display:flex;align-items:center;gap:9px;min-width:0;max-width:100%;flex:0 1 auto}
/* Clock marks a SCHEDULED run (Wave 2, 2026-08-02) — the icon sits inline with
   the label text. Manual runs render no icon at all, which is what makes the
   clock readable as "this one wasn't you". */
/* color:#000 is REQUIRED, not decorative. `.ic` is stroke:currentColor, and
   .runseg sets no color of its own, so without this the clock inherits
   body{color:var(--txt)} (#e8eaf0) onto the amber strip — about 1.47:1
   contrast, i.e. invisible, which defeats the whole point of marking a
   scheduled run. Every other element on this strip forces #000 the same way
   (.runseg-txt, .runseg-x). Review finding, 2026-08-02. */
.runseg .ic{width:13px;height:13px;opacity:.75;flex:0 0 auto;color:#000}
.runseg-txt{font-family:var(--mono);font-size:12px;letter-spacing:.03em;color:#000;white-space:nowrap;min-width:0;overflow:hidden;text-overflow:ellipsis}
/* The live caption: dimmed via OPACITY, never a lighter colour — the strip is
   solid amber and everything on it forces color:#000 (CLAUDE.md; inheriting
   body{color:var(--txt)} here is invisible). flex-shrink:60 below (vs. the
   label's default 1) makes the PHASE give up width first and hard: measured
   in tmp/runbar-measure.html (numbers in the fix's report) with a long label
   paired with a long phase in one segment, the phase starts losing width
   around 860-900px of row width while the label is still at its untouched
   natural size, and by 500px the phase has given up ~90% of its own width
   against ~2% for the label. Exact crossover widths scale with how long the
   real label/phase text is — the PROPORTION (phase collapses first and does
   almost all of the giving-way; the label barely moves until very narrow) is
   what the shrink ratio guarantees, not a fixed breakpoint. .runseg-x never
   shrinks (flex:0 0 auto below), so the × stays a fixed 20×20px target
   regardless of bar width. Both spans get their min-width:0/overflow:hidden/
   ellipsis from .runseg-txt above — a flex item's default min-width is auto,
   so without it neither span can shrink below its own content width and the
   label overflows the bar, pushing .runseg-x out instead of ellipsising. */
.runseg-phase{opacity:.72;flex-shrink:60}
/* An empty phase span (Fix 1: always rendered, even with no caption, so a
   caption appearing/disappearing is a text change rather than a structural
   one) must contribute nothing, or .runseg's gap:9px still opens a stray
   blank space where the caption would have gone. */
.runseg-phase:empty{display:none}
/* Dim the icon with the label, or a cancelled run's clock reads as more
   prominent than its own greyed-out text. */
.runseg.cancelling .runseg-txt,.runseg.cancelling .ic{opacity:.55}
.runbar-div{align-self:stretch;width:1px;background:rgba(0,0,0,.32)}
/* × cancel chip: mirrors the selbar button palette (black outline in the
   ORANGE strip; hover inverts). flex:0 0 auto pins its size — without it the
   button is a shrinkable flex item like any other, and the browser would
   start shrinking it too once both text spans are already at their ellipsis
   floor. */
.runseg-x{font-family:var(--mono);font-size:14px;line-height:1;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;border-radius:5px;border:1px solid #000;color:#000;background:transparent;cursor:pointer;transition:background .12s,color .12s,border-color .12s;flex:0 0 auto}
.runseg-x:hover{background:#000;color:var(--amber)}
/* A QUEUED run — a manual recrawl waiting behind another site's crawl. Dimmed
   because it is real work you asked for that has not started yet. */
.runseg.queued .runseg-txt{opacity:.62}
/* ▷ "play" a queued run: same box as .runseg-x (size, border, hover-invert —
   applied via the shared class, see runSegEl) so the two sit as a visually
   matched pair. .runseg-play itself carries no rules — kept as a class hook
   per the brief ("...with a runseg-play modifier IF any tweak is needed"),
   not because one turned out to be needed: canvas glyph-metrics measurement
   (actualBoundingBox at DM Mono 14px, the shared font-size) found ▶'s own ink
   (12×11) was already LARGER than ×'s (8×6) at the identical size — a filled
   triangle simply carries more ink than a thin cross — so bumping ▶'s size up
   to "match" × would have made an already-bigger glyph bigger still.
   Outline since 2026-08-07 (owner: "make the play triangle just the
   outline") — re-measured rather than assumed: U+25B7 ▷'s ink is 11×11 (vs
   ▶'s 12×11), i.e. marginally SMALLER, because actualBoundingBox tracks the
   glyph's ink EXTENT, not its fill — an outlined triangle traces the same
   envelope as the filled one, just without the fill. The prior conclusion
   therefore still holds, if anything more comfortably: both ▷ and × fit the
   20×20 box easily; no override needed. Not dimmed with the rest of
   .queued's text (opacity:.62 above) — it's a live control, not part of the
   caption, and dimming it would make it read as disabled when it's the one
   button on a queued segment that's fully live. */
/* Multi-line collapse caret (run-progress-granularity, 2026-08-06 evening) —
   always #runbar's first child, mirrors .domexp (styles.css, domain bar
   expand triangle) glyph-for-glyph: same font-size/line-height/width/
   text-align/user-select, so the two triangles read as one column scanning
   down the page. width:14px + text-align:center matches .domexp's box
   exactly; the ALIGNMENT itself (where that 14px box sits relative to
   #runbar's own left edge) is the margin-left below, measured against a live
   .domexp in tmp/runbar-measure.html — see that harness and the fix's report
   for the numbers, and re-measure there if either .domexp's or #runbar's own
   padding/border ever changes. color:#000 (not .domexp's var(--amber)) is the
   same black-on-amber rule every other element on this strip follows — the
   inherited var(--txt) would be near-invisible on solid amber. Hidden
   (.hidden, JS-toggled) unless the bar actually needs more than one line —
   "no caret and no clamp" when everything already fits. */
.runbar-caret{color:#000;font-size:10px;line-height:1;width:14px;text-align:center;flex:none;user-select:none;cursor:pointer;background:transparent;border:0;padding:0;margin-left:-1px;opacity:.75}
.runbar-caret:hover{opacity:1}
.selbar b{font-family:var(--mono);background:rgba(122,162,255,.3);color:var(--blue);padding:4px 11px;border-radius:999px;font-size:12px;letter-spacing:.04em}
/* The action group is an abs-pos box pinned to the bar's right content edge
   (right:14px = the bar's own padding) with its LEFT edge set by fitSelbar()
   at 100px right of the Deselect button (owner 2026-08-05) — so the buttons
   get the full remaining span. The earlier left:50% model capped the box at
   half the bar (shrink-to-fit from the 50% inset) and wrapped it early with
   dead space on both sides. Buttons centre inside the span and wrap onto more
   lines as it narrows; the bar can't grow for an abs child, so fitSelbar()
   also sets the bar's min-height. Vertical centring stays transform-based. */
.selbar-center{position:absolute;top:50%;transform:translateY(-50%);right:14px;display:flex;flex-wrap:wrap;justify-content:center;gap:8px;align-items:center}
/* Action buttons: amber outline + text (default), red for Delete. Deselect keeps
   plain-btn text but carries the amber outline too. Icons inherit currentColor via .ic stroke. */
/* Selbar button palette (owner 2026-07-18 redo, hover included; icons follow
   currentColor via .ic stroke).
   DARK (solid ORANGE strip): flat transparent chips, black outline + black
   text; hover inverts to a solid black fill with amber text — Delete's hover
   fill goes red-text instead.
   LIGHT (dark-grey strip): transparent chips, white outline + white text;
   hover fills amber with black text — Delete idles red-outlined and hovers
   to a red fill with black text. */
.selbar .btn{background:transparent;transition:background .12s,color .12s,border-color .12s}
.selbar .btn.act,.selbar .btn.deselect,.selbar .btn.danger{border-color:#000;color:#000}
.selbar .btn.act:hover,.selbar .btn.deselect:hover{background:#000;border-color:#000;color:var(--amber)}
.selbar .btn.danger:hover{background:#000;border-color:#000;color:var(--red)}
/* Gate-unmet selbar action (Recrawl with no whole site selected): greyed via
   class + aria-disabled, NOT the disabled attr — disabled suppresses mouseover
   in most browsers, which would kill the explanatory data-tip (spec §5). The
   4-class hover selector outranks .selbar .btn.act:hover's fill. */
.selbar .btn.off{opacity:.45;cursor:default}
.selbar .btn.act.off:hover{background:transparent;border-color:#000;color:#000}
/* The Issues modal's blocked Re-scan (crawl guard, spec 2026-08-23 §6c) uses the
   same off+aria-disabled pattern; scoped to .mfoot because the selbar rule above
   doesn't reach modal feet (only rescanBtn toggles .off there today). */
.mfoot .btn.off{opacity:.45;cursor:default}
/* True-center the actions only while the centered group provably can't reach
   the Deselect cluster — decided by fitSelbar() in app.js, which MEASURES the
   rendered pixels and stamps .stacked when they'd collide. A width threshold
   (the old @container 940px here) can't do this job: the collision point moves
   with the LABEL LENGTHS (site-aware counts, 2026-08-05 — a 3-digit mixed
   selection collides at ~1690px; "Deselect 3 URLs" never does). Stacked, the
   group leaves the absolute overlay, right-aligns on its own row, and its own
   flex-wrap (base rule above) keeps it inside the bar at any width. */
.selbar.stacked{flex-wrap:wrap}
.selbar.stacked .selbar-center{position:static;transform:none;margin-left:auto}

/* activity panel */
.activity{width:340px;flex:none;background:var(--panel);border-left:1px solid var(--line);display:flex;flex-direction:column}
.act-resize{width:6px;cursor:col-resize;background:var(--panel2);flex:none}
.act-head{display:flex;align-items:center;gap:8px;padding:11px 13px;border-bottom:1px solid var(--line);flex:none}
.act-title{font-family:var(--disp);font-weight:700;font-size:14px}
.live{display:inline-flex;align-items:center;gap:5px;font-family:var(--mono);font-size:9px;letter-spacing:.12em;color:var(--green)}
.live .pdot{width:6px;height:6px;border-radius:50%;background:var(--green)}
.act-head .sp{margin-left:auto}
.act-log{flex:1;overflow:auto}
.aln{padding:8px 13px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent)}
.atop{display:flex;align-items:center;gap:8px}
.ats{font-family:var(--mono);font-size:11.5px;color:var(--mut)}
.atag{font-family:var(--mono);font-weight:600;font-size:11.5px}
.atag.info{color:var(--green)}.atag.alert{color:var(--amber)}.atag.error{color:var(--red)}.atag.tick{color:var(--blue)}
.amsg{font-size:12.5px;margin-top:3px;line-height:1.45}
.aurl{display:block;margin-top:4px;color:var(--blue);font-family:var(--mono);font-size:11px;text-decoration:none;word-break:break-all;cursor:pointer}

/* modal */
.backdrop{position:fixed;inset:0;background:rgba(0,0,0,.6);display:flex;align-items:center;justify-content:center;z-index:50;padding:18px}
.modal{background:var(--panel);border:1px solid var(--amber);border-radius:14px;box-shadow:0 24px 70px rgba(0,0,0,.6);width:680px;max-width:100%;max-height:92vh;display:flex;flex-direction:column;overflow:hidden}
.modal.wide{width:1000px}
.modal.full{width:100%;height:84vh}
.modal.huge{width:90vw;height:90vh;max-width:90vw;max-height:90vh}
.mhead{display:flex;align-items:center;gap:12px;padding:15px 18px;border-bottom:1px solid var(--line);flex:none}
.mtitle{font-family:var(--disp);font-weight:800;font-size:18px}
.mhead .sp{margin-left:auto}
.mx{cursor:pointer;color:var(--mut);font-size:20px;line-height:1;border:1px solid var(--line);border-radius:8px;padding:2px 9px;background:var(--panel2)}
.mbody{padding:8px 18px 18px;overflow:auto}
.mfoot{display:flex;align-items:center;gap:10px;padding:14px 18px;border-top:1px solid var(--line);flex:none}
.mfoot .sp{margin-left:auto}
.divider{height:1px;background:var(--dim);margin:18px 0 2px}
.sechead{display:flex;align-items:center;gap:9px;margin:16px 0 10px}
.secnum{width:22px;height:22px;border-radius:7px;background:var(--panel2);border:1px solid var(--line);display:grid;place-items:center;font-family:var(--mono);font-size:11px;color:var(--amber)}
.sectitle{font-family:var(--disp);font-weight:700;font-size:14px}
/* The chooser's section head carries a right-aligned action ("Add URLs"), so it
   needs the same spacer rule .ulhead/.mfoot/.mhead already have — .sp is only
   ever scoped, never global. */
.sechead .sp{margin-left:auto}
.frow{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:10px 0}
/* Scan progress stepper (spec 2026-08-08 §6) — lives under the URL field; the
   active step is amber, upcoming steps dimmed amber, ticks green. */
.scanstatus{margin:6px 0 0;font-size:12.5px;font-family:var(--mono);color:var(--amber)}
.scanstatus .dim{opacity:.45}
.scanstatus .tick{color:var(--green);margin-left:4px}
.scanstatus .fail{color:var(--red)}
.lbl{font-size:13px}.hint{font-size:13.5px;color:var(--mut)}
.inp{padding:3px 10px;border:1px solid var(--amber);border-radius:9px;background:var(--panel2);color:var(--txt);font-family:var(--mono);font-size:13px;width:100%}
.num-inp{width:64px;padding:2px 8px;border:1px solid var(--amber);border-radius:8px;background:var(--panel2);color:var(--txt);font-family:var(--mono);font-size:13px;text-align:center}
.filter:focus,.inp:focus,.num-inp:focus{outline:none;border-color:var(--amber2);box-shadow:0 0 0 3px rgba(255,180,84,.18)}
input[type="time"]{color-scheme:dark;color:var(--amber)}
input[type="time"]::-webkit-calendar-picker-indicator{filter:brightness(0) saturate(100%) invert(73%) sepia(42%) saturate(900%) hue-rotate(331deg) brightness(103%) contrast(101%);cursor:pointer}
.seg{display:flex;gap:8px;flex-wrap:wrap}
.seg .opt{flex:1;min-width:210px;border:1px solid var(--line);border-radius:10px;padding:11px 12px;cursor:pointer;background:var(--panel2);display:flex;gap:10px;align-items:flex-start}
.seg .opt.active{border-color:var(--amber);box-shadow:0 0 0 3px rgba(255,180,84,.12);background:rgba(255,180,84,.06)}
.seg .opt h4{font-family:var(--disp);font-size:13px;margin:0 0 3px}.seg .opt p{font-size:11px;color:var(--mut);margin:0;line-height:1.3}
.toggle2{display:inline-flex;border:1px solid var(--amber);border-radius:9px;overflow:hidden}
.toggle2 button{padding:0 16px;height:21px;line-height:21px;background:var(--panel2);border:0;color:var(--txt);font-size:12.5px;cursor:pointer}
.toggle2 button.active{background:linear-gradient(180deg,var(--amber),var(--amber2));color:#1a1206;font-weight:700;font-family:var(--disp)}
.checks-col{display:flex;flex-direction:column;gap:11px}
.checks-indent{margin-left:26px}
.mixedlist{margin:4px 0 0;padding-left:20px;list-style:disc}
.mixedlist li{font-family:var(--mono);word-break:break-all;margin:2px 0}
.vsep{color:var(--dim);margin:0 8px}
.section-off{opacity:.45;pointer-events:none}
/* Full-screen screenshot lightbox: scroll to zoom, move cursor to pan, click to close. */
.zoomoverlay{position:fixed;inset:0;z-index:400;background:rgba(0,0,0,.92);overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:zoom-out}
.zimg{max-width:100vw;max-height:100vh;transform-origin:center center;will-change:transform;user-select:none;-webkit-user-drag:none}
.chk.lblchk{font-size:13px;color:var(--txt)}
.ulhead{display:flex;align-items:center;gap:9px;margin:8px 0;flex-wrap:wrap}
.ulhead .count{font-family:var(--mono);font-size:11px;color:var(--mut)}
.ulhead .sp{margin-left:auto}
.linklike{color:var(--blue);font-size:11.5px;cursor:pointer;background:none;border:0;padding:0}
.linklike:hover{color:var(--amber)}
/* Add-URLs action on the URL-chooser head: orange, not the default blue
   .linklike — it sits among this modal's amber accents. Plain .linklike's hover
   already goes amber, so this needs its own hover or the state change vanishes. */
.linklike.addurls{color:var(--amber)}
.linklike.addurls:hover{color:var(--amber);text-decoration:underline}
/* Issues modal, DNS-problems section header (§1d, 2026-07-24 batch): the
   "View public DNS records" external link — orange like the rest of the
   modal's accents, not the default blue .linklike. */
.dnslink{color:var(--amber);font-size:11.5px;text-decoration:none;white-space:nowrap}
.dnslink:hover{text-decoration:underline}
.urltree{border:1px solid var(--line);border-radius:10px;height:240px;min-height:120px;max-height:55vh;overflow:auto;background:var(--panel2);resize:vertical}
.tnode{display:flex;align-items:center;gap:8px;padding:6px 11px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);font-family:var(--mono);font-size:12px}
.tnode:last-child{border-bottom:0}
/* Issues modal (findingsModal body): solid dividers between issue rows in the
   full --line color (#262a33 in dark — owner request 2026-07-21), not the faint
   60%-transparent default the URL-tree uses elsewhere. Color-only override, so
   the .tnode:last-child border-0 above still wins (it zeroes the width). */
.issuesbody .tnode{border-bottom-color:var(--line)}
/* Issues modal animated hide (§1b, 2026-07-24 batch): applied to a row/section
   node to roll it away instead of a full modal reload. The class only carries
   `overflow:hidden` + the `transition` timing — the FROM/TO opacity/max-height/
   padding numbers are set in JS (see rollupOut() in app.js), because a
   stylesheet rule can never win a specificity fight against an inline
   max-height the JS just pinned, so baking the end state (max-height:0) into
   this class would never animate. ~200ms, matches the brief. */
.rollup{overflow:hidden;transition:opacity .2s ease,max-height .2s ease,padding-top .2s ease,padding-bottom .2s ease}
.tnode.branch{background:rgba(127,127,127,.04)}
/* Capture attempts under their CHECK row (spec 2026-08-05). Indented from the
   CHECK pill so the grouping is readable without a connector; muted because the
   parent row carries the verdict that actually happened. */
.tnode.attempt{padding-left:34px;color:var(--mut);border-bottom-color:color-mix(in srgb,var(--line) 30%,transparent)}
.tchev{width:11px;flex:none;color:var(--mut);cursor:pointer;font-size:9px;text-align:center}
.tchev.leaf{visibility:hidden}
/* Domain expand/collapse: a plain amber triangle (▲ open / ▼ collapsed). */
.domexp{color:var(--amber);font-size:10px;line-height:1;width:14px;text-align:center;flex:none;user-select:none}
.texp{display:inline-grid;place-items:center;width:16px;height:16px;flex:none;border:1px solid var(--line);border-radius:4px;color:var(--mut);cursor:pointer;font-size:14px;line-height:1;user-select:none;font-family:var(--mono)}
.texp:hover{border-color:var(--amber);color:var(--amber)}
.texp.amber{border-color:var(--amber);color:var(--amber)}
.tname{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* Broken-link row: url + "see more" hug; the url truncates inside the wrap so
   "see more" stays right beside it, found/Hide pushed to the row's right. */
.urlwrap{flex:1;min-width:0;display:flex;align-items:center;gap:6px}
.urlwrap .tname{flex:0 1 auto}
a.tlink{color:var(--blue);text-decoration:none}
a.tlink:hover{text-decoration:underline}
.tsrc{font-family:var(--mono);font-size:9px;letter-spacing:.06em;padding:1px 6px;border-radius:999px;border:1px solid var(--line);color:var(--mut);flex:none}
.tsrc.crawl{color:var(--blue);border-color:rgba(122,162,255,.4)}
.tsrc.sitemap{color:var(--green);border-color:rgba(98,211,154,.4)}
.tsrc\+map,.tsrc.both{color:var(--amber);border-color:rgba(255,180,84,.4)}

/* screenshot viewer */
.cols{display:flex;gap:12px}
.shotcol{flex:1;min-width:0;display:flex;flex-direction:column}
/* position:relative so the CHANGE column's % (.shotmeta.pct, pulled out of flow
   below) anchors to the head; with the % out of flow the label is the only
   in-flow child and centers over the image (owner 2026-07-25: "CHANGE is not
   centered above the diff image"). */
/* min-height is LOAD-BEARING (2026-08-04): the BASELINE header holds a <select>
   (the baseline picker) while LATEST/CHANGE hold plain text, and a select is
   taller. Without a floor the three headers measured 23/13/12px and pushed the
   BASELINE frame 10px below its siblings — which breaks the one thing this modal
   exists for, comparing the three images side by side. Pin it to the tallest
   content so every frame starts at the same y. align-items:center (not baseline)
   for the same reason: a select has no useful baseline to share with a label. */
.shothead{position:relative;display:flex;align-items:center;justify-content:center;gap:8px;margin-bottom:7px;min-height:24px}
.shotlabel{font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--mut)}
.shotmeta{font-family:var(--mono);font-size:10.5px;color:var(--mut)}
/* min-width restores the reserved 60px box the global .pct rule used to give
   the viewer's DIFF meta (removed in the grid conversion) — keeps the DIFF
   header from shifting when a diff % appears/clears (review finding). */
/* Absolutely positioned (right of the head) so the fixed 60px %-box no longer
   offsets the centered CHANGE label. .shotmeta.pct is used ONLY on the CHANGE
   header, so this is scoped to it and never affects the BASELINE/LATEST heads. */
.shotmeta.pct{position:absolute;right:0;color:var(--amber);min-width:60px;text-align:right}
.shotframe{border:1px solid var(--line);border-radius:10px;height:54vh;overflow-x:hidden;overflow-y:auto;background:#0e0f13;position:relative;transition:opacity .32s ease}
/* Viewer baseline/accept fade: dim ONLY the three image frames (not the buttons,
   labels, zoom slider, dropdown or mask controls) while a recapture runs, then
   fade back in once the fresh images are ready (owner request 2026-07-19). */
.cols.dim .shotframe{opacity:.12}
/* Recheck fade skips the baseline (it doesn't change on a recheck) — dim only
   the non-baseline columns' frames (latest + diff). */
.cols.dim-nb .shotcol:not(.basecol) .shotframe{opacity:.12}
.shotframe img{width:100%;display:block;cursor:zoom-in}
/* No-change diff placeholder (status ok): centered in the frame both ways with
   a green checkmark prefix (owner request 2026-07-24). Scoped under .shotframe
   so it doesn't touch the generic .empty state used elsewhere in the app. */
.shotframe .empty.emptycenter{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:6px;padding:0}
.shotlens{position:absolute;border-radius:50%;border:2px solid var(--amber);box-shadow:0 6px 24px rgba(0,0,0,.55);background-repeat:no-repeat;background-color:#0e0f13;pointer-events:none;z-index:5}
/* Ignore-areas overlay on the BASELINE pane: absolute child of the scrolling
   .shotframe (scrolls with the image); aspect-ratio pins its height to the
   100%-width image so %-positioned rects survive zoom + resizes. */
.masklayer{position:absolute;top:0;left:0;width:100%;pointer-events:none;z-index:2}
.masklayer.editing{pointer-events:auto;cursor:crosshair}
.maskrect{position:absolute;border:1.5px dashed var(--amber);background:rgba(255,180,84,.14);pointer-events:none;box-sizing:border-box}
.maskrect.draft{border-style:solid}
.maskrect.editable{pointer-events:auto;cursor:move}
.maskgrip{position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:var(--amber);border:1px solid rgba(0,0,0,.5);border-radius:3px;cursor:nwse-resize;pointer-events:auto}
.maskdel{position:absolute;top:-9px;right:-9px;width:18px;height:18px;border-radius:50%;background:var(--red);color:#fff;font-size:12px;line-height:18px;text-align:center;cursor:pointer;pointer-events:auto}
.lenslabel{position:absolute;top:50%;transform:translateY(-50%);font-family:var(--mono);font-size:10px;background:rgba(0,0,0,.7);color:#fff;padding:1px 7px;border-radius:8px;white-space:nowrap;pointer-events:none}
.lenslabel.ll-right{right:0;transform:translate(50%,-50%)}
.lenslabel.ll-left{left:0;transform:translate(-50%,-50%)}
/* screenshot-modal nav: URL info box stacked above each prev/next button.
   The box WRAPS rather than truncating (owner 2026-07-25) — a cut-off URL is
   useless for deciding whether to jump there. It is omitted entirely when there
   is no prev/next candidate (built conditionally in app.js), so the two columns
   routinely differ in height; the BOTTOM-alignment that keeps both buttons on
   one baseline comes from `.shotmodal .mfoot{align-items:flex-end}` below —
   .navcol's own justify-content can't do it, because .mfoot doesn't stretch its
   children and a content-sized column has no free space to justify. */
.navcol{display:flex;flex-direction:column;gap:6px;min-width:0;max-width:46%}
.navcol.next{align-items:flex-end}
.navcol .btn{justify-content:center;white-space:nowrap}
.navurlbox{max-width:100%;font-family:var(--mono);font-size:11px;color:var(--mut);background:var(--panel2);border:1px solid var(--line);border-radius:7px;padding:4px 10px;white-space:normal;overflow-wrap:anywhere;line-height:1.35}
.navcol.next .navurlbox{text-align:right}
/* baseline-history dropdown under the BASELINE image in the screenshot viewer */
/* Lives IN the BASELINE column header since 2026-08-04 (it replaced the plain
   date text there — every option already carries its own timestamp), not under
   the frame. So: no top margin, no width:100%, and a max-width so a long option
   can't shove the "BASELINE" label out of the centred .shothead row. */
.basesel{max-width:min(60%,230px);font-family:var(--mono);font-size:11px;padding:3px 6px;border:1px solid var(--line);border-radius:7px;background:var(--panel2);color:var(--txt);cursor:pointer}
.basesel:hover{border-color:var(--amber)}
/* .hdrbtn shares the shotbtn outline+hover treatment (owner request 2026-07-24)
   for the viewer title-bar "URL settings"/"Domain settings" buttons — just not
   shotbtn's full-width stacked-column layout. */
.shotbtn,.hdrbtn{border-color:var(--dim)}
.shotbtn:hover,.hdrbtn:hover{border-color:var(--amber)}
.shotbtn{width:100%;justify-content:center;margin-bottom:7px}
/* Buttons that sit BELOW their image frame (Reset Baseline, Check for changes —
   owner 2026-07-25) need the 7px gap on top instead of underneath. */
/* flex:none is LOAD-BEARING (2026-08-04). `.below` is also the app-shell layout
   class at the top of this file — `.below{display:flex;flex:1;min-height:0}`,
   index.html's main+activity row — and this modifier collides with it by name,
   so the button inherited flex:1. Inside .shotcol (a column flex container)
   that made it GROW into whatever vertical space the column had spare: measured
   104px under LATEST, which has no controls below it, against 32px for the
   identical Reset Baseline button under BASELINE, whose column is full. Renaming
   this modifier would be the deeper fix; pinning flex here is the contained one. */
.shotbtn.below{margin-top:8px;margin-bottom:0;flex:none}
.zoomslider{accent-color:var(--amber);width:120px;cursor:pointer;vertical-align:middle}
/* Uniform button treatment across the WHOLE screenshot viewer (owner
   2026-07-25): light-grey outline like Reset Baseline, orange on hover, and
   disabled buttons excluded. The :not() guards are load-bearing, not cosmetic —
   these rules are 0-3-0 / 0-2-0-plus, out-specifying the 0-2-0 `.btn.busy`
   (transparent) and `.btn.fail` (red) borders and sitting later in the file, so
   WITHOUT :not(.busy):not(.fail) they would repaint a mid-flight button grey and
   kill the busy/fail feedback. :not(:disabled) on the hover rule is what keeps
   disabled buttons from going orange under the cursor. */
.shotmodal .btn:not(.busy):not(.fail){border-color:var(--dim)}
.shotmodal .btn:hover:not(:disabled):not(.busy):not(.fail){border-color:var(--amber)}
.shotmodal .btn:disabled:not(.busy):not(.fail),.shotmodal .btn:disabled:hover:not(.busy):not(.fail){border-color:var(--dim)}
/* .navcol is content-sized inside .mfoot, so bottom-alignment has to come from
   the footer itself (see the .navcol comment above). */
.shotmodal .mfoot{align-items:flex-end}

/* debug log */
.subbar{display:flex;align-items:center;gap:8px;padding:7px 16px;border-bottom:1px solid var(--line);flex:none}
.subbar .muted{font-family:var(--mono);font-size:10.5px;color:var(--mut)}
.buffered{font-family:var(--mono);font-size:10.5px;color:var(--amber);font-weight:600}
/* Fat (2px) outline in BOTH states — off pills carry the same border weight as active ones,
   so toggling never changes the pill's size (border-box). */
.tagfilter{font-family:var(--mono);font-size:10px;letter-spacing:.06em;padding:3px 9px;border-radius:999px;border:2px solid var(--line);background:var(--panel2);color:var(--mut);cursor:pointer;user-select:none;-webkit-user-select:none}
/* enabled (active) = SOLID FILL in the pill's colour with black text (2026-07-18; was a
   full-strength outline with coloured text). Shared with the debug-log tag pills so both pill
   rows speak one visual language. */
.tagfilter.active{color:#000}
.tagfilter.active.info,.tagfilter.active.ok{background:var(--green);border-color:var(--green)}
.tagfilter.active.alert,.tagfilter.active.changed{background:var(--amber);border-color:var(--amber)}
.tagfilter.active.error{background:var(--red);border-color:var(--red)}
.tagfilter.active.tick,.tagfilter.active.pending{background:var(--blue);border-color:var(--blue)}
/* disabled (off) = still-coloured (thinner) outline + bright white text */
.tagfilter.off{border-color:var(--dim);color:var(--txt)}
.tagfilter.off.info,.tagfilter.off.ok{border-color:rgba(98,211,154,.5)}
.tagfilter.off.alert,.tagfilter.off.changed{border-color:rgba(255,180,84,.5)}
.tagfilter.off.error{border-color:rgba(255,111,111,.5)}
.tagfilter.off.tick,.tagfilter.off.pending{border-color:rgba(122,162,255,.5)}
/* divider between "new alerts" and the status pills: fat + lighter grey than the pill borders.
   align-self:stretch (no fixed height) = exactly as tall as the pills, top/bottom aligned. */
.pillsep{width:2px;align-self:stretch;background:var(--dim);margin:0 3px;flex-shrink:0}
.tagfilter.newpill{color:var(--mut)}
.tagfilter.newpill.has{opacity:1;color:var(--amber);border-color:rgba(255,180,84,.45)}
.tagfilter.active.newpill{opacity:1;background:var(--amber);border-color:var(--amber);color:#000}
/* "New alert" highlight (2026-08-04) — painted by app.js ONLY while the
   new-alerts filter is on, on the pills that actually carry the new alert.
   Same outline -> SOLID-FILL grammar as .tagfilter.active.newpill directly
   above, but in each pill's OWN severity colour, so a new ERROR stays red and
   a new lorem badge stays magenta: the highlight never costs severity.
   Grouped by colour rather than by element, so a new pill family is one
   selector rather than a new block.
   color:#000 is repeated per rule, NOT hoisted into a bare `.isnew{color:#000}`
   — at 0-1-0 that loses to `.dot.error{color:var(--red)}` at 0-2-0 and paints
   red text on a red fill. Every selector here carries its element class and so
   outranks the base rule without !important. `.flagbadge.isnew.err`/`.warn`
   (0-3-0) outrank the magenta `.flagbadge.isnew` default (0-2-0) whatever the
   source order. There is deliberately NO rule for a muted (severity-less)
   pill: DNS suggestions are never alerts, so they physically cannot light up. */
.dot.isnew.changed,.statusbar .seg.isnew.changed,.pill.isnew.changed,.pill.isnew.warn,.flagbadge.isnew.warn{background:var(--amber);border-color:var(--amber);color:#000}
.dot.isnew.error,.statusbar .seg.isnew.error,.pill.isnew.error,.flagbadge.isnew.err{background:var(--red);border-color:var(--red);color:#000}
/* blocked (bot-wall verdict, v6) is a real alert kind — alerts.js emits a W
   key for it — unlike retrying, which never carries .isnew because it never
   emits an alert key at all. Same solid-fill grammar, --blocked's own tone. */
.dot.isnew.blocked,.statusbar .seg.isnew.blocked{background:var(--blocked);border-color:var(--blocked);color:#000}
.dot.isnew.gone,.statusbar .seg.isnew.gone{background:var(--gone);border-color:var(--gone);color:#000}
.dot.isnew.noanswer,.statusbar .seg.isnew.noanswer{background:var(--blocked);border-color:var(--blocked);color:#000}
.pill.isnew.flag,.flagbadge.isnew.flag{background:var(--mag);border-color:var(--mag);color:#000}
/* issues pill matches the status-pill family in BOTH states (off = tinted outline + txt text,
   like ok/changed/error/pending get theirs; it used to sit muted-grey and looked disabled-er
   than its siblings) */
.tagfilter.off.issuespill{border-color:rgba(255,43,181,.5)}
.tagfilter.active.issuespill{opacity:1;background:var(--mag);border-color:var(--mag);color:#000}
/* issues-category dropdown (▼ on the issues pill). The pill is split by a
   full-height divider: left segment = filter on/off, right segment = ▼ menu. */
.issuesmenuwrap,.labelsfilterwrap{position:relative;display:inline-flex}
.tagfilter.issuespill{padding:0;display:inline-flex;align-items:stretch;overflow:hidden}
.issuespill .pillmain{display:inline-flex;align-items:center;padding:3px 9px}
.issuespill .pilldiv{width:2px;align-self:stretch;flex-shrink:0;background:rgba(255,43,181,.5)}
.tagfilter.active.issuespill .pilldiv{background:rgba(0,0,0,.45)}
.pilltri{font-size:11px;line-height:1;user-select:none;opacity:.85;display:inline-flex;align-items:center;padding:3px 8px}
.pilltri:hover{opacity:1}
.issuespill .pillmain:hover,.issuespill .pilltri:hover{background:rgba(255,43,181,.12)}
.tagfilter.active.issuespill .pillmain:hover,.tagfilter.active.issuespill .pilltri:hover{background:rgba(0,0,0,.12)}
.issuecat-tools{display:flex;justify-content:space-between;gap:16px;padding:5px 9px 7px;border-bottom:1px solid var(--line);margin-bottom:3px}
/* labels: the domain bar's label BUTTON + its dropdown + the edit modal rows */
.labelwrap{position:relative;display:inline-flex}
/* The label control was a split pill beside the "N URLs" pill (2026-07-24); it
   became the FIRST button of the .domactions cluster on 2026-08-07 (owner), so
   the site's label NAMES now live only in the hover tip (labelTipText, app.js).
   .labelwrap stays and is load-bearing twice: position:relative anchors the
   dropdown (appended to anchor.parentElement) AND the document-click close
   handler keys on .closest('.labelwrap'). Measured at 28px inside the cluster —
   it costs the actions-track budget nothing. */
.labelbtn{position:relative;padding:5px 6px;display:inline-flex}
/* "Has labels" state: DIMMED amber, not full amber (owner 2026-08-07) — full
   amber is the hover/active accent everywhere else in the console, so a
   permanently-amber button would read as permanently-hovered. */
.labelbtn.has{color:color-mix(in srgb,var(--amber) 55%,var(--mut));border-color:color-mix(in srgb,var(--amber) 40%,var(--dim))}
.labelbtn:hover,.labelbtn.has:hover{border-color:var(--amber);color:var(--amber)}
/* Count badge. position:absolute is REQUIRED, not cosmetic: the button box must
   stay exactly 28x28 or the 7-button cluster overflows left over the ISSUES
   column (see the --actions-w measurement above). */
.labelbtn-n{position:absolute;top:-5px;right:-4px;font-family:var(--mono);font-size:9px;line-height:1;padding:1.5px 3px;border-radius:6px;background:var(--panel);border:1px solid currentColor;color:inherit}
/* right-aligned (2026-08-07): the button now sits at the RIGHT end of the bar,
   so the menu must grow LEFTWARD. Left-aligned (the .issuesdrop default) it
   would grow toward the viewport edge and a long label name would push it
   off-screen. */
/* .issuesdrop.labeldrop, not bare .labeldrop: the element carries BOTH classes,
   .issuesdrop{left:0} is declared LATER in this file, and at equal specificity
   source order wins — so a bare .labeldrop{left:auto} silently loses and the
   menu stays left-aligned (caught by measurement, not by eye). */
.issuesdrop.labeldrop{cursor:default;left:auto;right:0}
.labeltools{border-bottom:0;border-top:1px solid var(--line);margin:3px 0 0;padding:7px 9px 5px;justify-content:flex-end}
.labelinp{width:180px;padding:4px 8px;font-size:12.5px}
.labelrow{display:flex;gap:8px;align-items:center;margin-bottom:8px}
/* Issues modal: (i) source-context toggle + the copyable code box it opens */
.seemore{color:var(--amber);font-size:11.5px;cursor:pointer;user-select:none;white-space:nowrap;flex:none}
.seemore:hover{text-decoration:underline}
.ctxbox{flex-basis:100%;margin:4px 0 2px 2px;padding:7px 9px;background:var(--panel2);border:1px solid var(--line);border-radius:8px;font-family:var(--mono);font-size:11px;line-height:1.5;color:var(--txt);word-break:break-all;white-space:pre-wrap;user-select:text;cursor:text;max-height:130px;overflow:auto}
/* "Linked from …" / "Appears on N pages" line under an issue row. Full-width so
   it wraps below everything else in the flex row, like .tnode's found-line. */
.linkctx{flex-basis:100%;margin-top:2px;font-family:var(--body);font-size:11.5px}
.linkctx .tlink{text-decoration:underline}
/* The specific broken link's href, highlighted inside the "see more" snippet
   (and the matched lorem-ipsum run in its own "see more" box) — orange TEXT,
   not an amber background chip (§1a, 2026-07-24 batch: owner wanted it to read
   as a highlight, not another pill). The element is a <mark>, whose UA default
   stylesheet paints a yellow background + black text — `background:none` +
   `color` here defeat that default explicitly, dropping the chip look rather
   than swapping it for the browser's own yellow highlight. */
.ctxhi{background:none;color:var(--amber);font-weight:600}
/* Issues modal, DNS problem rows (§1e, 2026-07-24 batch): a little extra
   breathing room between the amber title line (record name/DRIFT/EMAIL-AUTH
   tag) and the detail sub-line below it — the default .hint children stack
   with no gap. */
/* Breathing room between a finding's amber title and its first description
   line. Named .dnsdetail until 2026-08-04, when the same spacing was applied
   to the five sections that never had it (malformed link, lorem, mixed
   content, broken links, HTTP-404 rows) — the name no longer described it. */
.findingdetail{margin-top:4px;line-height:1.55}
.issuesdrop{position:absolute;top:calc(100% + 6px);left:0;z-index:60;background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:6px;box-shadow:0 14px 34px -10px rgba(0,0,0,.7);display:flex;flex-direction:column;gap:1px;min-width:200px}
/* Page issues | site issues, side by side (owner 2026-08-09). The two columns
   size to their own content — the rows are white-space:nowrap, so neither
   column can squeeze the other into wrapping. */
.issuecat-cols{display:flex;align-items:flex-start;gap:14px}
.issuecat-col{display:flex;flex-direction:column;gap:1px;flex:0 0 auto}
.issuecat{display:flex;gap:9px;align-items:center;padding:6px 9px;border-radius:7px;cursor:pointer;font-size:12.5px;color:var(--txt);white-space:nowrap;user-select:none}
.issuecat:hover{background:var(--panel2)}
.issuecat .box{width:15px;height:15px;border-radius:5px}
.issuecat .box svg{width:10px;height:10px}
.log{flex:1;overflow:auto;padding:10px 16px;background:#0b0c10;font-family:var(--mono);font-size:12px}
/* Bottom-of-list affordance for the Debug tab's infinite scroll: "load older",
   "loading…", or "— end of log —". Lives INSIDE .log so it scrolls with the
   lines and sits directly under the oldest one. */
.logmore{display:flex;justify-content:center;padding:12px 0 4px;border-top:1px solid var(--line);margin-top:6px}
.logmore .muted{font-size:11.5px}
.ln{display:flex;gap:9px;padding:1px 0;line-height:1.65}
.ln .lts{color:var(--mut);flex:none}
.ln .ltag{flex:none;font-weight:600;width:54px}
.ln .ltag.info{color:var(--green)}.ln .ltag.alert{color:var(--amber)}.ln .ltag.error{color:var(--red)}.ln .ltag.tick{color:var(--blue)}
.ln .lmsg{color:var(--txt);white-space:pre-wrap}
.ln .lurl{color:var(--blue);cursor:pointer}

/* login gate */
.gate{position:fixed;inset:0;background:var(--bg);display:flex;align-items:center;justify-content:center;z-index:100}
.gatecard{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:30px;width:330px;text-align:center}
.gatecard .brand{font-size:26px;display:block;margin-bottom:6px}
.gatecard input{width:100%;margin:14px 0 10px}
.gateerr{color:var(--red);font-size:12px;min-height:16px}

/* toolbar identity chip (user email + org name) */

/* members modal */
.memberrow{display:flex;align-items:center;gap:10px;padding:9px 0;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent)}
.memberrow:last-child{border-bottom:0}
.memberrow .memberemail{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--mono);font-size:12.5px}
.memberrow .memberrole{font-family:var(--mono);font-size:10.5px;color:var(--mut);text-transform:uppercase;letter-spacing:.05em}
.memberrow select.roleselect{font-family:var(--mono);font-size:11.5px;padding:4px 7px;border:1px solid var(--line);border-radius:6px;background:var(--panel2);color:var(--txt)}
.pill.pending-invite{background:rgba(122,162,255,.14);color:var(--blue);border-color:rgba(122,162,255,.35)}
.memberserr{color:var(--red);font-size:12px;min-height:16px;margin-top:6px}
.membersection{margin-top:16px}
.membersection:first-child{margin-top:0}
.empty{color:var(--mut);text-align:center;padding:40px;font-size:13px}
.discover-err{color:var(--red);padding:13px;font-size:12.5px;line-height:1.5}
.loading{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:40px}
.spinner{width:34px;height:34px;border:3px solid var(--line);border-top-color:var(--amber);border-radius:50%;animation:spin .8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.loadtext{color:var(--amber);font-size:13px;text-align:center}
/* Uptime pill: matches the ISSUES pills (.pill) — un-bolded, same 3px/9px
   padding, same tint recipe (rgba(...,.14) fill / rgba(...,.35) border) — so the
   STATE/CHANGES/ISSUES columns read as one pill family (owner 2026-07-25). */
.uptime-badge{font-family:var(--mono);font-size:var(--pill-font,11px);padding:calc(3px*var(--pill-scale,1)) calc(9px*var(--pill-scale,1));border-radius:999px;white-space:nowrap}
.uptime-badge.up{background:rgba(98,211,154,.14);color:var(--green);border:1px solid rgba(98,211,154,.35)}
.uptime-badge.down{background:rgba(255,111,111,.14);color:var(--red);border:1px solid rgba(255,111,111,.35)}
.uptime-badge.slow{background:rgba(255,180,84,.14);color:var(--amber);border:1px solid rgba(255,180,84,.35)}
.uptime-badge.pending,.uptime-badge.ok{background:var(--panel2);color:var(--mut);border:1px solid var(--dim)}
/* Recrawl churn markers (2026-07-22): auto-added (bold url + blue pill, 7 days)
   and auto-removed tombstone (greyed url + grey pill, read-only until 7-day purge).
   Same recipe as .dot/.scanpill (owner 2026-08-07 — was 9.5px 600-weight /
   2px 8px / 999px, the last URL-row holdout from before the .dot restyle), so
   the STATE pill reads as a sibling of the status pill two tracks over; the
   .added/.removed variants below keep supplying the border colour. */
.autopill{display:inline-flex;align-items:center;font-size:var(--pill-font,11px);font-family:var(--mono);letter-spacing:.06em;white-space:nowrap;padding:calc(1px*var(--pill-scale,1)) calc(6px*var(--pill-scale,1));border-radius:6px}
.autopill.added{background:rgba(122,162,255,.14);color:var(--blue);border:1px solid rgba(122,162,255,.4)}
.autopill.removed{background:rgba(150,150,140,.14);color:var(--mut);border:1px solid rgba(150,150,140,.4)}
.url.autoadded{font-weight:700;color:var(--txt)}
.url.autoremoved{color:var(--mut)}
/* pointer-events stays ON (owner 2026-08-04): the tip on a disabled action
   button is what EXPLAINS why it's dead ("URL was auto-removed — …"), and
   pointer-events:none suppressed it. Safe: ib() renders a disabled button as a
   <span> with no click handler at all, and no ancestor of .rowicons has one. */
.iconbtn.disabled{opacity:.35;cursor:default}
.sparkwrap{position:relative;border:1px solid var(--line);border-radius:8px;background:var(--panel2);margin:8px 0}
.sparksvg{width:100%;height:48px;display:block}
.sparkguide{position:absolute;top:0;height:48px;width:1px;background:var(--dim);opacity:0;pointer-events:none;transition:opacity .08s}
.sparkdot{position:absolute;width:8px;height:8px;border-radius:50%;background:var(--amber);transform:translate(-50%,-50%);pointer-events:none;box-shadow:0 0 0 2px var(--panel2),0 0 0 4px rgba(255,180,84,.25)}
.sparktip{position:absolute;top:52px;transform:translateX(-50%);z-index:10;pointer-events:auto;user-select:text;width:238px;background:var(--panel);border:1px solid var(--amber);border-radius:9px;padding:8px 11px;box-shadow:0 0 0 1px rgba(255,180,84,.15),0 14px 34px -10px rgba(0,0,0,.7);white-space:nowrap;transition:opacity .08s}
.sparktip .stt-url{font-family:var(--mono);font-size:10.5px;color:var(--mut);overflow:hidden;text-overflow:ellipsis}
.sparktip .stt-time{font-size:12px;margin:2px 0 6px;overflow:hidden;text-overflow:ellipsis}
.sparktip .stt-row{font-family:var(--mono);font-size:11px;color:var(--mut);line-height:1.55;overflow:hidden;text-overflow:ellipsis}
.sparktip .stt-row b{color:var(--txt);font-weight:400}

/* ---------- billing: plan-state banners (Phase 2E Task 13) ---------- */
.banners{display:flex;flex-direction:column;flex:none}
.banner{display:flex;align-items:center;gap:10px;padding:9px 16px;font-size:12.5px;border-bottom:1px solid var(--line)}
.banner .bmsg{flex:1}
.banner .btn{padding:5px 10px;font-size:11.5px}
.banner.warn{background:rgba(255,180,84,.12);color:var(--amber)}
.banner.danger{background:rgba(255,111,111,.12);color:var(--red)}
.banner.info{background:rgba(122,162,255,.12);color:var(--blue)}
.banner .bx{cursor:pointer;padding:0 4px;color:inherit;opacity:.7;font-size:15px;line-height:1}
.banner .bx:hover{opacity:1}

/* ---------- billing page ---------- */
.usagebar{margin:10px 0}
.usagebar-head{display:flex;justify-content:space-between;font-size:12px;color:var(--mut);margin-bottom:5px}
.usagebar-head b{color:var(--txt);font-weight:600}
.usagebar-track{height:8px;border-radius:999px;background:var(--panel2);border:1px solid var(--line);overflow:hidden}
.usagebar-fill{height:100%;border-radius:999px;background:linear-gradient(90deg,var(--amber2),var(--amber));transition:width .3s ease}
.usagebar-fill.over{background:linear-gradient(90deg,var(--red),#ff9a9a)}
.billing-plancard{display:flex;align-items:center;gap:14px;padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel2);margin-bottom:14px}
.billing-planname{font-family:var(--disp);font-weight:800;font-size:19px;text-transform:capitalize}
.billing-status{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.08em;padding:3px 9px;border-radius:999px;background:var(--panel);border:1px solid var(--line);color:var(--mut)}
.billing-status.active,.billing-status.comped{color:var(--green);border-color:rgba(98,211,154,.35)}
.billing-status.past_due{color:var(--red);border-color:rgba(255,111,111,.35)}
.billing-status.canceled{color:var(--mut)}

/* ---------- pricing grid ---------- */
.pricing-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin:14px 0}
.pricing-card{border:1px solid var(--line);border-radius:11px;padding:14px 12px;background:var(--panel2);display:flex;flex-direction:column;gap:8px}
.pricing-card.current{border-color:var(--amber);box-shadow:0 0 0 1px var(--amber) inset}
.pricing-tier{font-family:var(--disp);font-weight:800;font-size:15px;text-transform:capitalize}
.pricing-caps{font-size:11.5px;color:var(--mut);line-height:1.6;flex:1}
.pricing-feats{display:flex;flex-wrap:wrap;gap:4px}
.pricing-feats .pill{font-size:9.5px;padding:2px 7px}
.pricing-feats .pill.off{opacity:.35}
.pricing-card .btn{width:100%;justify-content:center;margin-top:4px}

/* ---------- comp-admin (operator-only) ---------- */
.comp-form{border-top:1px solid var(--line);margin-top:16px;padding-top:14px}

/* ---------- admin: operator orgs overview (all tenants) ---------- */
/* Tabbed nav, mimicking skriinsjot.com/admin (owner 2026-07-25): underline tabs,
   NOT the .toggle2 pill capsule — a flex row on a 1px bottom rule; the active tab
   is amber text + a 2px amber underline (margin-bottom:-1px overlaps the rule),
   inactive tabs are muted uppercase mono. */
.admintabs{display:flex;gap:4px;flex-wrap:wrap;border-bottom:1px solid var(--line);margin:0 0 14px}
.admintabs button{font-family:var(--mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;font-weight:600;color:var(--mut);background:transparent;border:0;border-bottom:2px solid transparent;padding:11px 16px;margin-bottom:-1px;cursor:pointer;transition:.15s;white-space:nowrap}
.admintabs button:hover{color:var(--txt)}
.admintabs button.active{color:var(--amber);border-bottom-color:var(--amber)}
.admintbl-wrap{overflow:auto;max-height:64vh;border:1px solid var(--line);border-radius:10px}
/* Debug-log tab (owner 2026-07-25). .log's own `flex:1` only produced a
   scrolling viewport when its parent was the `.modal.full` flex column; inside
   .admin-tabbody (a plain block div, and .mbody has no flex:1 either) it is
   inert, so the log would render at full content height and push the filter
   strip out of view while .mbody scrolls. Cap it explicitly instead. */
.admin-tabbody .log{max-height:62vh;border:1px solid var(--line);border-radius:10px}
.admin-tabbody .subbar{padding-left:0;padding-right:0}
/* Reports tab (Task 7, §6.3): the .huge admin modal (90vh) has room the
   tenants tab doesn't use — .tall gives Reports a taller cap of its own
   without touching the 64vh default above (tenants tab unaffected). */
.admintbl-wrap.tall{max-height:72vh}
.admintbl{width:100%;border-collapse:collapse;font-size:12.5px}
.admintbl th,.admintbl td{text-align:left;padding:8px 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);vertical-align:top}
.admintbl thead th{position:sticky;top:0;z-index:1;background:var(--panel2);font-family:var(--disp);font-weight:700;font-size:10.5px;text-transform:uppercase;letter-spacing:.05em;color:var(--mut)}
.admintbl tbody tr:not(.admin-editrow):hover{background:color-mix(in srgb,var(--amber) 6%,transparent)}
.admintbl tfoot td{font-weight:700;border-top:2px solid var(--line);background:var(--panel2)}
.admintbl td.admin-num{text-align:right;font-family:var(--mono)}
.admin-org-name{font-weight:600}
.admin-org-slug{font-family:var(--mono);font-size:11px;color:var(--mut)}
.admin-member{font-family:var(--mono);font-size:11.5px;line-height:1.55;word-break:break-all}
.admin-sub{font-size:11px;color:var(--mut);margin-top:3px}
.admin-copyid{cursor:pointer;font-size:11px;color:var(--blue);background:none;border:0;padding:0;margin-top:3px;display:inline-block}
.admin-actions{display:flex;gap:6px;flex-wrap:wrap}
.admin-actions .btn{padding:4px 9px;font-size:11.5px}
.admin-editrow td{background:color-mix(in srgb,var(--amber) 4%,transparent)}

/* ---------- zone import ---------- */
.zonebox{margin-top:4px;border:1px solid var(--line);border-radius:10px;background:var(--panel2);padding:12px}
.zonetext{width:100%;min-height:180px;resize:vertical;border:1px solid var(--amber);border-radius:8px;background:var(--bg);color:var(--txt);font-family:var(--mono);font-size:12px;line-height:1.55;padding:10px 12px;white-space:pre;overflow:auto}
/* Line-number gutter beside the zone editor: identical font metrics + a 1px
   transparent border so row 1 aligns with the textarea's first text row
   (border+padding offsets match); scroll is synced from the textarea. */
.zoneed{display:flex;align-items:stretch;gap:6px}
.zoneed .zonetext{flex:1;width:auto;min-width:0}
/* Bottom slack (30px vs the textarea's 10px): the wrap=off textarea grows a
   horizontal scrollbar on long lines (DKIM TXT), shrinking its clientHeight —
   without matching slack the gutter's scrollTop clamps ~one row short at the
   bottom and the numbers sit beside the wrong lines (review finding). */
.zonegutter{flex:none;min-width:30px;text-align:right;padding:10px 2px 30px 0;border:1px solid transparent;font-family:var(--mono);font-size:12px;line-height:1.55;color:var(--dim);white-space:pre;overflow:hidden;user-select:none;-webkit-user-select:none}
.zonetext:focus{outline:none;border-color:var(--amber2);box-shadow:0 0 0 3px rgba(255,180,84,.18)}
.zactions{display:flex;align-items:center;gap:8px;margin-top:10px;flex-wrap:wrap}
.zactions .sp{margin-left:auto}
.zcap{font-size:11.5px;color:var(--dim);font-family:var(--mono)}
.zsum{display:flex;gap:8px;flex-wrap:wrap;margin:2px 0 10px}
.zrej{font-family:var(--mono);font-size:12px;color:var(--red);background:rgba(255,111,111,.08);border:1px solid rgba(255,111,111,.3);border-radius:6px;padding:6px 9px;margin:5px 0}
.zrej .zfix{display:block;color:var(--mut);margin-top:3px;font-family:var(--body);font-size:12.5px}
.zonecard{margin-top:10px;border:1px solid rgba(98,211,154,.4);border-radius:10px;background:var(--panel2);padding:12px}
.zonecard.drifting{border-color:rgba(255,111,111,.45)}

/* Instant hover tip (tooltip.js) — replaces the native `title`, whose ~1s delay
   is browser-controlled and can't be tuned. Hidden by OPACITY, never
   display:none, so tipPlacement can measure the box before positioning it.
   position:fixed and a child of <body>, never of .main (a container-type
   element is the containing block for its fixed descendants). z-index sits
   above .modal so a tip over a modal's own buttons isn't painted behind it. */
/* white-space:pre-line — the label button's tip is "labels:" + ONE NAME PER LINE
   (owner 2026-08-07) and show() assigns via textContent, so without this every
   \n collapses to a space and the whole list renders on one line. pre-line, not
   pre/pre-wrap: it honours the newlines while still collapsing incidental
   spaces and still wrapping a long name inside max-width. */
.tip{position:fixed;left:0;top:0;z-index:400;max-width:260px;padding:5px 9px;border-radius:7px;white-space:pre-line;
  background:var(--panel2);border:1px solid var(--line);color:var(--txt);
  font-size:11.5px;line-height:1.35;box-shadow:0 8px 22px rgba(0,0,0,.55);
  pointer-events:none;opacity:0;transition:opacity .08s}
.tip.on{opacity:1}
/* Tips whose text is user CONTENT rather than a control description — today
   only the domain bar's label list — match the size that content has in its own
   editor: .inp in the label modal is 13px (owner 2026-08-09). */
.tip.tip-lg{font-size:13px}

/* ---------- UI density panel (floating, draggable; spec 2026-08-07) ----------
   Deliberately NOT a modal: no .backdrop (which dims + click-closes + gates
   the 30 s fallback poll — the preview being tuned must stay live), child of
   <body> (never .main, whose container-type hijacks position:fixed — the .tip
   rule), z-index 40 = above the sticky bars (5) and the lifted label-dropdown
   bar (6), BELOW .backdrop (50) so a real modal still covers it, and far
   below .tip (400) / toast (200). */
.uipanel{position:fixed;z-index:40;width:300px;max-height:80vh;overflow:auto;background:var(--panel);border:1px solid var(--dim);border-radius:11px;box-shadow:0 10px 30px rgba(0,0,0,.55)}
.uipanel-head{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line);cursor:move;user-select:none;position:sticky;top:0;background:var(--panel);z-index:1}
.uipanel-title{font-family:var(--disp);font-weight:700;font-size:13.5px}
.uipanel-head .sp,.uipanel-foot .sp{margin-left:auto}
.uipanel-body{padding:10px 12px;display:flex;flex-direction:column;gap:8px}
.uirow{display:grid;grid-template-columns:112px 1fr 52px;align-items:center;gap:8px;font-size:12px;color:var(--mut);cursor:default}
.uirow input[type=range]{width:100%;margin:0;accent-color:var(--amber)}
.uirow input[type=range]:disabled{opacity:.35}
.uilbl{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.uival{font-family:var(--mono);font-size:11px;color:var(--txt);text-align:right;white-space:nowrap}
.uipanel select{background:var(--panel2);color:var(--txt);border:1px solid var(--dim);border-radius:7px;padding:3px 6px;font-size:12px}
.uipanel-foot{display:flex;gap:8px;padding:10px 12px;border-top:1px solid var(--line)}
