* {
  box-sizing: border-box;
}
html,
body {
  margin: 0;
  height: 100%;
}
body {
  background: var(--bg);
  color: var(--text);
  font: 14px / 1.5 system-ui, -apple-system, sans-serif;
}
.muted {
  color: var(--muted);
}

#app {
  display: grid;
  grid-template-columns: 248px 1fr;
  height: 100vh;
  --tagpanel-w: 320px;
}

/* sidebar */
#sidebar {
  background: var(--panel);
  border-right: 1px solid var(--border);
  padding: 18px 14px;
  display: flex;
  flex-direction: column;
  gap: 14px;
  overflow: hidden;
}
#sidebar h1 {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 16px;
  font-weight: 650;
  margin: 0;
  /* Match the height of the main toolbar's control row (tallest widget = 36px search). Both this
     brand row and #topbar's .tb-row share an 18px top padding, so equal heights + centered content
     line the "JayBoard" logo/wordmark up vertically with the top bar. */
  min-height: 36px;
}
.brand-logo {
  color: var(--accent);
  flex: none;
}
/* The wordmark is the one item in this row centered as text, not as a fixed-size box: align-items
   centers its whole line box, so the font's asymmetric ascent/descent leaves the visible glyphs
   sitting slightly off-center next to the logo and the collapse icon (both fixed-size, geometrically
   centered). system-ui resolves to different fonts per OS, so the drift is font-dependent. Leading-
   trim collapses the line box to the cap→baseline block, so the wordmark centers on its cap box and
   lines up with the icons regardless of which font is in play. */
#sidebar h1 span {
  text-box-trim: trim-both;
  text-box-edge: cap alphabetic;
}
/* #search uses the shared .field primitive (widgets.css); .searchbox #search below adds icon layout. */
/* Toolbar search: a leading magnifier icon inside the field (icon language matches the chips). */
.searchbox {
  position: relative;
  display: inline-flex;
  align-items: center;
  /* Shrinks under pressure, and now grows into the space the tag combobox used to hold — that control
     moved to #tagpanel, and the row it shared has nothing left to push onto a second line. The cap is
     still a cap, just a wider one: a search field spanning a 27" monitor is harder to aim at than a
     generous one, and the type chips would be stranded at the far left.
     The 150px BASIS is not a width — flex-grow means the box is never rendered that narrow — it is the
     WRAP THRESHOLD: a flex line breaks before it shrinks a child below its basis, and at the old 240 this
     box was the one that broke the row.
     With the tag panel OPEN at 1440 or narrower the row still wraps, and that is the better of the two
     outcomes rather than an oversight: the panel takes 320px off the content column, and the only basis
     that keeps one line there (110) renders the field at 115px. A search box you cannot read a query in,
     to save a toolbar row, is the wrong way round. Measured with the panel open: 1280 and 1440 wrap,
     1680 and up hold one row. With it shut nothing wraps at any of those widths.
     The 424px CAP: 352 was the width this box had before the tag combobox moved out of the row, and this
     is that plus a fifth — a multiple of 8, like the rest of the toolbar's spacing. It only binds on a
     wide window; everywhere narrower the row runs out of space first and flex-grow decides. */
  flex: 1 1 150px;
  min-width: 0;
  max-width: 424px;
}
.searchbox .search-ic {
  position: absolute;
  left: 11px;
  display: inline-flex;
  color: var(--muted);
  pointer-events: none;
}
.searchbox .search-ic .ic {
  width: 15px;
  height: 15px;
}
.searchbox #search {
  height: 36px;
  width: 100%; /* the box decides the width now (see .searchbox); the input just fills it */
  padding: 0 12px 0 34px;
}

/* Keyboard / mouse shortcuts help modal — shared by the gallery and the moodboard (board.html also
   loads this stylesheet). Opened by the ? button in each toolbar. */
.help-modal {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
}
.help-modal[hidden] {
  display: none;
}
.help-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(0, 0, 0, 0.55);
}
.help-card {
  position: relative;
  width: min(480px, 92vw);
  max-height: 84vh;
  overflow-y: auto;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 20px 22px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.help-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.help-head h3 {
  margin: 0;
  font-size: 15px;
}
.help-close {
  background: none;
  border: 0;
  color: var(--muted);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 4px;
}
.help-close:hover {
  color: var(--text);
}
.help-list {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 9px;
}
.help-row {
  display: flex;
  align-items: baseline;
  gap: 12px;
}
.help-row dt {
  flex: 0 0 132px;
  text-align: right;
  color: var(--text);
  font: 600 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
.help-row dd {
  margin: 0;
  color: var(--muted);
  font-size: 13px;
}
.help-sub {
  margin: 8px 0 0;
  padding-top: 11px;
  border-top: 1px solid var(--border);
  color: var(--text);
  font-weight: 600;
  font-size: 12px;
}
/* About, under the shortcut list. Ruled off with the same 1px line .help-sub uses, so the panel reads
   as one list with a footer rather than as two stacked cards. Gallery only: board.html loads this
   stylesheet too, but its modal has no About block, and an unused rule costs nothing there. */
.help-about {
  margin-top: 14px;
  padding-top: 12px;
  border-top: 1px solid var(--border);
}
.help-about h4 {
  margin: 0 0 6px;
  color: var(--text);
  font-weight: 600;
  font-size: 12px;
}
.help-about p {
  margin: 0 0 11px;
  color: var(--muted);
  font-size: 13px;
  line-height: 1.5;
}
/* The link and the version sit on one row, pushed apart. With no extension URL set the link is
   [hidden] and the version stays where it is — space-between over two items, not a centred pair. */
.help-about-foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
}
.help-about-foot a {
  color: var(--accent);
  font-size: 13px;
  text-decoration: none;
}
.help-about-foot a:hover {
  text-decoration: underline;
}
.help-about-foot a[hidden] {
  display: none;
}
.help-ver {
  margin-left: auto;
  color: var(--muted);
  font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* "All items" above the Projects heading, ruled off from it the way #side-footer is ruled off from
   the list above it — the same 1px line the Moodboards section sits under. */
#all-items {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--border);
}
#projects {
  display: flex;
  flex-direction: column;
  gap: 2px;
  overflow-y: auto;
  flex: 1;
}
.project {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  /* Reserve the height of the hover-revealed ✎ button (20px + padding) so showing it never
     grows the row — same fix the moodboard rows use. */
  min-height: 34px;
  border-radius: 8px;
  cursor: pointer;
  color: var(--muted);
}
.project:hover {
  background: var(--panel-2);
}
/* Where you are is said with weight and contrast, not with a filled row: the sidebar sits beside a grid
   of images and does not need to compete with it. Hover keeps the fill, so the two states never say the
   same thing — pointing at a row looks nothing like being on it. */
.project.active {
  color: var(--text);
  font-weight: 600;
}
.project .name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.project .n {
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* project row highlighted as the target while dragging cards onto it */
.project.drop-target {
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  outline: 2px dashed var(--accent);
  outline-offset: -2px;
}
/* Drag-to-reorder cues (projects + moodboards): the grabbed row dims; a line shows where it will drop. */
.project.dragging,
.board-row.dragging {
  opacity: 0.45;
}
.project.reorder-before,
.board-row.reorder-before {
  box-shadow: inset 0 2px 0 0 var(--accent);
}
.project.reorder-after,
.board-row.reorder-after {
  box-shadow: inset 0 -2px 0 0 var(--accent);
}
.prow-right {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
}
.prow-edit {
  display: none;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 5px;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
}
.project:hover .prow-edit {
  display: inline-flex;
}
.prow-edit:hover {
  background: color-mix(in srgb, var(--text) 14%, transparent);
  opacity: 1;
}
.prow-edit .ic {
  width: 13px;
  height: 13px;
}

/* projects section header + "new project" button */
.side-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: -8px;
  font-size: 11px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.side-head-actions {
  display: inline-flex;
  gap: 2px;
}
.icon-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 6px;
  color: var(--muted);
  cursor: pointer;
}
.icon-btn:hover {
  background: var(--panel-2);
  color: var(--text);
}
.icon-btn .ic {
  width: 16px;
  height: 16px;
}

/* collapsible sidebar */
#sidebar h1 #theme-toggle {
  margin-left: auto;
  flex: none;
}
#sidebar h1 #sidebar-collapse {
  flex: none;
}
/* Expand button lives at the top-left of the main area; only shown once collapsed. */
#sidebar-expand {
  display: none;
}
#sidebar-expand.sidebar-toggle {
  flex: none;
  width: 36px;
  height: 36px;
  border: 1px solid var(--border);
  border-radius: 8px;
  margin-right: 2px;
}
#sidebar-expand.sidebar-toggle:hover {
  border-color: var(--accent);
}
body.sidebar-collapsed #app {
  grid-template-columns: 1fr;
}
body.sidebar-collapsed #sidebar {
  display: none;
}
body.sidebar-collapsed #sidebar-expand {
  display: inline-flex;
}

#side-footer {
  display: flex;
  flex-direction: column;
  gap: 10px;
  border-top: 1px solid var(--border);
  padding-top: 12px;
}

#boards {
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 26vh;
  overflow-y: auto;
}
.board-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  /* Reserve the height of the hover-revealed ✎ button (20px + padding) so showing it
     never grows the row — otherwise the footer, pinned to the bottom, shifts up. */
  min-height: 34px;
  border-radius: 8px;
  /* Muted like an unselected project row — nothing in the sidebar reads as current except what is. */
  color: var(--muted);
  text-decoration: none;
  font-size: 13px;
}
.board-row:hover {
  background: var(--panel-2);
}
.board-row > svg {
  color: var(--accent);
  flex: none;
}
/* reuse the project row's hover ✎ button styling for the moodboard rows */
.board-row:hover .prow-edit {
  display: inline-flex;
}
.board-row .name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.board-row .n {
  font-size: 11px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.boards-empty {
  padding: 4px 10px;
  font-size: 12px;
}
/* main */
#main {
  overflow-y: auto;
  /* Always reserve the scrollbar's space so the navbar/grid don't shift by the scrollbar width
     when switching between a short (no scrollbar) and tall (scrollbar) view. */
  scrollbar-gutter: stable;
  /* No top padding: the sticky #topbar owns the top spacing (via its own padding-top) so it sticks
     flush to the very top and its opaque background covers cards scrolling underneath. A scroll
     container's top padding sticks the header lower, leaving a see-through strip above it. */
  padding: 0 22px 40px;
}
#topbar {
  /* A column of two rows: [filters + sort/search] on top, then the full-width tag filter below. */
  display: flex;
  flex-direction: column;
  position: sticky;
  top: 0;
  background: var(--bg);
  /* A persistent hairline divider below the toolbar (the scroll shadow layers on top of it). */
  border-bottom: 1px solid var(--border);
  /* Break out of #main's 22px horizontal padding so the bar (and its shadow) spans the full width edge
     to edge, then re-inset the same 22px so the toolbar content stays aligned with the grid below.
     The RIGHT side also has to cross the scrollbar gutter. #main reserves one permanently
     (scrollbar-gutter: stable, so the grid doesn't jump when a scrollbar appears), and a gutter is not
     padding — it is width the scroll container holds back, which a negative margin of -22px does not
     reach. So the hairline sat flush against the sidebar on the left and stopped 15px short of the tag
     panel on the right. --sbw is that gutter, measured in gallery.ts because it is 0 on any platform
     whose scrollbars overlay and cannot be written down here. The padding grows by the same amount, so
     the toolbar's contents stay lined up with the cards under them. */
  margin: 0 calc(-22px - var(--sbw, 0px)) 0 -22px;
  padding: 18px calc(22px + var(--sbw, 0px)) 12px 22px;
  /* Above all grid content — the card kind/video badges (z-index 2–3) and the selected ✓ overlay —
     so nothing shows over the sticky bar while scrolling. Stays below the overlays that must cover
     it: the selection bar (40), modals (50+), and menus. */
  z-index: 30;
  /* A soft drop-shadow fades in once the grid scrolls beneath the bar (JS toggles .scrolled), so the
     toolbar reads as a raised layer over the content — and none at the very top. */
  transition: box-shadow 0.18s ease;
}
#topbar.scrolled {
  /* Dark theme (--bg is near-black), so the shadow must be near-opaque black to read at all — a low
     alpha is invisible black-on-black. A crisp hairline + a soft fall-off gives the raised-layer look. */
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.6), 0 10px 22px -6px rgba(0, 0, 0, 0.85);
}
/* Top row: kind filters on the left, sort + search pinned right. */
.tb-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}
#filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px; /* between the type filter (kind chips) and the tag combobox */
  /* Take the space between the collapse button and the sort/search cluster. */
  flex: 1;
  min-width: 0;
  /* No left margin: the kind filter's left edge lines up with the card grid below (both sit at
     #main's 22px padding). The .tb-row gap already separates it from the collapse button. */
}
/* Single-line taginput (opts.singleLine): pills + field never wrap; overflow scrolls sideways with the
   scrollbar hidden (still scrollable via wheel / trackpad / drag). */
