
/* A flashed ROW keeps the same persistent green accent a flashed CARD gets, so the
   row you followed a link to stays findable after the animation ends. */
tr.flash-card>td:first-child{border-left:3px solid var(--green-soft)}
.lv-flash-missing{color:var(--muted);font-size:.9rem;margin:.5rem 0}
/* Optional result-scoped header slot (ListView.Header), inside the swappable root. */
.lv-header{margin:0 0 .8rem}
/* Opt-in per-row expandable detail row (RowDetailKey) — a full-width line under a row,
   e.g. a day-context strip. In table mode it's a colspan strip; in the stacked-card
   mobile/dynamic layouts it attaches under the row's card (pulled up, no separate box). */
/* The house table look, SCOPED to the list. These values are the same ones
   TableComponentCSS puts on bare table/td/th via BaseCSS — repeated here
   scoped to .lv-table so a ListView looks like the house list even in a plug that
   carries its own palette instead of BaseCSS. plug-drugs was exactly that case:
   /compare had every affordance (search, sort, column menu, bulk actions) and
   still did not look like plan.zm.is, because td/th fell back to browser defaults
   — 1px padding, no rule between rows, no header treatment. A list component that
   only looks right when some OTHER stylesheet is present is not self-contained.
   TestListViewTableStylingMatchesTableComponent pins the two together so they
   cannot drift.

   :where() is load-bearing, not decoration: it contributes ZERO specificity, so
   these act as DEFAULTS that any existing rule still beats. Written as plain
   .lv-table th the rule scores (0,1,1) and would override .lv-selcol /
   .lv-colmenu-col / .lv-expcol (0,1,0), silently widening the checkbox and
   column-menu cells in every list in the mesh. Measured: 12.8px where the house
   list is 4/8px. With :where() they score (0,0,1) — the same as the generic
   BaseCSS rule they stand in for — so the per-column rules keep winning and a
   plug that loads BaseCSS sees no change at all. */
:where(.lv-table){border-collapse:separate;border-spacing:0;width:100%;background:var(--panel);border:1px solid var(--border);overflow:hidden}
:where(.lv-table) td,:where(.lv-table) th{padding:.62rem .8rem;text-align:left;font-size:.86rem;border-bottom:1px solid var(--border)}
:where(.lv-table) th{background:var(--th-bg);color:var(--text-hi);font-weight:600;font-size:.72rem;text-transform:uppercase;letter-spacing:.06em}
:where(.lv-table) tbody tr:last-child td{border-bottom:none}
:where(.lv-table) tbody tr{transition:background .12s ease}
:where(.lv-table) tbody tr:hover{background:var(--row-hover)}
tr.lv-detail>td{background:var(--panel);padding:.2rem .8rem .4rem}
/* See the Render comment: a definite width here keeps a wide panel child from
   re-sizing the entire table, and gives descendants something to overflow within.
   min-width:0 lets flex/grid children actually shrink rather than refusing below
   their content. */
.lv-detail-inner{width:1px;min-width:100%;min-height:0;box-sizing:border-box}
.lv-detail-inner>*{min-width:0;max-width:100%}
.lv-expcol{width:1%;white-space:nowrap}
/* Geometry copied verbatim from .lv-details-btn: the chevron and the ⓘ are peer row
   controls, so one relationship gets one number rather than two near-identical ones. */