.taginput.taginput-single {
  flex-wrap: nowrap;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none; /* Firefox */
}
.taginput.taginput-single::-webkit-scrollbar {
  display: none; /* WebKit */
}
.taginput.taginput-single .chip {
  flex: none; /* don't let pills shrink — they keep their size and the row scrolls instead */
}

/* ---------- filter facets: inline on desktop, a popover on mobile (≤860px) ---------- */
/* Desktop: the Filter button is hidden and the menu + its groups collapse to nothing (display:contents),
   so #kinds and #untagged-chip become direct flex children of #filters and render inline. The tags facet
   that used to sit after search (via `order`) is gone — it is #tagpanel now — which leaves the row as
   type · search, and search free to grow across what it vacated. */
#filter-btn {
  display: none;
}
#filter-menu,
.filter-group {
  display: contents;
}
.filter-group-label {
  display: none;
}
/* Button internals (styled here so they're ready when the mobile @media block reveals the button). */
.filter-btn {
  align-items: center;
  gap: 7px;
  height: 36px;
  padding: 0 12px;
  white-space: nowrap;
}
.filter-btn .filter-ic {
  display: inline-flex;
  color: var(--muted);
}
.filter-btn .filter-ic .ic {
  width: 16px;
  height: 16px;
}
.filter-btn.active,
.filter-btn[aria-expanded="true"] {
  border-color: var(--accent);
}
.filter-btn.active .filter-ic {
  color: var(--accent);
}
.filter-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: var(--radius-pill);
  background: var(--accent);
  color: #fff;
  font: 700 11px system-ui;
  font-variant-numeric: tabular-nums;
}
.filter-count[hidden] {
  display: none;
}

.tb-right {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: none;
}
/* Desktop: the action buttons lay out inline in .tb-right (the wrapper collapses). On mobile the
   wrapper becomes a real flex row so the actions sit together below Sort — see the responsive block. */
.tb-actions {
  display: contents;
}
/* Twice .tb-right's 8px gap between the sort control and the icons that follow it. Sort is a labelled
   control that reads as part of the filtering; the icons after it are actions. With one gap for both
   joins they ran together as a single strip of seven things. The wrapper is display:contents here, so
   this margin is what separates the two groups — the mobile block below drops it, where .tb-actions is
   a real row of its own and the two are already on separate lines. */
.tb-actions > :first-child {
  margin-left: 8px;
}
/* Image-size slider — a bare icon + range input (no pill chrome) that drives the grid's column count,
   so dragging it grows/shrinks every card. */
/* Image size — an icon that opens a menu, anchored by its wrapper the way .sortwrap anchors the sort
   dropdown. Laid out like #autotag-btn: glyph and caret side by side, ~42px against the slider's 124px. */
.sizewrap {
  position: relative;
  display: inline-flex;
}
#size-btn {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
#size-btn .size-ic {
  display: inline-flex;
  color: var(--muted);
  flex: none;
}
/* Sort control: a segmented "key ▾ | direction" unit (mirrors the tag AND/OR toggle), whose key
   button opens a custom dropdown instead of a native <select> — matching the app's other menus. */
/* Wrapper holds the (clipped) control + the dropdown, so overflow:hidden on .sortctl can round the
   segment hover corners without clipping the menu that opens below it. */
.sortwrap {
  position: relative;
  display: inline-flex;
}
.sortctl {
  display: inline-flex;
  align-items: stretch;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden; /* clip the segments to the rounded border (no corner slivers) */
  transition: border-color 0.12s ease;
}
/* No hover highlight; on focus just switch the border to accent — identical to .field:focus and
   .taginput:focus-within, so the toolbar's focusable widgets read the same. */
.sortctl:focus-within {
  border-color: var(--accent);
}
.sort-seg {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 34px;
  padding: 0 11px;
  border: 0;
  background: transparent;
  color: var(--text);
  font: 600 13px system-ui;
  cursor: pointer;
  white-space: nowrap;
}
.sort-seg:first-child {
  border-radius: 7px 0 0 7px;
}
.sort-seg + .sort-seg {
  border-left: 1px solid var(--border);
}
#sort-dir {
  border-radius: 0 7px 7px 0;
  padding: 0 9px;
}
#sort-dir .ic {
  display: block;
  width: 16px;
  height: 16px;
}
.sort-ic,
.sort-caret {
  display: inline-flex;
  color: var(--muted);
}
.sort-ic .ic {
  width: 15px;
  height: 15px;
}
.sort-caret .ic {
  width: 14px;
  height: 14px;
  transition: transform 0.15s ease;
}
.sortctl.open .sort-caret .ic {
  transform: rotate(180deg);
}

/* Sort dropdown — reuses the tag/combobox menu look (panel-2 card + rounded rows). */
.sort-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 30;
  min-width: 100%;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.sort-opt {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: var(--text);
  font: 500 13px system-ui;
  white-space: nowrap;
  text-align: left;
  cursor: pointer;
}
.sort-opt:hover {
  background: var(--panel);
}
.sort-opt.active {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.sort-opt .ic {
  flex: none;
  width: 15px;
  height: 15px;
  color: var(--muted);
}
.sort-opt.active .ic {
  color: var(--accent);
}
.sort-opt-label {
  flex: 1;
}
.sort-check {
  flex: none;
  width: 12px;
  color: var(--accent);
  font-size: 12px;
  text-align: right;
}

/* ✨ auto-tag dropdown (toolbar #autotag-menu + viewer #m-autotag-menu). The wrapper anchors the menu;
   the toolbar variant drops down flush to the right edge, the modal variant flush to the left. */
.aiwrap {
  position: relative;
  display: inline-flex;
}
.ai-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 40;
  min-width: 200px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 5px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
/* Opens upward. The two other ✨ sit in the top toolbar and drop down; the selection bar sits at the
   bottom of the window, where a downward menu would open off-screen. Only the anchor changes — the rows,
   the padding and the shadow are the same menu. */
.ai-menu--up {
  top: auto;
  bottom: calc(100% + 6px);
}
.ai-opt {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 0;
  border-radius: 7px;
  background: none;
  color: var(--text);
  text-align: left;
  cursor: pointer;
}
.ai-opt:hover {
  background: var(--panel);
}
.ai-opt .ic {
  flex: none;
  width: 16px;
  height: 16px;
  color: var(--accent);
}
.ai-opt-label {
  font: 600 13px system-ui;
  line-height: 1.2;
}
/* Toolbar buttons use the shared .btn / .btn--primary / .btn--icon primitives (widgets.css). */

/* kind filter (all / images / videos / links) */
/* Kind filter — a segmented control (mirrors the sort control): one rounded, bordered unit with the
   four icon+count cells butted together and hairline dividers between them. */
/* kind filter — shared .segmented / .segmented__opt shell (widgets.css); cells add kind-specific bits */
.kindchip {
  height: 34px;
  gap: 6px;
  padding: 0 12px;
  font-size: 12px;
  white-space: nowrap;
}
.kindchip:hover {
  background: var(--panel);
  color: var(--text);
}
.kindchip.active {
  /* soft accent tint + accent glyph/count, not the segmented default's solid fill */
  background: color-mix(in srgb, var(--accent) 18%, var(--panel-2));
  color: var(--accent);
}
.kindchip.active:hover {
  background: color-mix(in srgb, var(--accent) 26%, var(--panel-2));
}
.kindchip .ic {
  width: 15px;
  height: 15px;
  flex: none;
}
.kindchip .n {
  opacity: 0.75;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  /* Centered in a fixed min-width (set inline per render) so the count never resizes the cell. */
  text-align: center;
}
.kindchip.active .n {
  opacity: 1;
}
/* Standalone "Untagged" toggle chip: the same cell look as the content-type chips, but its own
   bordered pill (it isn't part of the segmented control), so it can stay lit alongside a selected
   content type and narrow the view to that type's untagged items. */
.kindchip--solo {
  display: inline-flex;
  align-items: center;
  height: 36px;
  border: 1px solid var(--border);
  background: var(--panel-2);
  border-radius: var(--radius);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.kindchip--solo.active {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}

/* Bookmark with no screenshot: a plain gray placeholder tile (the inline aspect-ratio reserves its size). */
.card .media.linkempty {
  background: var(--panel-2);
}
/* Text-only bookmark (e.g. a tweet with no media): show the saved text as a quote card. */
.card .media.linkcard {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 10px;
  padding: 14px 16px;
  background: var(--panel-2);
  overflow: hidden;
}
.card .media.linkcard .linkcard-text {
  font-size: 14px;
  line-height: 1.45;
  color: var(--text);
  display: -webkit-box;
  -webkit-line-clamp: 7;
  line-clamp: 7;
  -webkit-box-orient: vertical;
  overflow: hidden;
}
.card .media.linkcard .linkcard-host {
  font-size: 12px;
  color: var(--muted);
  flex: none;
}

/* Background jobs used to render as a banner here, between the toolbar and the grid. They are rows in the
   uploads tray now (#uploads, below) — the same corner, the same markup, one place to look. */

/* masonry via CSS grid row-spans (span computed in JS) — fills left-to-right, natural ratios */
#grid {
  display: grid;
  /* Column count is set by the toolbar size slider — applyGridCols() writes an inline repeat(N, …), so
     each notch adds/removes a column (a guaranteed visible change). This literal is just the pre-JS
     default; minmax(0, 1fr) lets the columns shrink to share the width evenly. */
  grid-template-columns: repeat(6, minmax(0, 1fr));
  grid-auto-rows: 8px;
  column-gap: 12px;
  row-gap: 0;
  align-items: start;
  /* breathing room below the sticky heading/count before the cards start */
  margin-top: 14px;
}
/* Infinite-scroll trigger: a zero-height marker just past the grid. An IntersectionObserver watches it
   (with a rootMargin lookahead) and loads the next page as it nears the viewport. */
.grid-sentinel {
  width: 100%;
  height: 1px;
}
.card {
  position: relative;
  background: var(--panel);
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  transition: transform .12s ease;
  /* Long-press selects on touch (see gallery.ts): stop the OS image callout / text-selection from
     hijacking the hold. No effect on mouse input. */
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
/* Compact drag image shown while dragging cards to a project (built in makeDragGhost, off-screen). */
.drag-ghost {
  position: fixed;
  top: -9999px;
  left: -9999px;
  width: 56px;
  height: 56px;
  pointer-events: none;
}
.drag-ghost .dg-thumb {
  width: 56px;
  height: 56px;
  border-radius: 8px;
  overflow: hidden;
  border: 1px solid var(--border);
  background: #0a0c0f;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.5);
}
.drag-ghost .dg-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.drag-ghost .dg-count {
  position: absolute;
  right: 3px;
  bottom: 3px;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  border-radius: 9px;
  background: var(--accent);
  color: #fff;
  font: 600 11px system-ui;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.4);
}
.card:hover {
  transform: translateY(-4px);
}
.card.selected {
  box-shadow: 0 0 0 2px var(--accent);
}
.card.selected::after {
  content: "✓";
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  background: var(--accent);
  color: #fff;
  font-size: 13px;
  font-weight: 700;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.45);
}
.card .media {
  position: relative;
  display: block;
  line-height: 0;
  overflow: hidden;
  /* Match the card's corners here so the img/video below can `inherit` per-corner
     radii from this wrapper (top rounded always; bottom squared when a caption
     follows, see below). The radius on the media element itself is what actually
     clips a composited <video> layer. */
  border-radius: inherit;
}
/* With a caption below, keep the media's bottom square — the card rounds the bottom. */
.card .media:not(:last-child) {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
}
.card .media img,
.card .media video {
  width: 100%;
  height: auto;
  display: block;
  background: #0a0c0f;
  /* clicks should open the modal, not interact with the media element */
  pointer-events: none;
  /* A <video> paints on its own GPU layer, which an ancestor's overflow:hidden +
     border-radius cannot clip — so its square corners bleed over the rounded
     border (most visible at the top on hover). Round the element itself, matching
     the media wrapper's corners (top rounded; bottom too only when there's no
     caption), so it clips its own layer. */
  border-radius: inherit;
  /* Start hidden and fade in once loaded (the `.loaded` class is added in buildCardEl on the
     load/loadeddata event) so items don't snap in from the dark placeholder as they decode. */
  opacity: 0;
  transition: opacity 0.4s ease;
}
.card .media img.loaded,
.card .media video.loaded {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .card .media img,
  .card .media video {
    transition: none;
  }
}
/* Corner badges — icon-only pills at the card's top-right, laid out left→right. The "saved offline"
 * marker (green) sits to the LEFT of the content-type badge; kind colour comes from an inline --bc. */
.card .card-badges {
  position: absolute;
  top: 8px;
  right: 8px;
  z-index: 2;
  display: flex;
  gap: 6px;
  /* Fade in rather than pop in over the dark placeholder: buildCardEl drops `.badge-pending` when the
     media the badge sits on reveals, or (favicon linkcards, no media) a couple of frames after mount. */
  transition: opacity 0.4s ease;
}
.card .card-badges.badge-pending {
  opacity: 0;
}
@media (prefers-reduced-motion: reduce) {
  .card .card-badges {
    transition: none;
  }
}
.card .cardbadge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  color: #fff;
  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.45);
  pointer-events: none;
}
.card .kindbadge {
  background: color-mix(in srgb, var(--bc) 88%, #05070a);
  border: 1px solid color-mix(in srgb, var(--bc) 55%, #fff);
}
.card .cardbadge .ic {
  width: 14px;
  height: 14px;
  flex: none;
}
#empty {
  text-align: center;
  margin-top: 80px;
}
#empty .empty-title {
  font-size: 17px;
  font-weight: 650;
  color: var(--text);
  margin: 0 0 6px;
}
#empty .empty-sub {
  max-width: 420px;
  margin: 0 auto;
  color: var(--muted);
  font-size: 14px;
  line-height: 1.5;
}

/* modal */
#modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
}
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.85);
}
.modal-stage {
  position: relative;
  z-index: 51;
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Wide left/right padding reserves a gutter for the prev/next arrows so the media never sits under
     them (the arrows live in these gutters; the media is capped to the padded content box). */
  padding: 24px 72px;
  /* Give the (frequently-changing) media its own compositing layer, so swapping the image on
     arrow-key navigation repaints only the stage — the backdrop, sidebar behind it, and the
     text panel stay cached and don't flicker. Isolated here (not on #modal) to keep subpixel
     text antialiasing on the meta panel. */
  transform: translateZ(0);
}
#modal-img,
#modal-video {
  max-width: 100%;
  max-height: 92vh;
  object-fit: contain;
  border-radius: 8px;
  background: #000;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
}
/* Video wrapper hugs the <video> so the capture button can overlay its corner. */
.mv-wrap {
  position: relative;
  display: inline-flex;
  max-width: 100%;
  max-height: 92vh;
}
/* Capture-frame button overlaid on the video — top-right, clear of the native bottom controls. */
.mv-capture {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 0;
  border-radius: 8px;
  background: rgba(0, 0, 0, 0.55);
  color: #fff;
  cursor: pointer;
  opacity: 0.85;
  transition: opacity 0.12s ease, background 0.12s ease;
}
.mv-capture:hover {
  opacity: 1;
  background: rgba(0, 0, 0, 0.72);
}
.mv-capture svg {
  width: 17px;
  height: 17px;
}
.mv-capture.busy {
  cursor: default;
}
.mv-capture.busy svg {
  animation: mtool-pulse 0.9s ease-in-out infinite;
}
.modal-linkcard {
  display: flex;
  flex-direction: column;
  gap: 14px;
  text-align: left;
  color: var(--text);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 18px;
  /* Fill the padded stage (which already reserves the arrow gutters) up to a generous cap. */
  width: min(960px, 100%);
}
/* The stage is a bare media host now: videos and bookmark screenshots strip the card chrome and show
   like a plain image, so everything in the viewer reads the same. */
.modal-linkcard.bare {
  gap: 0;
  width: auto;
  max-width: 100%;
  background: none;
  border: 0;
  padding: 0;
}
.modal-linkcard.bare .mv-wrap,
.modal-linkcard.bare #modal-video,
.modal-linkcard.bare .ml-thumb {
  max-height: 92vh;
}
.modal-linkcard .mv-wrap,
.modal-linkcard #modal-video,
.modal-linkcard .ml-thumb {
  align-self: center;
  max-width: 100%;
  max-height: 70vh;
}
/* Bookmark screenshot: framed like the plain-image viewer (#modal-img) now the header has moved out. */
.modal-linkcard .ml-thumb {
  object-fit: contain;
  border-radius: 8px;
  background: #000;
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.6);
}
/* Fallback when a bookmark has no screenshot: a plain gray placeholder box in the viewer. */
.ml-empty {
  align-self: center;
  width: min(720px, 100%);
  aspect-ratio: 16 / 9;
  background: var(--panel-2);
  border-radius: 8px;
}
.ml-empty[hidden] {
  display: none;
}
.modal-nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 52;
  background: color-mix(in srgb, var(--text) 8%, transparent);
  color: #fff;
  border: 0;
  width: 46px;
  height: 70px;
  font-size: 30px;
  cursor: pointer;
  border-radius: 10px;
  opacity: 0.8; /* slightly faded at rest; full on hover */
}
.modal-nav:hover {
  background: color-mix(in srgb, var(--text) 18%, transparent);
  opacity: 1;
}
.modal-nav.prev {
  left: 16px;
}
.modal-nav.next {
  right: 16px;
}
.modal-meta {
  position: relative;
  z-index: 51;
  flex: 0 0 374px;
  width: 374px;
  align-self: stretch;
  background: var(--panel);
  border-left: 1px solid var(--border);
  padding: 18px;
  overflow-y: auto;
  /* Always reserve the scrollbar's space, the same reason #main does: paging through a library with ←/→
     lands on items whose panels differ in height, and without this every field, tag pill and tool jumps
     sideways by the scrollbar's width each time one description is long enough to scroll and the next is
     not. Reserving it costs the column ~15px on a desktop and nothing on the platforms that draw the
     scrollbar over the content. */
  scrollbar-gutter: stable;
}
/* Top toolbar: auto-tag, open, download, delete, close — right-aligned above the meta. */
.modal-tools {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 4px;
  /* Break out to the sidebar edges so the divider spans full width (like the topbar's), then re-inset
     the icons. A hairline border below separates the tool row from the meta. */
  margin: -2px -18px 14px;
  padding: 0 18px 10px;
  border-bottom: 1px solid var(--border);
}
/* The collapse chevron sits at the far left of the toolbar, apart from the action icons — and hangs out
   past it by the width of its own padding, so the ARROW lines up with the meta column underneath rather
   than the invisible 30px box around it. Measured, not guessed: the ink started 12px inside the fields
   below it. The hit area overhangs into the panel's padding, which is what an icon button's box is for. */
.mtool-collapse {
  margin-right: auto;
  margin-left: -12px;
}
/* Same on the other end: the ✕ is a glyph centred in its box, so the box being flush left the mark 9px
   short of the column's right edge. */
.modal-tools .modal-close {
  margin-right: -9px;
}
.mtool-collapse svg {
  transition: transform 0.15s ease;
}
/* Collapsed: the detail panel shrinks to just its toolbar, and the chevron flips to point back the way
   it will come. The axis is the one the panel actually moves on — sideways here, and downwards on a
   phone, where the ≤640 block turns both of these a quarter turn. */
#modal.meta-collapsed .mtool-collapse svg {
  transform: rotate(180deg);
}
#modal.meta-collapsed .modal-meta > :not(.modal-tools) {
  display: none;
}
#modal.meta-collapsed .modal-meta {
  flex: 0 0 auto;
  width: auto;
  overflow: visible;
  /* A rail of icons and nothing else, so the 18px that frames a column of fields is just distance from
     the edge — it made the panel 79px wide to hold a 30px button. */
  padding: 10px 8px;
}
/* The ✨ drops its caret here. It opens the same menu, but the mark for that is chrome a rail this narrow
   has no room for, and it was the reason this button alone was 42px in a column of 30s. */