.lv-exp-btn{display:inline-flex;align-items:center;justify-content:center;background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;min-height:1.9rem;padding:.15rem .55rem;font-size:.95rem;line-height:1;box-sizing:border-box;vertical-align:middle}
.lv-exp-btn:hover{color:var(--text);border-color:var(--accent,#2ea043)}
.lv-exp-chev{display:inline-block;transition:transform .12s ease}
.lv-exp-btn[aria-expanded="true"] .lv-exp-chev{transform:rotate(90deg)}
@media (prefers-reduced-motion:reduce){.lv-exp-chev{transition:none}}
/* Stacked-card layouts (mobile media query + the dynamic .lv-cards collapse) turn the
   detail row into a block attached under the row's card. On a real desktop table the cell
   stays a table-cell so its colspan spans the full width — do NOT force display:block there. */
.lv-cards tbody tr.lv-detail{margin-top:-.75rem;border-top:none;box-shadow:none;padding:0}
.lv-cards tbody tr.lv-detail>td{display:block;text-align:left;border-bottom:none}
/* In a stacked card the chevron cell is already a block, so collapsing an EMPTY one
   (a row with no panel) costs no table alignment — unlike on a real table, where
   display:none on a <td> would shift the whole row. Hence the stacked-only scope. */
.lv-cards tbody td.lv-expcol:empty{display:none}
.lv-cards tbody td.lv-expcol{border-bottom:none;padding-bottom:0}
@media(max-width:640px){
  .tbl-stack tbody tr.lv-detail{margin-top:-.75rem;border-top:none;box-shadow:none;padding:0}
  .tbl-stack tbody tr.lv-detail>td{display:block;text-align:left;border-bottom:none}
  .tbl-stack tbody td.lv-expcol:empty{display:none}
  .tbl-stack tbody td.lv-expcol{border-bottom:none;padding-bottom:0}
}
/* Opt-in (ListView.LoadingBar) indeterminate loading bar: invisible until htmx adds
   .htmx-request to it for the duration of a search/sort/page fetch, then a primary
   segment sweeps across. Pure CSS, no JS. */
.lv-loadbar{position:relative;height:3px;margin:0 0 .55rem;overflow:hidden;background:rgba(127,127,127,.16);opacity:0;transition:opacity .15s ease}
.lv-loadbar.htmx-request{opacity:1}
.lv-loadbar>span{position:absolute;top:0;left:0;height:100%;width:35%;background:var(--primary)}
.lv-loadbar.htmx-request>span{animation:lv-loadbar 1.05s cubic-bezier(.45,0,.2,1) infinite}
@keyframes lv-loadbar{0%{transform:translateX(-130%)}100%{transform:translateX(330%)}}
@media (prefers-reduced-motion:reduce){.lv-loadbar.htmx-request>span{animation-duration:2.4s}}
/* Opt-in filter chip-bar (ListView.Facet) — the shared pill-switcher (matches the
   plug-dash/alpha .dash-filter-chip look): a wrapping row of rounded chips, the
   active one highlighted. Sits above the search/count controls. */
.lv-facet{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);margin:.1rem 0 .7rem;align-items:center}
.lv-facet-chip{font-size:.78rem;height:1.85rem;box-sizing:border-box;padding:0 .75rem;border:1px solid var(--border,#30363d);background:var(--card,#161b22);color:var(--muted,#8b949e);border-radius:var(--r-pill);display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);cursor:pointer;user-select:none;white-space:nowrap;text-decoration:none}
.lv-facet-chip:hover{color:var(--text-hi,#f0f6fc)}
.lv-facet-chip.active,.lv-facet-chip[aria-pressed="true"]{background:rgba(88,166,255,.18);border-color:var(--border,#30363d);color:var(--text-hi,#f0f6fc);font-weight:650;box-shadow:0 1px 2px rgba(0,0,0,.12)}
.lv-facet-chip.active:hover,.lv-facet-chip[aria-pressed="true"]:hover{background:rgba(88,166,255,.24);color:var(--text-hi,#f0f6fc)}
.lv-inline-cols{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);margin:.1rem 0 .7rem;align-items:center}
.lv-inline-label{font-size:.78rem;color:var(--muted);font-weight:600;margin-right:.1rem}
/* Inline list title: sits on the controls row's baseline, level with the count on
   the right. The top margin keeps breathing room above so it isn't cramped. */
.lv-title{margin:.9rem 0 0;font-size:1.05rem;line-height:1.2;font-weight:600;color:var(--text-hi)}

/* The MODULE the controls sit in. ListView wraps its controls row in this, which
   is where the row's top/bottom spacing comes from — so a filter rendered
   without it sat flush against whatever was above and looked subtly unlike every
   list in the fleet. Shared, so a filter bar outside a ListView is the same
   component rather than an imitation of it. */
.listview{margin:1rem 0}
/* The row the field sits in. Shared with ListView so a filter above a card grid
   and a filter above a table have the same gap and the same baseline. */
.lv-controls{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);align-items:center;margin-bottom:.75rem}
/* flex-grow with a 100% basis so the form spans the whole controls row: the input
   takes every spare pixel and the square submit button is pushed to the right edge,
   lining up with the edge of the content below. */
.lv-search{display:flex;gap:var(--sp-2,.5rem);align-items:stretch;flex:1 1 100%;min-width:12rem}
/* The search box must adopt the host's dark theme, not the browser default white.
   Theme tokens with dark fallbacks so it's correct even on a plug that doesn't
   define the full token set (e.g. docs). */
.lv-search input[type=search]{flex:1;margin:0;height:2.4rem;box-sizing:border-box;padding:0 .6rem;background:var(--panel2,#0d1117);color:var(--text-hi,#c9d1d9);border:1px solid var(--border2,#30363d)}
.lv-search input[type=search]::placeholder{color:var(--faint,#6e7681)}
.lv-search input[type=search]:focus{outline:none;border-color:var(--primary,#1f6feb)}
/* Square: equal sides, so the search control matches the delete buttons instead of
   being a rectangle with rounded corners. Width AND height from the same token the
   input beside it uses, so the pair stays flush. */
.lv-search button{height:var(--control-h);width:var(--control-h);margin:0}
.lv-clear{font-size:.8rem;color:var(--muted);white-space:nowrap;align-self:center}

/* Mobile "Sort" chip row: only rendered for MobileStack lists (whose <thead> —
   and its sort links — is clipped on narrow screens). Hidden on desktop; shown
   at the same ≤640px breakpoint the stacked-card layout kicks in. */
/* Mobile SORT dropdown: an icon summary that expands a <details> menu of fields.
   Hidden on desktop (the header sort links are used); shown ≤640px where the
   header is clipped. Styling mirrors the column menu (.lv-cols). */
.lv-msort{position:relative;display:none}
@media(max-width:640px){.lv-msort{display:inline-block}}
.lv-msort>summary{cursor:pointer;list-style:none;height:2.4rem;display:inline-flex;align-items:center;gap:var(--sp-1,.25rem);padding:0 .7rem;border:1px solid var(--border);background:var(--panel);color:var(--text);font-size:.8rem;user-select:none}
.lv-msort>summary::-webkit-details-marker{display:none}
.lv-msort-cur{color:var(--muted);white-space:nowrap}
/* Anchor RIGHT: the sort chip sits at the RIGHT end of the controls line (next to
   the results count), so a left:0 menu extended off the RIGHT edge of the screen —
   clipping every option label. right:0 opens the ~11rem menu leftward from the
   chip's right edge, keeping it on-screen; max-width guards a narrow viewport. */
.lv-msort-menu{position:absolute;z-index:30;top:2.4rem;right:0;left:auto;min-width:11rem;max-width:calc(100vw - 2rem);display:flex;flex-direction:column;gap:var(--sp-0,.15rem);padding:.4rem;border:1px solid var(--border);background:var(--panel);box-shadow:0 6px 18px rgba(0,0,0,.4)}
.lv-msort-opt{display:flex;align-items:center;justify-content:space-between;gap:var(--sp-2,.5rem);padding:.42rem .55rem;font-size:.85rem;color:var(--text);text-decoration:none;white-space:nowrap}
.lv-msort-opt:hover{background:var(--panel2)}
.lv-msort-opt.is-active{color:var(--green-soft)}
.lv-cols{position:relative}
.lv-cols>summary{cursor:pointer;list-style:none;height:2.4rem;display:inline-flex;align-items:center;padding:0 .8rem;border:1px solid var(--border);background:var(--panel);font-size:.85rem;user-select:none}
.lv-cols>summary::-webkit-details-marker{display:none}
.lv-cols-form{position:absolute;z-index:30;top:2.4rem;left:0;min-width:11rem;display:flex;flex-direction:column;gap:var(--sp-1,.25rem);padding:.7rem;border:1px solid var(--border);background:var(--panel);box-shadow:0 6px 18px rgba(0,0,0,.4)}
.lv-cols-form label{display:flex;gap:var(--sp-2,.5rem);align-items:center;font-size:.85rem;color:var(--text);white-space:nowrap}
.lv-cols-form button{margin-top:.35rem}
/* Column menu living in the table header: a compact right-aligned icon whose
   popup opens rightward under it. The tablewrap scrolls horizontally (clipping
   an absolute popup), so un-clip it ONLY while the menu is open. */
.lv-colmenu-col{width:1%;white-space:nowrap;text-align:right;padding:.2rem .4rem}
.lv-colmenu-col .lv-cols>summary{height:1.7rem;padding:0 .4rem;border:1px solid transparent;background:transparent;color:var(--muted);font-size:.9rem}
.lv-colmenu-col .lv-cols>summary:hover{color:var(--text-hi);border-color:var(--border)}
.lv-colmenu-col .lv-cols-form{right:0;left:auto;top:1.9rem}
/* Un-clipping to show the popup used to scroll the whole PAGE. A wide table inside a
   visible-overflow wrap contributes its full width to the document: measured on
   docs.zm.is/plug/alpha.zm.is with the input schemas expanded, the table grows to 2046px
   in a 984px wrap, and opening this menu took the document from 1896 to 2502 — 606px of
   horizontal page scroll, reported as "introduces horizontal scroll when input schema
   containing wide content is expanded".
   overflow-x:clip is the fix: unlike hidden/auto it does NOT establish a scroll container,
   so overflow-y:visible stays legal on the other axis and the popup escapes downward while
   the table stays clipped sideways. The plain rule remains as the fallback for engines
   without clip, where the old behaviour is still better than a hidden menu. */
.lv-tablewrap:has(details.lv-cols[open]){overflow:visible}
@supports (overflow-x:clip){.lv-tablewrap:has(details.lv-cols[open]){overflow-x:clip;overflow-y:visible}}
.lv-tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin-top:.7rem}
/* The wrap is an overflow BFC, so the generic table{margin:1rem 0} would be trapped
   INSIDE it and stack with the pager's margin into an oversized, hard-to-predict gap.
   Zero the table's own margin so spacing is controlled solely by .lv-tablewrap (top)
   and .lv-pager (bottom) — identical on every ListView page (app-shell or Resource). */
.lv-table{margin:0}
/* A Numeric column holds a short count or figure, so it must never wrap — the same
   rule (and reason) as the timestamp below. Reported on org.zm.is/projects: the Plans
   column had 47px of content width, "1 plan" needed 44 and fitted, "0 plans" needed 52
   and broke after the digit, orphaning "plans" onto a second line. The count read as
   malformed on exactly the rows where the number was NOT one, which is most of them. */
.lv-table th.lv-num,.lv-table td.lv-num{text-align:right;white-space:nowrap}
/* Tighten cells on mobile so the essential columns fit without horizontal scroll. */
@media(max-width:640px){.lv-table td,.lv-table th{padding:.4rem .45rem}}
/* Keep a localized timestamp on ONE line so it never wraps a date into 2–3 lines;
   the .lv-tablewrap scrolls horizontally if the row genuinely exceeds the width. */
.lv-table time{white-space:nowrap}
.lv-table a.lv-sort{color:inherit;text-decoration:none;display:inline-flex;gap:var(--sp-1,.25rem);align-items:center;white-space:nowrap}
.lv-table a.lv-sort:hover{color:var(--text);text-decoration:underline}
.lv-arrow{font-size:.6rem;color:var(--accent,#2ea043)}
.lv-empty{text-align:center;color:var(--muted);padding:1.5rem!important}
/* PAGER = one capsule + the page-size control, on a single row.
   Was: three scattered pieces 79px tall — a "Per page" label with the select pushed
   BELOW it, a floating "1-5 of 5", and three separately-rounded nav buttons. The
   select dropped to its own line because the shared theme sets select{width:100%},
   so it filled the label; lv-size select{width:auto} is what puts it back beside the
   text. The range moved into the capsule's middle segment as a second line, which is
   what makes the footer compact instead of sprawling. */
.lv-pager{display:flex;flex-wrap:wrap;gap:var(--sp-3,.75rem);align-items:center;margin-top:.6rem;font-size:.85rem;color:var(--muted)}
/* Explicit height + align-self, because the form was measuring 58px — the full pager
   height — while the capsule beside it was 38px. The select then centred inside the
   TALLER box and sat 6px low, which is why "flush" kept not happening. Both now take
   --control-h and centre against the same line. */
.lv-size{display:flex;gap:var(--sp-2,.5rem);align-items:center}
/* The label carries the height too. With only the form constrained, the label
   measured the same 38px but rendered 6px LOWER — it overflowed rather than centring,
   so the select never lined up with the capsule however the parent was aligned.
   Constraining both removes the ambiguity. */
/* margin:0 is the actual fix. The shared theme styles label{display:block;
   margin:1rem 0 .25rem} for STACKED forms (label above its input). Inline control
   labels inherit it, and 16px top + 4px bottom around a 38px select is exactly the
   58px this form measured — which is why the select sat 6px below the capsule and
   why forcing heights on the parents never helped: the margin box was always 58
   whatever the box was told to be. */
.lv-size label{display:flex;align-items:center;gap:var(--sp-2,.5rem);white-space:nowrap;margin:0}
/* Height comes from the SAME token as the capsule below, so the two sit flush on one
   line instead of missing each other by a couple of pixels. */
.lv-size select{width:auto;height:var(--control-h)}
/* The "Per page" dropdown must adopt the host's dark theme, not browser-default
   white — same treatment as the search box.
   background-COLOR, not the background shorthand, and no padding-right. Both matter:
   the house select rule is appearance:none PLUS a chevron drawn as a background-image
   PLUS padding-right:2rem to keep the value clear of it. The shorthand wiped that image
   and the padding shorthand wiped the room, so this control rendered with no dropdown
   affordance at all and its value hard against the right border — "number goes outside
   field". Overriding a paint property with a shorthand is how you silently delete the
   two declarations beside it. */
.lv-size select{margin:0;padding-top:.3rem;padding-bottom:.3rem;padding-left:.5rem;background-color:var(--panel2,#0d1117);color:var(--text-hi,#c9d1d9);border:1px solid var(--border2,#30363d)}
.lv-size select:focus{outline:none;border-color:var(--primary,#1f6feb)}
/* The capsule: prev | page+range | next, sharing edges. Only the OUTER corners
   curve; the two internal joins stay straight, so the three read as one control
   rather than three buttons that happen to be adjacent. -1px margins collapse the
   doubled borders into single dividers. These are ordinary selectors, so they
   deliberately beat the zero-specificity corners rule in theme.go, which would
   otherwise round all four corners of each segment. */
.lv-nav{display:inline-flex;align-items:stretch;gap:0;margin-left:auto}
.lv-nav>*{border:1px solid var(--border);background:var(--panel2);margin-left:-1px;min-height:var(--control-h);border-radius:0/*corners-ok:capsule*/}
.lv-nav>*:first-child{margin-left:0;border-top-left-radius:var(--r-ctl);border-bottom-left-radius:var(--r-ctl)}
.lv-nav>*:last-child{border-top-right-radius:var(--r-ctl);border-bottom-right-radius:var(--r-ctl)}
.lv-pageof{display:flex;flex-direction:column;justify-content:center;align-items:center;
  padding:.2rem .7rem;line-height:1.2;white-space:nowrap}
/* Both lines are the SAME size and the SAME secondary colour, so the capsule reads as
   one muted unit rather than a bright counter with a faint subtitle. When prev/next
   are live (real buttons, primary text) the contrast is theirs to carry, not this. */
.lv-pageno,.lv-range{font-size:.78rem;color:var(--muted)}
.lv-page{display:inline-flex;align-items:center;padding:.35rem .7rem;border:1px solid var(--border);color:var(--text);text-decoration:none}
.lv-page:hover{background:rgba(255,255,255,.05)}
/* A disabled pager arrow dims its LABEL, never the element.
   opacity on the element fades its border and BACKGROUND too, so a disabled Prev/Next
   rendered its 1px border as rgb(236,239,242) while the middle segment kept the
   declared rgb(208,215,222) — three pieces with visibly different edges, in a control
   whose whole point is to read as one. The declared values matched, which is why this
   survives a naive check: you have to compare what RENDERS, not what is set. */
.lv-page.lv-disabled{color:var(--faint);cursor:default;pointer-events:none}
/* On mobile the .lv-table is governed by the SHARED webui.theme.go rule: it stays
   a COMPACT real table by default, and opts into the stacked "label: value" cards
   only when the host sets ListView.MobileStack (which emits class="tbl-stack" on
   the <table>). The data-label attrs the stack mode needs are already on every
   <td>. This file no longer keeps its own mobile-stacking copy. */
/* Responsive column priority: a column tagged Priority N gets lv-pri-N and is
   hidden inline as the viewport narrows (higher N hides sooner). No value is lost:
   the per-row ⓘ details button — shown only while a column IS hidden, via the
   JS-toggled .lv-has-hidden on the table — opens a modal listing every field. */
/* The ⓘ details button lives in the merged utility column (lv-colmenu-col, under
   the column switcher). It is hidden until a priority column is ACTUALLY collapsed
   (JS sets .lv-has-hidden), so on wide screens the column carries only the switcher. */
.lv-details-btn{display:none;align-items:center;justify-content:center;background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;min-height:1.9rem;padding:.15rem .55rem;font-size:.95rem;line-height:1;box-sizing:border-box;vertical-align:middle}
.lv-table.lv-has-hidden .lv-details-btn,.lv-table.lv-always-details .lv-details-btn{display:inline-flex}
/* Match the sibling action buttons' height EXACTLY rather than hardcoding one. A
   plug's action cell may hold full-height .btn controls (--control-h), compact
   .btn.sm ones, or a bare link — so any fixed height here is wrong for someone.
   align-self:stretch takes the height from the .lv-actions-wrap flex row, which is
   set by whatever the plug actually rendered, so the ⓘ lines up on every list.
   Reported on steps.zm.is/reflections, where a 1.9rem ⓘ sat beside 2.4rem buttons.
   The min-height above stays as the floor for a standalone utility cell (no
   siblings), where there is no row height to inherit. */
.lv-actions-wrap>.lv-details-btn{align-self:stretch}
.lv-details-btn:hover{color:var(--text);border-color:var(--accent,#2ea043)}
/* The MERGED trailing actions cell (utility column carrying a data-k: the caller's
   Edit/Preview buttons PLUS the absorbed ⓘ + column switcher). Two owner reviews live
   here, both from making the <td> ITSELF a flex container — DON'T reintroduce that:

   1) width:auto overrides the .lv-colmenu-col width:1% ABOVE. A width:1% actions column
      collapses to the header's tiny switcher-icon width, so the buttons OVERFLOW leftward
      and paint over the neighbouring data column (f5a3ba52 "actions in wrong column";
      b9643 "buttons overlap the Updated column"). width:auto sizes the column to its
      buttons. The icon-only standalone utility column (no data-k) keeps width:1%.
   2) The cell stays a normal table-cell with vertical-align:middle so it STRETCHES to the
      row height and centres its controls — even in a TALL wrapping row. When the <td> was
      display:flex it lost the table-cell row-height stretch, so in a tall row the buttons
      floated to the TOP instead of centring (b9643 "buttons float to the top of the cell").
      The gap between the controls (so the Delete|ⓘ borders don't touch as a doubled border,
      f5a3ba52 task) is owned by the inner .lv-actions-wrap inline-flex, NOT the td.
   Inert on mobile: .tbl-stack makes every td display:block (label:value), where neither
   cell width nor vertical-align applies. */
.lv-colmenu-col[data-k]{width:auto;text-align:right;vertical-align:middle}
.lv-actions-wrap{display:inline-flex;align-items:center;justify-content:flex-end;gap:var(--sp-2,.5rem);flex-wrap:nowrap}
.lv-colmenu-col[data-k] .lv-details-btn{margin-left:0}
@media(max-width:900px){.lv-table .lv-pri-3{display:none}}
@media(max-width:680px){.lv-table .lv-pri-2{display:none}}
@media(max-width:540px){.lv-table .lv-pri-1{display:none}}
.lv-modal{position:fixed;inset:0;z-index:1000;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.6);padding:1rem}
.lv-modal.open{display:flex}
.lv-modal-box{position:relative;background:var(--panel,#161b22);border:1px solid var(--border);max-width:24rem;width:100%;max-height:80vh;overflow:auto;padding:1.1rem 1.2rem;box-shadow:0 12px 40px rgba(0,0,0,.5)}
.lv-modal-box h3{margin:0 0 .7rem;font-size:1rem}
.lv-modal-x{position:absolute;top:.55rem;right:.7rem;background:none;border:none;color:var(--muted);font-size:1.35rem;line-height:1;cursor:pointer}
.lv-modal-x:hover{color:var(--text)}
.lv-kv{display:flex;justify-content:space-between;gap:var(--sp-4,1rem);padding:.45rem 0;border-bottom:1px solid var(--border);font-size:.88rem}
.lv-kv-sep{height:0;border-top:2px solid var(--border);margin:.5rem 0 .1rem}
.lv-kv:last-child{border-bottom:none}
.lv-kv .lv-k{color:var(--muted);flex:0 0 auto}
.lv-kv .lv-v{text-align:right;color:var(--text);word-break:break-word}
/* .lv-facts — the SAME row on a PAGE rather than in the details modal.
   space-between + text-align:right is right in a ~28rem modal and wrong the moment
   the row is full-page: measured on jobs' prospect page at 1408px, every label sat
   1173-1261px from its own value, so "Status" and "new" were a screen apart. That is
   what a plug's hand-built .prospect-facts grid was for, and deleting those grids in
   favour of the shared row is only an improvement if the shared row can do this.
   The container decides the layout, per STYLEGUIDE.md — one component, two contexts,
   no second implementation. */
.lv-facts .lv-kv{display:grid;grid-template-columns:minmax(8rem,max-content) minmax(0,1fr);gap:var(--sp-1) var(--sp-4);align-items:baseline}
.lv-facts .lv-kv .lv-v{text-align:left}
@media(max-width:560px){.lv-facts .lv-kv{grid-template-columns:1fr;gap:0}.lv-facts .lv-kv .lv-k{font-size:.8rem}}
/* Row-hover "more info" popover: a single shared floating card (reused across all
   rows/lists, positioned by JS near the cursor + clamped into the viewport). It
   draws from the SAME per-row .lv-rowdata payload the ⓘ modal reads, but shows ONLY
   the fields currently OFF-SCREEN (responsively collapsed or column-menu hidden) —
   so it surfaces what you can't see rather than repeating the row. House panel
   styling mirrors .lv-modal-box.
   Additive progressive enhancement: hover/keyboard-focus only — touch/no-hover
   devices keep the click ⓘ modal (unchanged). Hidden until JS positions + shows it. */

.lv-hovercard{position:fixed;z-index:2147483100;display:none;max-width:24rem;min-width:12rem;max-height:60vh;overflow:auto;padding:.7rem .85rem;background:var(--panel,#161b22);border:1px solid var(--border);box-shadow:0 12px 40px rgba(0,0,0,.5);font-size:.85rem;pointer-events:none}
.lv-hovercard.open{display:block}
.lv-hovercard .lv-kv{padding:.35rem 0}
@media (prefers-reduced-motion:no-preference){.lv-hovercard.open{animation:lv-hovercard-in .1s ease}}
@keyframes lv-hovercard-in{from{opacity:0}to{opacity:1}}
/* Touch / coarse-pointer devices have no reliable hover — never show the hovercard
   there (the ⓘ modal is the affordance). This also covers the no-hover case. */
@media (hover:none),(pointer:coarse){.lv-hovercard{display:none!important}}

/* Multi-select: a leading checkbox column + a toolbar shown (by JS) once ≥1 row is
   selected. Selection persists across htmx search/sort/page swaps. */
.lv-selcol{width:1%;white-space:nowrap;text-align:center;padding-left:.5rem;padding-right:.25rem}
.lv-selcol input[type=checkbox]{cursor:pointer;width:1rem;height:1rem;margin:0;vertical-align:middle}
/* width:1% above is the TABLE idiom for "shrink to fit" — meaningful only while the
   cell is laid out by a table. The stacked (.tbl-stack, <=640px) and dynamic card
   (.lv-cards, any width) layouts switch every td to display:block, where 1% stops
   being a hint and becomes literally 1% of the card: ~3px. The checkbox then renders
   as an untappable sliver above the first labelled row, which is what "multi-select
   is broken on mobile" looked like on every Selectable list in the mesh. Reset the
   width wherever the cell is no longer a table cell. .lv-colmenu-col[data-k] carries
   width:auto above for exactly this reason; .lv-expcol shares the same defect. */
.lv-cards .lv-selcol,.lv-cards .lv-expcol{width:auto}
@media(max-width:640px){.tbl-stack .lv-selcol,.tbl-stack .lv-expcol{width:auto}}
/* Touch targets: a 1rem box is fine for a mouse and small for a thumb. */
@media(pointer:coarse){.lv-selcol input[type=checkbox]{width:1.25rem;height:1.25rem}}
.lv-selbar{display:flex;flex-wrap:wrap;gap:var(--sp-2,.5rem);align-items:center;margin-bottom:.7rem;padding:.5rem .8rem;border:1px solid var(--accent,#2ea043);background:rgba(46,160,67,.1)}
/* The selbar ships with the 'hidden' attribute (no active selection). .lv-selbar's
   display:flex above would otherwise WIN over the UA [hidden]{display:none} (a class
   beats an attribute selector of equal specificity by source order), leaving the
   toolbar visible at "0 selected". This restores the hide; the JS clears 'hidden'
   to reveal it once 1+ rows are selected. */
.lv-selbar[hidden]{display:none}
.lv-selcount{font-weight:600;font-size:.85rem;color:var(--text)}
.lv-selactions{display:flex;gap:var(--sp-2,.5rem);flex-wrap:wrap}
.lv-selbtn{padding:.35rem .85rem;border:1px solid var(--border);background:var(--panel);color:var(--text);cursor:pointer;font-size:.82rem;font-weight:600}
.lv-selbtn:hover{background:var(--panel2,#30363d);color:var(--text);border-color:var(--accent,#2ea043)}
/* Danger (Delete) bulk button. The selector is PARENT-SCOPED (.lv-selbar …) so its
   specificity beats the shared .btn.danger:hover / .btn:hover from AppShellCSS (which
   the button also matches via its "btn" class) regardless of stylesheet load order —
   otherwise the base .btn:hover set a light --panel background while the text stayed
   white → the unreadable white-on-white the owner reported. Uses the --danger token so
   both light and dark themes render a dark-red fill with white text on hover. */
.lv-selbar .lv-selbtn.danger{background:var(--danger,#da3633);color:#fff;border-color:rgba(0,0,0,.18)}
.lv-selbar .lv-selbtn.danger:hover{background:var(--danger-strong,#b62b28);color:#fff;border-color:rgba(0,0,0,.28)}
.lv-selclear{margin-left:auto;background:none;border:1px solid var(--border);color:var(--muted);cursor:pointer;padding:.32rem .7rem;font-size:.8rem}
.lv-selclear:hover{color:var(--text)}