#modal.meta-collapsed #m-autotag {
  width: 30px;
  gap: 0;
  padding: 0;
}
#modal.meta-collapsed #m-autotag .menu-caret {
  display: none;
}
#modal.meta-collapsed .modal-tools {
  flex-direction: column;
  align-items: flex-end;
  flex-wrap: nowrap;
  margin: 0;
  padding: 0;
  border-bottom: 0;
}
/* Neither nudge survives into the rail: there is no column of fields to line up with there, only icons
   that should sit as a column of their own. */
#modal.meta-collapsed .mtool-collapse {
  margin-right: 0;
  margin-left: 0;
}
#modal.meta-collapsed .modal-tools .modal-close {
  margin-right: 0;
}
/* On phones the toolbar stays a horizontal bar; collapsing just drops the fields and enlarges the media. */
@media (max-width: 640px) {
  #modal.meta-collapsed .modal-meta {
    width: 100%;
  }
  #modal.meta-collapsed .modal-tools {
    flex-direction: row;
    align-items: center;
  }
  /* Stacked, not side by side: the panel closes downwards here, so the chevron says down and then up
     rather than right and then left. A quarter turn on each, so there is still one arrow to maintain. */
  .mtool-collapse svg {
    transform: rotate(90deg);
  }
  #modal.meta-collapsed .mtool-collapse svg {
    transform: rotate(270deg);
  }
  #modal.meta-collapsed #modal-img,
  #modal.meta-collapsed #modal-video,
  #modal.meta-collapsed .mv-wrap,
  #modal.meta-collapsed .modal-linkcard .mv-wrap,
  #modal.meta-collapsed .modal-linkcard #modal-video,
  #modal.meta-collapsed .modal-linkcard .ml-thumb,
  #modal.meta-collapsed .modal-linkcard.bare .mv-wrap,
  #modal.meta-collapsed .modal-linkcard.bare #modal-video,
  #modal.meta-collapsed .modal-linkcard.bare .ml-thumb {
    max-height: 82vh;
  }
}
.mtool {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  background: none;
  border: 0;
  border-radius: 7px;
  color: var(--muted);
  cursor: pointer;
  text-decoration: none;
}
.mtool:hover {
  background: var(--panel-2);
  color: var(--text);
}
.mtool.danger:hover {
  color: var(--danger);
}
.mtool[hidden] {
  display: none;
}
.mtool svg {
  width: 16px;
  height: 16px;
}
.modal-close {
  font-size: 15px;
}
.modal-meta label {
  display: block;
  margin: 16px 0 5px;
  font-size: 12px;
  color: var(--muted);
}
/* The bare "?" beside the Note label. Note is the one text field auto-tagging never writes — applyAi
 * REPLACES the description with the AI summary — and that difference is invisible until it has already
 * eaten something you typed, so it's worth saying next to the field itself. The tooltip is the shared
 * `title` one (tooltip.ts), so this needs no wiring. */
.help-q {
  /* A ring, so the "?" reads as an affordance rather than as punctuation that wandered into the label.
     Built like .thk-key — inline-flex to centre the glyph, --border and --muted so it sits in the same
     family as the other small badges — but circular, which is what a help marker is everywhere else.
     14px against a 12px label: big enough to look deliberate, small enough not to outweigh the word it
     annotates. `line-height: 1` because the default leading pushes a glyph off-centre inside a circle. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  margin-left: 5px;
  border: 1px solid var(--border);
  border-radius: 50%;
  font-size: 10px;
  line-height: 1;
  opacity: 0.6;
  cursor: help;
}
.help-q:hover,
.help-q:focus-visible {
  opacity: 1;
}
/* The modal's text fields (#m-description, dialog inputs) use the shared .field primitive (widgets.css). */
/* The description auto-grows to fit its content (see autogrow), so a long AI write-up opens fully visible
 * rather than behind an inner scrollbar, and a ~4-row floor keeps an empty one from collapsing to a sliver.
 * Scoped by id so the floor can't inflate the tag/combo inputs.
 *
 * The ceiling is the other half of that bargain. Unbounded growth is fine for the few sentences an
 * `extensive` tagging writes, but a pathological description — nine long sentences measured at 496px —
 * pushes Note, Tags and the hotkey legend below the fold and puts this whole panel into scroll. Past 40vh
 * the field scrolls itself instead, which costs one long description its full view and keeps the rest of
 * the panel reachable. */
#m-description {
  min-height: 76px;
  max-height: 40vh;
}
/* No drag handle on the two auto-growing fields. .field offers `resize: vertical` and that is right for a
 * plain textarea, but these two rewrite their own inline height on every `input` — so a height you dragged
 * to survived exactly until the next keystroke, and the grip sat in the corner promising something it
 * could not keep. It also landed beside this panel's own scrollbar once the description was tall, which is
 * how it got noticed. The primitive keeps its handle for textareas that do not auto-grow. */
#m-description,
#m-note {
  resize: none;
}
/* The AI "Auto-tag" tool icon is accent-coloured so it reads as the AI affordance among the
   muted tool icons. `.mtool.busy` pulses the glyph while an action (auto-tag / capture / save
   offline) runs — these are icon buttons now, so there's no text label to swap. */
.mtool.ai {
  color: var(--accent);
}
/* The viewer's ✨ button carries a dropdown (Both / Tags / Description); its wrapper anchors the menu.
   The tools row is right-aligned, so the menu is right-anchored too — it opens leftward into the meta
   panel instead of spilling off the right edge of the window. */
.mtool-menu {
  position: relative;
  display: inline-flex;
}
.ai-menu--modal {
  right: 0;
  left: auto;
}
/* Header ✨ auto-tag icon (left of the help button): accent-coloured so it reads as the AI
   affordance among the muted toolbar icons, and pulses while a background pass is starting.
   Reuses the mtool-pulse keyframe defined just below. */
#autotag-btn {
  color: var(--accent);
}
#autotag-btn:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}
#autotag-btn.busy {
  cursor: default;
}
#autotag-btn.busy svg {
  animation: mtool-pulse 0.9s ease-in-out infinite;
}
.mtool.ai:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
}
.mtool.busy {
  cursor: default;
}
.mtool.busy svg {
  animation: mtool-pulse 0.9s ease-in-out infinite;
}
@keyframes mtool-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.3;
  }
}
.m-meta {
  margin: 12px 0 0;
  padding-bottom: 14px;
  border-bottom: 1px solid var(--border);
  font-size: 12px;
  /* Two shared columns (labels sized to the widest label, values fill the rest) so every value — in
     particular the URL / Offline URL links — starts at the same x and lines up down the panel. */
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 6px 12px;
  align-items: baseline;
}
.m-meta:empty {
  border-top: 0;
  padding-top: 0;
  margin-top: 0;
}
/* Transparent wrapper: its dt/dd flow straight into the .m-meta grid columns so they share a column. */
.m-row {
  display: contents;
}
.m-row dt {
  color: var(--muted);
  white-space: nowrap; /* keep multi-word labels (e.g. "Offline URL") on one line */
}
.m-row dd {
  margin: 0;
  text-align: right;
  font-variant-numeric: tabular-nums;
  word-break: break-word;
  min-width: 0; /* let a long value (the URL link) shrink so it can ellipsise instead of overflowing */
}
/* URL values left-align so their "http…" starts at the value column's left edge, lining the
   URL / Offline URL links up with one another. */
.m-row dd:has(.m-url) {
  text-align: left;
}
/* Source-URL value: a single-line link that ellipsises from the end (host stays visible); full URL in
   the title tooltip, and the "Open original" tool button opens it. */
.m-url {
  display: inline-block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  vertical-align: bottom;
  color: var(--accent);
  text-decoration: none;
}
.m-url:hover {
  text-decoration: underline;
}

/* ---------- tag hotkeys (viewer) ---------- */
.m-tags-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 8px;
}
.m-tags-head label {
  margin: 0;
}
/* The legend now lives at the very bottom of the panel, under Save/Delete — set it off with a rule. */
.thk-head {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.thk-panel {
  margin-top: 8px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
}
.thk-row {
  display: flex;
  align-items: center;
  gap: 7px;
  min-width: 0;
  background: var(--bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: 7px;
  padding: 5px 8px;
  font: 12px system-ui;
  text-align: left;
  cursor: pointer;
}
.thk-row:hover {
  border-color: var(--muted);
}
.thk-row.on {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.thk-key {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  height: 18px;
  padding: 0 4px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  font: 600 11px ui-monospace, monospace;
  color: var(--muted);
}
.thk-row.on .thk-key {
  color: var(--accent);
  border-color: var(--accent);
}
.thk-label {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.thk-mark {
  flex: none;
  color: var(--accent);
  font-size: 11px;
}
.thk-new {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 2px 0;
  font: 12px system-ui;
  color: var(--muted);
}
.thk-empty {
  grid-column: 1 / -1;
  font-size: 12px;
}
/* Edit / Done, in the legend's header rule. Quiet by default — it is used once and then not again for
   months, so it should not compete with the tags themselves. */
.thk-edit {
  flex: none;
  background: none;
  border: 0;
  padding: 0;
  color: var(--muted);
  font: 600 11px system-ui;
  cursor: pointer;
}
.thk-edit:hover {
  color: var(--text);
}
/* A slot in edit mode: same row shape as the legend, with a dropdown where the tag name would be.
   Scoped under .modal-meta to outrank `.modal-meta label` above (0,1,1 beats .thk-row's 0,1,0), which
   would otherwise make each row a block with a 16px top margin and space the ten slots out down the
   panel. Kept as a <label> rather than a <div> so clicking the number focuses its dropdown. */
.modal-meta .thk-edit-row {
  display: flex;
  margin: 0;
  color: var(--text);
  cursor: default;
}
/* The slot's searchable picker. Sized to sit in the legend row where the tag name goes, so edit mode
   is the same ten rows as the legend rather than a differently-shaped panel. */
.thk-row .tagpicker {
  flex: 1;
  min-width: 0;
  min-height: 0;
  padding: 1px 4px;
  border-radius: 5px;
  background: var(--panel-2);
}
.thk-row .tagpicker-field {
  font-size: 12px;
  min-height: 20px;
}
.ic {
  flex: none;
}

/* project create / edit modal */
/* Every dialog has to be listed here — the .pm-card inside carries no positioning of its own, so one
   left out does not look slightly wrong, it lands wherever normal flow puts it (#link-modal sat in the
   bottom-left corner until it was added). */
#proj-modal,
#board-modal,
#board-edit-modal,
#tags-modal,
#link-modal,
#upload-modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
}
.pm-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.6);
}
.pm-card {
  position: relative;
  z-index: 61;
  width: min(92vw, 420px);
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 14px;
  padding: 22px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.6);
}
.pm-card h3 {
  margin: 0 0 14px;
  font-size: 16px;
}
.pm-card label {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--muted);
}
/* A field and the label of the NEXT one sat 6px apart, which read as one group of three lines rather than
   as two questions. The gap is what says where one of them ends. */
.pm-card .field + label,
.pm-card > div + label {
  margin-top: 16px;
}
/* `.pm-card label` above sets display:block for the field labels, and outranks .pm-toggle's own flex —
   which left every checkbox in a dialog sitting flush against its text. The toggle is a label too. */
.pm-card label.pm-toggle {
  display: flex;
}
/* .pm-card dialog text inputs use the shared .field primitive (widgets.css). */
/* Delete warning — shown only while the delete button is armed. */
.pm-warn {
  margin: 16px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--danger);
}
.pm-count {
  margin: 12px 0 0;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.pm-count:empty {
  display: none;
}
/* Auto-tag-on-save checkbox row. */
.pm-toggle {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
  font-size: 13px;
  /* Hug the label to the checkbox: the default 1.5 line-height makes the text's optical centre sit
     below the line-box centre that align-items:center targets, so the label reads as dropped. */
  line-height: 1.3;
  color: var(--text);
  cursor: pointer;
}
.pm-toggle input {
  flex: none; /* keep the checkbox square when a long label wraps */
  width: 15px;
  height: 15px;
  margin: 0;
  padding: 0;
  /* Optical nudge: align-items:center aligns box centres, but the label's line box reserves descender
     space below the text, so the visible glyphs sit slightly low — drop the box 1px to match them. */
  position: relative;
  top: 1px;
  accent-color: var(--accent);
  cursor: pointer;
}
/* Caption under the AI detail selector describing the Auto-tag action. */
.pm-cap {
  margin: 14px 0 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--muted);
}
.pm-cap:empty {
  display: none;
}
/* Settings rows in a dialog — a titled section, then label/description on the left and a switch on the
   right. Used by the board's own Settings dialog (board.html) and by the gallery's moodboard dialog, which
   offers the same settings; it lives here because gallery.css is the sheet both pages load. Class-scoped
   under .pm-card, which beats the `.pm-card label` / `.pm-card input` defaults above. */
.pm-card .be-section {
  margin: 20px 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.pm-card .be-field {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  margin: 12px 0 0;
  cursor: pointer;
}
.pm-card .be-field-text {
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
}
.pm-card .be-field-title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.pm-card .be-field small {
  font-size: 11px;
  color: var(--muted);
}
.pm-card .be-switch {
  flex: none;
  width: 18px;
  height: 18px;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  accent-color: var(--accent);
  cursor: pointer;
}
/* AI section of the settings modal — set off from the project fields by a divider + heading. */
.pm-ai {
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.pm-section-head {
  margin-bottom: 12px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
.pm-sublabel {
  display: block;
  margin-bottom: 6px;
  font-size: 12px;
  color: var(--muted);
}
.seg {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: 8px;
  overflow: hidden;
}
.seg-opt {
  border: 0;
  background: var(--panel-2);
  color: var(--muted);
  font: 600 12px system-ui;
  padding: 7px 14px;
  cursor: pointer;
  transition: background 0.12s ease;
}
.seg-opt + .seg-opt {
  border-left: 1px solid var(--border);
}
.seg-opt:hover {
  color: var(--text);
}
.seg-opt.active {
  background: var(--accent);
  color: #fff;
}
.pm-actions {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-top: 20px;
}
.pm-spacer {
  flex: 1;
}
.pm-actions button {
  border: 1px solid var(--border);
  border-radius: 8px;
  padding: 8px 14px;
  font: 600 13px system-ui;
  background: var(--panel-2);
  color: var(--text);
  cursor: pointer;
}
.pm-actions button:hover {
  border-color: var(--accent);
}
.pm-actions .primary {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
.pm-actions .danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}
.pm-actions .danger:hover {
  background: rgba(229, 72, 77, 0.12);
  border-color: var(--danger);
}
/* Armed (second-click-to-confirm) state — solid red so it clearly reads as destructive. */
.pm-actions .danger.armed,
.pm-actions .danger.armed:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}

[hidden] {
  display: none !important;
}

/* ---------- multi-select action bar ---------- */
#selbar {
  position: fixed;
  left: 50%;
  bottom: 24px;
  transform: translateX(-50%);
  z-index: 40;
  display: flex;
  align-items: center;
  gap: 12px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  padding: 10px 14px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
/* The selection bar's ✨ wrapper needs to anchor its menu, and the bar is a flex row of plain buttons —
   without this the absolutely-positioned menu would hang off the bar itself. */
#selbar .aiwrap {
  position: relative;
  display: inline-flex;
}
#selbar-count {
  font-size: 13px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
/* :not(.ai-opt) because the ✨ menu is a CHILD of this bar — it has to be, to anchor to the button that
   opens it — and without the exclusion every row in it inherited the bar's own button chrome: a border, a
   panel fill and an 8px radius, so three flat menu rows rendered as three white cards. The bar's button
   style is for the bar's own controls. */
#selbar button:not(.ai-opt) {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text);
  border-radius: 8px;
  padding: 7px 12px;
  font: 600 13px system-ui;
  cursor: pointer;
}
#selbar button:not(.ai-opt):hover {
  border-color: var(--accent);
}
/* The bar's two menu buttons. Both carry a caret, which is what says "this opens something" now that the
   labels are plain words — a bar of five controls is read at a glance rather than word by word. */
#selbar-autotag,
#selbar-move-btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
}
#selbar-autotag svg,
#selbar-move-btn svg {
  flex: none;
}
.menu-caret {
  color: var(--muted);
}
/* The toolbar ✨ is an icon button, so its caret sits beside the sparkle rather than after a word. It has
   no fixed width, so this only needs the two to line up. */
#autotag-btn {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}
/* The viewer's ✨ opens the same menu, so it wears the same mark — but .mtool is a 30px square built for
   one glyph, and `.mtool svg` sizes everything inside it to 16px. So: room for two, and a caret that
   stays the 13px it is everywhere else. */
#m-autotag {
  width: auto;
  gap: 3px;
  padding: 0 5px;
}
#m-autotag .menu-caret {
  width: 13px;
  height: 13px;
}
/* `button.danger`, not `.danger`: the base rule above carries a :not(), which lifts its specificity to
   match a class — so a plain .danger selector no longer outranks it and Delete rendered in --text. */
#selbar button.danger {
  color: var(--danger);
  border-color: var(--danger);
}
#selbar button.danger:hover {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
}
.selbar-move {
  position: relative;
}
#selbar-menu {
  position: absolute;
  bottom: calc(100% + 8px);
  left: 0;
  min-width: 180px;
  max-height: 240px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 6px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.selbar-menu-item {
  text-align: left;
  border: 0 !important;
  background: none !important;
  border-radius: 6px !important;
  padding: 7px 10px !important;
  color: var(--text);
  font: 500 13px system-ui !important;
  white-space: nowrap;
  cursor: pointer;
}
.selbar-menu-item:hover {
  background: var(--accent) !important;
  color: #fff;
}
.selbar-menu-empty {
  padding: 7px 10px;
  font-size: 12px;
}

/* Scrims behind the mobile drawers — never shown on desktop (only the ≤860 block reveals them). */
#sidebar-backdrop,
#tagpanel-backdrop {
  display: none;
}

/* ---------- responsive: tablet / phone (≤860px) ---------- */
@media (max-width: 860px) {
  /* Single content column; the sidebar detaches into an off-canvas drawer below. */
  #app {
    grid-template-columns: 1fr;
  }

  /* Sidebar → off-canvas drawer, hidden off the left edge until the topbar toggle slides it in. This
     overrides both the base #sidebar and the desktop `body.sidebar-collapsed #sidebar { display:none }`
     so a collapsed-on-desktop preference doesn't leave the drawer unreachable on a phone. */
  #sidebar,
  body.sidebar-collapsed #sidebar {
    display: flex;
    position: fixed;
    left: 0;
    top: 0;
    bottom: 0;
    width: min(84vw, 300px);
    transform: translateX(-100%);
    transition: transform 0.22s ease;
    /* Above the topbar (30) and selection bar (40); below the modals (50+). */
    z-index: 45;
  }
  body.sidebar-open #sidebar {
    transform: translateX(0);
    box-shadow: 0 0 48px rgba(0, 0, 0, 0.6);
  }
  /* The toggle is always available on mobile — not gated on the desktop collapsed state. */
  #sidebar-expand.sidebar-toggle,
  body.sidebar-collapsed #sidebar-expand {
    display: inline-flex;
  }
  /* Dim, tap-to-close scrim shown only while the drawer is open. */
  #sidebar-backdrop {
    position: fixed;
    inset: 0;
    z-index: 44;
    background: rgba(0, 0, 0, 0.5);
  }
  body.sidebar-open #sidebar-backdrop {
    display: block;
  }

  /* Tag panel → off-canvas drawer on the right, the mirror of the sidebar's. It stops being a grid
     column here (there is only one), so it is laid out and slid the same way: always display:flex,
     parked off the right edge, transformed in by the same body class that revealed the column on
     desktop. Kept out of the `display:none` base rule for that reason — you cannot transition out of
     display:none, and the drawer is the one place this panel animates. */
  #tagpanel,
  body.tagpanel-open #tagpanel {
    display: flex;
    position: fixed;
    right: 0;
    top: 0;
    bottom: 0;
    width: min(84vw, 300px);
    transform: translateX(100%);
    transition: transform 0.22s ease;
    z-index: 45;
  }
  body.tagpanel-open #tagpanel {
    transform: translateX(0);
    box-shadow: 0 0 48px rgba(0, 0, 0, 0.6);
  }
  /* The panel is out of flow now, so opening it must not still try to carve a column out of the row. */
  body.tagpanel-open #app,
  body.sidebar-collapsed.tagpanel-open #app {
    grid-template-columns: 1fr;
  }
  #tagpanel-backdrop {
    position: fixed;
    inset: 0;
    z-index: 44;
    background: rgba(0, 0, 0, 0.5);
  }
  body.tagpanel-open #tagpanel-backdrop {
    display: block;
  }
  /* Hide the Moodboards section from the drawer on mobile — it's a desktop-oriented feature and keeps the
     compact sidebar focused on projects. */
  #side-footer {
    display: none;
  }

  /* Compact two-row toolbar. Row 1 is the control cluster — Filter and Sort on the left, the action
     buttons pinned right; row 2 is the search field stretching the full width beneath them:
         [≡] Filter Sort ········· actions
         Search ···························
     #filters and .tb-right are display:contents so their children (Filter/Search and Sort/actions) drop
     into this grid directly and can interleave across the two rows — a plain stack couldn't put the
     actions beside Sort while Search runs full-width beneath everything. The sort cell (col 3) is the
     1fr, so it absorbs the slack and shoves the actions to the right edge. */
  .tb-row {
    display: grid;
    grid-template-columns: auto auto 1fr auto;
    grid-template-areas:
      "burger filter sort   actions"
      "search search search  search";
    column-gap: 8px;
    row-gap: 10px;
    align-items: center;
  }
  /* The toolbar packs burger + Filter + Sort + the action buttons across a fixed set of grid columns,
     so their combined width — not the search field — is what runs a phone out of room and shoves the
     actions off the right edge. Drop the text labels on Filter and Sort (their icons carry the meaning;
     the sort key label still shows in the dropdown), which frees the ~70px needed to keep the actions
     on-screen down to a 360px viewport. */
  .filter-btn .filter-lbl,
  .sortctl .sort-label {
    display: none;
  }
  /* With no key label, tighten the Sort key segment so it's a snug icon + caret. */
  #sort-key {
    padding: 0 9px;
    gap: 5px;
  }
  #sidebar-expand.sidebar-toggle {
    grid-area: burger;
  }
  /* The width-based column cap (≤4) already equals the size slider's minimum here, so the slider can't
     change anything — hide it to declutter the crowded mobile toolbar. */
  .sizewrap {
    display: none;
  }

  /* #filters collapses to nothing so #filter-btn, its popover and .searchbox become direct grid items of
     .tb-row (see grid-areas above). The facet filters (type / untagged / tags) stay tucked behind the
     Filter button so they don't eat toolbar space; the button's popover holds them. */
  #filters {
    display: contents;
  }
  /* Filter button: row 1, hard left after the hamburger, sized to its content (Search stretches; it must not). */
  #filter-btn {
    grid-area: filter;
    display: inline-flex;
    justify-self: start;
  }
  /* .tb-right collapses too, so Sort and the actions place independently: Sort beside Filter and the
     action buttons pinned to the right edge, all on row 1 above the full-width search. */
  .tb-right {
    display: contents;
  }
  /* Sort sits immediately right of Filter (start of the flexible column), not stretched across it. */
  .sortwrap {
    grid-area: sort;
    justify-self: start;
  }
  .tb-actions {
    grid-area: actions;
    justify-self: end;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  /* The desktop group-separator margin: on this layout Sort is on its own line, so there is nothing left
     to separate from and it would just indent the row. */
  .tb-actions > :first-child {
    margin-left: 0;
  }
  /* No physical keyboard on a phone, so the keyboard-shortcuts button is dead weight — drop it. */
  #help-btn {
    display: none;
  }
  /* Same reason: the viewer's number-key "Tag hotkeys" legend (header + panel) is useless without a
     keyboard — hide it so the detail panel isn't cluttered on mobile. Tags are still managed via the
     Tags field above it. */
  .thk-head,
  #m-hotkeys {
    display: none;
  }
  #filter-menu {
    display: none; /* override the desktop display:contents; the popover is closed until opened */
    position: absolute;
    top: calc(100% + 8px);
    left: 0;
    /* Above the sticky toolbar's own content and the grid scrolling beneath it (mirrors .sort-menu). */
    z-index: 31;
    flex-direction: column;
    gap: 14px;
    /* #filters is now display:contents, so this anchors to the sticky #topbar; left:0 lines it up with the
       toolbar's content edge. Cap the width to the viewport minus the ~22px inset on each side so it never
       runs off the right edge on a narrow phone. */
    width: min(320px, calc(100vw - 44px));
    padding: 14px;
    background: var(--panel-2);
    border: 1px solid var(--border);
    border-radius: 12px;
    box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  }
  body.filter-open #filter-menu {
    display: flex;
  }
  /* Each facet is a label + its control(s); the full-width label forces the controls onto the next row. */
  .filter-group {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 10px;
  }
  .filter-group-label {
    display: block;
    flex-basis: 100%;
    font: 600 12px system-ui;
    color: var(--muted);
  }
  #filter-menu #kinds {
    flex-wrap: wrap;
  }
  /* Search gets row 2 entirely to itself, full width across every column — replacing the desktop 32vw
     cap (unusably narrow on a phone). */
  .searchbox {
    grid-area: search;
    align-self: center;
    width: 100%;
    /* Don't let the <input>'s intrinsic width inflate the shared grid columns (row 1 sizes them). */
    min-width: 0;
  }
  .searchbox #search {
    width: 100%;
    max-width: none;
    min-width: 0;
  }

  /* Viewer: the meta panel becomes a right-hand drawer over the media (phones stack it instead — ≤640). */
  .modal-meta {
    position: fixed;
    right: 0;
    top: 0;
    bottom: 0;
    width: min(88vw, 374px);
    flex-basis: auto;
  }
  .modal-nav.prev {
    left: 6px;
  }
  .modal-nav.next {
    right: calc(min(88vw, 374px) + 6px);
  }
}

/* ---------- responsive: phone (≤640px) ---------- */
@media (max-width: 640px) {
  /* Stack the viewer: media on top, meta as a scrollable sheet below (persistently visible). The ≤860
     right-drawer would cover the image on a phone, so revert the meta to an in-flow panel under the stage. */
  #modal {
    flex-direction: column;
  }
  .modal-stage {
    padding: 12px 40px;
    flex: 1 1 46%;
    min-height: 0;
  }
  #modal-img,
  #modal-video,
  .mv-wrap,
  .modal-linkcard .mv-wrap,
  .modal-linkcard #modal-video,
  .modal-linkcard .ml-thumb,
  .modal-linkcard.bare .mv-wrap,
  .modal-linkcard.bare #modal-video,
  .modal-linkcard.bare .ml-thumb {
    max-height: 42vh;
  }
  .modal-meta {
    position: static;
    width: 100%;
    flex: 1 1 auto;
    align-self: stretch;
    border-left: 0;
    border-top: 1px solid var(--border);
  }
  /* Arrows sit inside the (now top) media area again, clear of the meta sheet. */
  .modal-nav {
    width: 40px;
    height: 56px;
    font-size: 26px;
  }
  .modal-nav.prev {
    left: 4px;
  }
  .modal-nav.next {
    right: 4px;
  }
}

/* ---------- tag combobox (server/public/taginput.ts) ---------- */
.taginput {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  width: 100%;
  min-height: 40px;
  padding: 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: text;
  transition: border-color 0.12s ease;
}
.taginput:focus-within {
  border-color: var(--accent);
}
/* The tag input's inner field is borderless and grows to fill the .taginput shell (not a .field). */
.taginput .taginput-field {
  flex: 1 1 90px;
  min-width: 90px;
  width: auto;
  margin: 0;
  padding: 4px 2px;
  background: transparent;
  border: 0;
  outline: none;
  color: var(--text);
  font: 13px system-ui;
}
.taginput .taginput-field::placeholder {
  color: var(--muted);
}
/* Give the pill and the field the same line-box height so every wrapped row is identical: otherwise
   the field (shorter than a pill) makes a line taller/shorter as pills wrap onto or off of its row,
   which nudges the rows below it vertically each time a tag is added. */
.taginput .chip,
.taginput .taginput-field {
  min-height: 26px;
}
/* Tag pills use the shared .chip primitive (widgets.css); .tp-label / .tp-x are the pill's contents. */
.chip .tp-label {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tp-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  width: 16px;
  height: 16px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: color-mix(in srgb, var(--text) 14%, transparent);
  color: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.tp-x:hover {
  background: color-mix(in srgb, var(--text) 30%, transparent);
}

/* ---------- single-select tag picker (createTagPicker, server/public/taginput.ts) ----------
   Same shell as .taginput so the two read as one family, but one line and one value: no pills to
   wrap, so no flex-wrap and no growing. */
.tagpicker {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  min-height: 32px;
  padding: 2px 6px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: text;
  transition: border-color 0.12s ease;
}
.tagpicker:focus-within {
  border-color: var(--accent);
}
.tagpicker-field {
  flex: 1;
  min-width: 0;
  width: auto;
  margin: 0;
  padding: 2px 0;
  background: transparent;
  border: 0;
  outline: none;
  color: var(--text);
  font: 13px system-ui;
  text-overflow: ellipsis; /* a long tag truncates rather than scrolling the value out of sight */
  cursor: pointer;
}
.tagpicker-field::placeholder {
  color: var(--muted);
}
/* An unset slot reads as a placeholder even though the field holds a real empty value. */
.tagpicker-field.is-empty::placeholder {
  color: var(--muted);
  font-style: italic;
}
.tagpicker-caret {
  flex: none;
  color: var(--muted);
  font-size: 10px;
  pointer-events: none; /* the host's mousedown handles the click; don't let the glyph eat it */
}
/* The row for the tag already on this key, so an open menu shows where you are. */
.menu-item.picked {
  font-weight: 600;
}
.tp-clear {
  color: var(--muted);
  font-style: italic;
}
.menu-item.active .tp-clear {
  color: #fff;
}
/* No horizontal scrollbar. A .menu-item is a flex row, and a flex child defaults to min-width:auto —
   so a tag longer than the field refused to shrink and pushed the menu into scrolling sideways. The
   picker's field is narrow by design (one row of a ten-row legend), which is why it showed up here
   and not on the wider comboboxes. Let the label shrink and ellipsize instead; the menu is also
   widened past the field (minWidth in createMenuPortal) so it rarely has to. */
.tagpicker-menu {
  overflow-x: hidden;
}
.tagpicker-menu .ti-label {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The tag dropdown uses the shared .menu / .menu-item primitives (widgets.css); .ti-* are item contents. */
.ti-swatch {
  flex: none;
  width: 12px;
  height: 12px;
  border-radius: 3px;
}
.ti-create {
  color: var(--muted);
}
.menu-item.active .ti-create,
.menu-item.active .ti-new {
  color: #fff;
}

/* ---------- tag filter: AND/OR match toggle + clear ---------- */
/* Shared .segmented / .segmented__opt shell (widgets.css) — this is the pill variant with a purple active. */
.tagmode {
  /* Match the tag chips' rendered height (12px font + 3px padding + 1px border) so the AND/OR
     toggle lines up flush with them in the filter row. */
  height: 26px;
  border-radius: var(--radius-pill);
}
.tm-opt {
  font: 700 10px var(--font);
  letter-spacing: 0.06em;
  padding: 0 10px;
}
.tm-opt.active {
  background: #8b5cf6;
  color: #fff;
}

/* ---------- tag filter panel ---------- */
/* Opened by #tagpanel-btn in the toolbar's right-hand action group. On desktop it is a real third column
   of #app, so it PUSHES the grid and the cards reflow into what is left — nothing you were looking at
   ends up hidden underneath it. Mirrors #sidebar (same --panel ground, same hairline against the
   content), on the other edge.
   Visibility is body.tagpanel-open and nothing else: the base rule hides it from first paint, before the
   bundle has run, and the mobile block below reuses the same class to slide the drawer. */
#tagpanel {
  display: none;
  flex-direction: column;
  gap: 10px;
  background: var(--panel);
  border-left: 1px solid var(--border);
  padding: 18px 14px;
  overflow: hidden;
  min-width: 0;
}
body.tagpanel-open #tagpanel {
  display: flex;
}
body.tagpanel-open #app {
  grid-template-columns: 248px 1fr var(--tagpanel-w);
}
body.sidebar-collapsed.tagpanel-open #app {
  grid-template-columns: 1fr var(--tagpanel-w);
}
/* Same 36px + 18px padding as the sidebar brand row and the toolbar's control row, so the panel title
   sits on the line the logo and the search box are already on. */
.tagpanel-head {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 36px;
}
.tagpanel-title {
  flex: 1;
  min-width: 0;
  margin: 0;
  font-size: 16px;
  font-weight: 650;
}
.tagpanel-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  font-size: var(--fs-sm);
  min-height: 26px;
  /* Breaks out of the panel's padding and re-insets it, exactly as .tagpanel-search does above, so its
     shadow runs the full width.
     More top margin than the head's 6px, deliberately, so the two are NOT symmetric: the head is a field
     you type into and the list follows it, while this row holds a count and a button, and at 6px the
     "N selected" line sat tight under the last tag row and read as another entry in the list rather than
     a summary of it. 14px + the panel's 10px flex gap. */
  margin: 14px -14px 0;
  padding: 0 14px;
  transition: box-shadow 0.18s ease;
}
/* The mirror of .tagpanel-search.scrolled: cast UPWARD, and shown while there is still list below the
   fold rather than above it. Without it the last visible row looked like the last row — the same
   ambiguity at the bottom that the head's shadow removes at the top, and the more misleading of the two,
   since a tag list you have scrolled to the end of is the one case where you would go looking elsewhere
   for a tag that was there all along. */
.tagpanel-foot.more {
  box-shadow: 0 -1px 0 rgba(0, 0, 0, 0.35), 0 -8px 16px -8px rgba(0, 0, 0, 0.7);
}
/* Sized to its own two options — it is a choice between two words, not a pair of buttons to aim at, and
   the field beside it takes the rest. */
#tagpanel #tag-mode {
  flex: none;
}
/* The panel's search row: the field, and the AND/OR toggle when there is one. Same leading-magnifier
   geometry as .searchbox in the toolbar — the icon is absolutely placed against this box and the field
   is padded past it — without .searchbox's flex sizing, since this row is simply as wide as the panel. */
.tagpanel-search {
  position: relative;
  display: flex;
  align-items: center;
  gap: 8px;
  /* Break out of the panel's 14px padding and re-inset it, the way #topbar does with #main's 22px, so
     the scroll shadow below runs the full width of the panel instead of stopping 14px short at each
     edge. The field itself lands back where it was, lined up with the tag rows under it. */
  margin: 0 -14px 6px;
  padding: 0 14px;
  /* 6px on top of the panel's 10px flex gap. At 10 the first tag row sat right under the field — and
     under the scroll shadow, which needs somewhere to fall before the list starts. The toolbar gives
     search 27px before the first card; this is the panel's version of that. */
  transition: box-shadow 0.18s ease;
}
.tagpanel-search #tagpanel-q {
  flex: 1;
  min-width: 0;
}
/* The tag list is long by design, and with the head pinned there was nothing to say the list was already
   scrolled — the first visible row looked like the first row. Same idea as #topbar.scrolled over the
   grid, and the same 0.18s fade: a hairline plus a short fall-off, lighter than the toolbar's because it
   sits on --panel rather than the near-black --bg and has less width to carry. JS toggles the class. */
.tagpanel-search.scrolled {
  box-shadow: 0 1px 0 rgba(0, 0, 0, 0.35), 0 8px 16px -8px rgba(0, 0, 0, 0.7);
}
.tagpanel-search .search-ic {
  position: absolute;
  /* 14px back past the negative margin, then the same 11px into the field that .searchbox uses. */
  left: 25px;
  display: inline-flex;
  color: var(--muted);
  pointer-events: none;
}
.tagpanel-search .search-ic .ic {
  width: 15px;
  height: 15px;
}
.tagpanel-search #tagpanel-q {
  padding-left: 34px;
}
/* Overrides .tags-list's fixed 340px — that height exists so the manage-tags MODAL doesn't resize while
   you filter it. Here the list takes whatever the column has left, so the tags scroll inside the panel
   and the head/foot stay put. */
.tagpanel-list {
  flex: 1;
  height: auto;
  min-height: 0;
}
/* The rows are <button>s, not the modal's <div>s — one tap target per tag, reachable by keyboard. Strip
   the button chrome and let .tag-row (shared with the manage-tags list) do the layout. */
.tagpanel-row {
  width: 100%;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* :hover is listed too, and deliberately identical. .tag-row:hover — shared with the manage-tags list —
   is declared further down this file, so at equal specificity it won the cascade and repainted a
   selected row in the plain hover grey: pointing at a tag made it look switched off. Selection is state
   and hover is a pointer position; the state wins. */
.tagpanel-row.on,
.tagpanel-row.on:hover {
  background: color-mix(in srgb, var(--accent) 16%, transparent);
}
.tagpanel-row.on .tag-name {
  font-weight: 650;
}
.tagpanel-row:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
/* The toolbar button. The badge rides its top-right corner so the selected-tag count is legible while
   the panel is shut — the panel is off-screen most of the time, and a filter you cannot see is a filter
   you forget is on. */
.tagpanel-btn {
  position: relative;
  overflow: visible;
}
.tagpanel-btn .filter-count {
  position: absolute;
  top: -3px;
  right: -3px;
  pointer-events: none;
}
.tagpanel-btn.active,
.tagpanel-btn[aria-expanded="true"] {
  color: var(--accent);
}

/* ---------- first-run overlay ---------- */
/* Reuses .help-modal / .help-backdrop / .help-card wholesale — same scrim, same card, same close — so
   the one new thing on screen is the row of three steps. Only the width differs: .help-card is
   min(480px, 92vw), and three cards do not fit in 480. */
.wel-card {
  width: min(760px, 92vw);
}
.wel-title {
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.wel-title h3 {
  margin: 0;
  font-size: 15px;
}
.wel-title span {
  font-size: var(--fs-sm);
  color: var(--muted);
}
/* The head is built for one line; this one carries a sub-line, so its ✕ aligns to the top of the pair. */
.wel-close {
  align-self: flex-start;
}
.wel-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
}
.wel-step {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 16px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}
/* The step number rides on the title's line rather than a line of its own — a numeral alone above a
   heading reads as a heading of its own, and cost every card a row to say one character. */
.wel-n {
  color: var(--muted);
}

/* Each step's picture: a small drawing of the moment itself rather than a glyph standing in for it —
   the toolbar the extension installs into, a marquee dragged across a page, tags arriving on a save.
   Drawn in CSS on the app's own tokens rather than shipped as images: at this size a screenshot is
   unreadable, three files would have to be kept in step with a UI that moves, and nothing exists to
   photograph for the first two anyway. */
.wel-vis {
  position: relative;
  height: 116px;
  padding: 10px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}

/* 1 — a browser toolbar, with the extension's piece lit in it. */
.wv-bar {
  display: flex;
  align-items: center;
  gap: 6px;
}
.wv-dot {
  width: 7px;
  height: 7px;
  border-radius: var(--radius-pill);
  background: var(--border);
}
.wv-addr {
  flex: 1;
  height: 16px;
  margin-left: 4px;
  border-radius: var(--radius-pill);
  background: var(--panel-2);
}
.wv-piece {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 7px;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  color: var(--accent);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 34%, transparent);
}
.wv-piece .ic {
  width: 15px;
  height: 15px;
}
.wv-tiles {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 5px;
  margin-top: 10px;
}
.wv-tiles span {
  height: 44px;
  border-radius: 5px;
  background: var(--panel-2);
}

/* 2 — a page mid-drag. The rectangle is the accent and the two pictures it swept up are green with their
   badge numbers, because that is exactly what the overlay does (select.ts: #marquee and .sel). */
.wv-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 6px;
}
.wv-grid span {
  position: relative;
  height: 38px;
  border-radius: 5px;
  background: var(--panel-2);
}
.wv-picked {
  border: 2px solid var(--green);
  background: color-mix(in srgb, var(--green) 14%, var(--panel-2)) !important;
}
.wv-picked i {
  position: absolute;
  top: -6px;
  left: -6px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  border-radius: var(--radius-pill);
  background: var(--green);
  color: #fff;
  font: 700 9px var(--font);
  font-style: normal;
}
/* Over the picture it is about to pick, not trailing a rectangle: one click is one picture. */
.wv-cursor {
  position: absolute;
  left: 96px;
  top: 26px;
  color: var(--text);
}
.wv-cursor .ic {
  width: 16px;
  height: 16px;
  fill: var(--panel);
}

/* 3 — what the pass wrote, and the grid it wrote it onto. Tags first: they are the thing this step is
   about, and underneath them is the library rather than one blank rectangle. */
.wv-tags {
  display: flex;
  align-items: center;
  gap: 5px;
}
/* Ragged columns on purpose — the gallery is a masonry, so equal tiles would be the wrong picture of it.
   Three independent columns rather than a grid: grid rows share a track height, which lines every tile's
   top and bottom up with its neighbours' and produces exactly the tidy rows this is not. The overflow is
   the point too, since a library carries on past the frame. */
.wv-masonry {
  display: flex;
  gap: 5px;
  margin-top: 9px;
}
.wv-masonry div {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 5px;
}
.wv-masonry span {
  flex: none;
  border-radius: 4px;
  background: var(--panel-2);
}
/* Nine tiles, nine heights — written out rather than generated, so the shape is the same every time this
   is drawn and can be judged by eye once. */
.wv-masonry div:nth-child(1) span:nth-child(1) {
  height: 30px;
}
.wv-masonry div:nth-child(1) span:nth-child(2) {
  height: 18px;
}
.wv-masonry div:nth-child(1) span:nth-child(3) {
  height: 26px;
}
.wv-masonry div:nth-child(2) span:nth-child(1) {
  height: 20px;
}
.wv-masonry div:nth-child(2) span:nth-child(2) {
  height: 32px;
}
.wv-masonry div:nth-child(2) span:nth-child(3) {
  height: 22px;
}
.wv-masonry div:nth-child(3) span:nth-child(1) {
  height: 24px;
}
.wv-masonry div:nth-child(3) span:nth-child(2) {
  height: 22px;
}
.wv-masonry div:nth-child(3) span:nth-child(3) {
  height: 28px;
}
/* The two hues a tag pill actually takes in the grid — a tag's colour is derived from its name, so these
   are two out of that set rather than colours invented for the drawing. */
.wv-tag {
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font: 600 10px var(--font);
}
.wv-tag--a {
  background: color-mix(in srgb, #8b5cf6 20%, transparent);
  color: color-mix(in srgb, #8b5cf6 80%, var(--text));
}
.wv-tag--b {
  background: color-mix(in srgb, var(--green) 20%, transparent);
  color: color-mix(in srgb, var(--green) 80%, var(--text));
}
.wv-spark {
  display: inline-flex;
  color: var(--accent);
}
.wv-spark .ic {
  width: 14px;
  height: 14px;
}
.wel-h {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-weight: 600;
  /* One line, always: these are three short phrases in a column each wide enough for them, and a title
     that wraps drops the card's whole stack by a row against its neighbours. */
  white-space: nowrap;
}
/* Grows so the three cards' bottom rows line up whatever each one carries — a button, keys, a checkbox
   — and so card 1 keeps its shape when there is no install link to render a button for. */
.wel-p {
  margin: 0;
  flex-grow: 1;
  font-size: var(--fs-sm);
  line-height: 1.5;
  color: var(--muted);
}
.wel-btn {
  justify-content: center;
}
.wel-keys {
  display: flex;
  gap: 5px;
}
.wel-keys kbd {
  padding: 3px 7px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  font: 600 11px var(--font);
}
/* .pm-toggle carries a 16px top margin for the settings dialog it was written for; here the card's own
   gap does that job. */
.wel-toggle {
  margin-top: 0;
}
.wel-foot {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 18px;
}
/* "Don't show again" is .btn--icon for its bare look, but it is a phrase rather than a glyph — give it
   the horizontal padding a label needs. */
.wel-secondary {
  padding: 0 12px;
}
/* Under the layout breakpoint three columns cannot hold their words; stack them, and the modal scrolls
   inside the max-height .help-card already sets. */
@media (max-width: 720px) {
  .wel-cards {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* ---------- manage tags modal ---------- */
.tags-card {
  width: min(520px, 92vw);
  display: flex;
  flex-direction: column;
  gap: 12px;
  max-height: 82vh;
}
.tags-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.tags-head h3 {
  margin: 0;
}
.tags-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  font-size: 15px;
  cursor: pointer;
}
.tags-close:hover {
  background: var(--bg);
  color: var(--text);
}
.tags-scope-label {
  margin-top: -4px;
  font-size: var(--fs-sm);
}
.tags-list {
  /* Fixed height so filtering (fewer rows) never shrinks the modal and shifts the layout. */
  height: 340px;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 1px;
  /* Same slim, arrow-less scrollbar as the dropdown menus / tag combobox (widgets.css .menu). */
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.tags-list::-webkit-scrollbar {
  width: 11px;
}
.tags-list::-webkit-scrollbar-track {
  background: transparent;
}
.tags-list::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: var(--radius-pill);
  border: 3px solid transparent;
  background-clip: padding-box;
}
.tags-list::-webkit-scrollbar-thumb:hover {
  background: var(--muted);
  background-clip: padding-box;
}
.tags-list::-webkit-scrollbar-button {
  display: none;
}
.tag-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 6px;
  border-radius: 8px;
  /* Fixed height so swapping the row's actions (icon buttons ⇄ Cancel/Delete text buttons) in delete
     mode never changes the row height and shifts the rows below it. */
  min-height: 38px;
}
.tag-row:hover {
  background: var(--bg);
}
.tag-dot {
  flex: none;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.tag-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tag-count {
  color: var(--muted);
  font-size: var(--fs-sm);
  font-variant-numeric: tabular-nums;
}
.tag-row-actions {
  display: flex;
  align-items: center;
  gap: 4px;
  flex: none;
}
.tag-act {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}
.tag-act:hover {
  background: var(--panel);
  color: var(--text);
}
.tag-act.danger:hover {
  background: color-mix(in srgb, var(--danger, #e5484d) 18%, transparent);
  color: var(--danger, #e5484d);
}
.tag-act .ic {
  width: 15px;
  height: 15px;
}
/* Reveal the row's rename/delete buttons on hover only — keep the list calm at rest. */
.tag-row .tag-row-actions {
  opacity: 0;
  transition: opacity 0.1s ease;
}
.tag-row:hover .tag-row-actions,
.tag-row:focus-within .tag-row-actions {
  opacity: 1;
}
.tag-rename-input {
  flex: 1;
  min-width: 0;
  height: 28px;
}
/* Text buttons for the inline delete confirm (the .tag-act icon buttons are 26px squares — too
   cramped for words). */
.tag-cbtn {
  flex: none;
  padding: 3px 11px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel-2);
  color: var(--text);
  font: 600 12px system-ui;
  cursor: pointer;
  white-space: nowrap;
}
.tag-cbtn:hover {
  border-color: var(--accent);
}
.tag-cbtn.danger {
  background: transparent;
  border-color: var(--danger);
  color: var(--danger);
}
.tag-cbtn.danger:hover {
  background: color-mix(in srgb, var(--danger) 14%, transparent);
}
/* While confirming a delete, keep the row's actions visible even if the pointer leaves the row. */
.tag-row.confirming .tag-row-actions {
  opacity: 1;
}
.tags-empty {
  padding: 20px;
  text-align: center;
}

/* ---------- adding items from the app: the uploads tray + the drop overlay ---------- */
/* Deliberately a corner tray and not a modal — see docs/backlog/add-from-app.md. It sits above the grid
   and the selection bar (40) and below everything that covers them: the mobile sidebar drawer and its
   scrim (45 / 44), the item viewer (50+) and the dialogs (60+). It used to tie with the viewer at 50 and
   win on DOM order, which put a corner panel on top of the viewer's own sidebar — over the tags and the
   project of the very item being looked at. Work does not stop when it is covered, and the header carries
   the counts the moment the viewer closes. */
#uploads {
  position: fixed;
  right: 18px;
  bottom: 18px;
  z-index: 43;
  width: min(92vw, 340px);
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 12px;
  box-shadow: 0 16px 48px rgba(0, 0, 0, 0.4);
  overflow: hidden;
}
.up-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--border);
}
.up-title {
  font-size: 13px;
  font-weight: 650;
  color: var(--text);
}
.up-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--muted);
  cursor: pointer;
}
.up-close:hover {
  background: var(--panel);
  color: var(--text);
}
/* Wider than the square icon buttons beside it, because it is a word: "Clear" has to be readable as the
   one control that removes a row — the ✕ next to it closes the panel and leaves every row where it is. */
.up-clear {
  padding: 2px 8px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.up-clear:hover {
  background: var(--panel);
  color: var(--text);
}
/* Stop all. Bordered and on --panel so it reads as a button among the borderless icon controls beside it,
   and in --danger text rather than a filled red block: a tray with work running is the normal state, and a
   red slab in the corner of every busy moment is alarm fatigue. It fills only once armed (.armed), which
   is the one moment it IS the dangerous choice. */
.up-stopall {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel);
  color: var(--danger);
  font: 600 11px system-ui;
  cursor: pointer;
}
.up-stopall:hover {
  border-color: var(--danger);
}
.up-stopall.armed {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}
.up-confirm {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--panel);
  border-bottom: 1px solid var(--border);
}
.up-confirm-text {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  line-height: 1.45;
  color: var(--text);
}
/* The reassurance half of the question, quieter than the question itself. */
.up-confirm-note {
  color: var(--muted);
}
.up-confirm-actions {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex: none;
}
.up-confirm-btn {
  padding: 3px 9px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel-2);
  color: var(--text);
  font: 600 11px system-ui;
  cursor: pointer;
}
.up-confirm-btn.danger {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}
/* The square stop is a glyph, not a character: it needs the muted-until-hover treatment the ✕ gets from
   `color: inherit`, but it also has to turn danger-coloured on hover, because that hover IS the moment
   the person is deciding to interrupt something. */
.up-stop.stop:hover {
  color: var(--danger);
}
.up-head-actions {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  flex: none;
}
/* Collapsed keeps the header — and therefore the counts — so folding a long batch away never costs you
   sight of whether it is still running. */
#uploads.collapsed #up-body {
  display: none;
}
#up-toggle svg {
  transition: transform .15s ease;
}
#uploads.collapsed #up-toggle svg {
  transform: rotate(-90deg);
}
/* What is about to be added, in the confirm dialog. Capped and scrolled: dropping a folder of 200 files
   must not grow the card past the viewport and push its own buttons off screen. */
.up-filelist {
  max-height: 180px;
  overflow-y: auto;
  margin: 4px 0 16px;
  padding: 8px 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  font-size: 12px;
  color: var(--muted);
  line-height: 1.7;
}
.up-filelist div {
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.up-rows {
  max-height: 40vh;
  overflow-y: auto;
  padding: 8px 12px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* A row is an item: an icon for what it is, its name, and a quiet line under it with size and age — the
   shape of a browser's download tray, which is the thing people already know how to read here. */
.up-row {
  display: flex;
  align-items: flex-start;
  gap: 9px;
  font-size: 12px;
  color: var(--muted);
}
.up-ic {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  margin-top: 1px;
  color: var(--muted);
  font-size: 13px; /* the ✨ / 🔖 / 🖼 a whole-pass row uses instead of an icon */
  line-height: 1;
}
.up-ic svg {
  width: 15px;
  height: 15px;
}
.up-row.done .up-ic {
  color: var(--ok, #17b877);
}
.up-row.err .up-ic {
  color: var(--danger);
}
/* Stopped is deliberately NOT the danger colour. The person asked for this; a red row would report their
   own decision back to them as a fault, which is how a tray teaches people to stop reading it. Muted, like
   the state text beside it — present, unalarming, and clearly not a ✓. */
.up-row.stopped .up-ic {
  color: var(--muted);
}
/* min-width:0 so the name's ellipsis has something to bite on inside the flex row. */
.up-info {
  flex: 1;
  min-width: 0;
}
.up-meta {
  margin-top: 1px;
  font-size: 11px;
  color: var(--muted);
}
/* Sits in the name's line, so a row in flight is one line tall like every other row. */
.up-row .up-bar {
  flex: none;
  width: 64px;
  height: 4px;
  background: var(--panel);
  border-radius: 3px;
  overflow: hidden;
}
.up-row .up-fill {
  height: 100%;
  background: var(--accent);
  border-radius: 3px;
  transition: width .25s ease;
}
.up-row.done .up-fill {
  background: var(--ok, #17b877);
}
.up-row.err .up-fill {
  background: var(--danger);
}
.up-line {
  display: flex;
  align-items: center;
  gap: 8px;
}
/* The name is the part that identifies the row, so it keeps the space and the state gives way. */
.up-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--text);
}
.up-state {
  flex: none;
  display: inline-flex;
  align-items: center;
  gap: 6px;
}
.up-row.err .up-state,
.up-row.err .up-why {
  color: var(--danger);
}
.up-why {
  margin-top: 3px;
  font-size: 11px;
  line-height: 1.35;
}
.up-retry {
  padding: 2px 8px;
  border: 1px solid var(--border);
  border-radius: 5px;
  background: var(--panel-2);
  color: var(--text);
  font: 600 11px system-ui;
  cursor: pointer;
}
.up-retry:hover {
  border-color: var(--accent);
  color: var(--accent);
}
/* Stop / dismiss on a server job row — an AI or import pass can be aborted mid-flight, and a failed job
   can be cleared away. Uploads have no equivalent: they are this tab's, and closing the tray drops them. */
.up-stop {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  border-radius: 4px;
  background: none;
  color: inherit;
  font-size: 13px;
  line-height: 1;
  cursor: pointer;
}
.up-stop:hover {
  background: var(--panel);
  color: var(--text);
}

/* Full-window overlay while a file drag is over the page. Full-window rather than grid-only because a
   drag that lands two pixels outside the grid should still work — the target is "this app", not a box. */
#dropzone {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: grid;
  place-items: center;
  background: color-mix(in srgb, var(--bg, #0c0e12) 72%, transparent);
  backdrop-filter: blur(2px);
}
.dz-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 34px 46px;
  border: 2px dashed var(--accent);
  border-radius: 16px;
  background: var(--panel);
  color: var(--text);
  font-size: 15px;
  font-weight: 650;
}
.dz-ic svg {
  width: 30px;
  height: 30px;
  color: var(--accent);
}

/* tooltip (.jb-tip), buttons, fields, menus, chips, and segmented controls now live in widgets.css */
