* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 14px;
  background: var(--bg);
  color: var(--text-primary);
  -webkit-font-smoothing: antialiased;
}

a { color: inherit; text-decoration: none; }

/* Form controls inherit neither font-family nor colour from body, so an unclassed <button> falls back to
   the browser's system ButtonText. That colour knows nothing about the .dark class — the browser still
   reports color-scheme: normal — so in dark mode it arrives as black on a near-black panel. Every .btn-*
   variant sets its own colour; this reset is what stops the next unstyled control from going invisible. */
button, input, select, textarea, optgroup, option { font-family: inherit; color: inherit; }

/* ---------- Glass surface (ui.md §2.4) ---------- */
.glass {
  background: var(--surface);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  border: 1px solid var(--border);
  border-radius: var(--radius-glass);
}

.card { padding: 16px; }
.card.compact { padding: 12px; }
.card.spacious { padding: 24px; }

/* ---------- Shell (ui.md §4) — top-nav only, no sidebar ---------- */
.app-shell { display: flex; flex-direction: column; height: 100vh; overflow: hidden; }

.top-nav {
  height: 48px; flex-shrink: 0; display: flex; align-items: center; gap: 16px;
  padding: 0 16px; background: var(--nav-bg); border-bottom: 1px solid var(--nav-border);
  backdrop-filter: blur(var(--glass-blur));
  /* 45, and NOT the 40 the title bar uses. Equal z-indexes are resolved by DOM order, and the title bar
     comes later — so at 40 the bar painted over the whole top nav, and the user popup (z-index 60, but
     inside THIS stacking context) could not out-rank it however high its own z-index went. That is how the
     popup ended up behind the filter bar. The ladder is now:
       content < .page-title-bar 40 < .top-nav 45 < .overlay 50 < .dialog 51 < .lightbox 200
     The nav outranks the title bar because its menu hangs down over the page, while a dialog must still
     cover the whole shell. */
  position: relative; z-index: 45;
}
.top-nav .brand { display: inline-flex; align-items: center; gap: 10px; }
.top-nav .brand-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 26px; height: 26px; border-radius: 7px;
  /* 14px, not the 10px the three-letter monogram used: the mark is a fixed 26px square, so dropping to two
     letters left it visibly loose in its own box (measured 55% of the width used; 14px puts it back at ~63%
     with even side padding, which is where the three-letter version sat). The tab icon's ZZ is proportioned
     the same way. */
  font-size: 14px; font-weight: 700; letter-spacing: 0.02em; color: #fff;
  background: var(--brand-gradient);
}

/* Brand wordmark — one definition, used by the shell's brand and by the login screens' heading so the
   two cannot drift. Lilita One is a single-weight display face; the gradient is the theme's brand
   gradient clipped to the text. The plain `color` below is deliberate: `color: transparent` only ever
   applies inside the @supports block, so a browser without background-clip: text shows a readable blue
   name instead of nothing at all. */
.wordmark {
  font-family: 'Lilita One', var(--font-sans);
  font-weight: 400; letter-spacing: 0.01em; line-height: 1;
  color: var(--brand);
}
.top-nav .brand-name { font-size: 19px; }
/* Owns its own size (and weight, which Lilita One has only one of) rather than inheriting the generic
   .login-card h1 rule — that rule also renders "Access denied", which is not a wordmark. */
.login-card .wordmark { font-size: 20px; font-weight: 400; margin: 8px 0 2px; }

@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  .wordmark {
    background: var(--brand-gradient);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

/* Windows High Contrast / forced colours repaint a transparent fill as nothing, which would erase the
   wordmark; hand it the system text colour and drop the clipped gradient. */
@media (forced-colors: active) {
  .wordmark { background: none; color: CanvasText; -webkit-text-fill-color: CanvasText; }
}
.top-nav-right { margin-left: auto; display: flex; align-items: center; gap: 10px; }
/* ---------- Screen-reader-only ----------
   For content that has to exist for assistive tech but must not be painted: the Issue lists' page heading,
   which is the document's own <title> and would otherwise be static text in the bar. `position: absolute`
   rather than display:none — display:none removes it from the accessibility tree too, which is the opposite
   of the point. The clip pair covers old and new engines; `white-space: nowrap` stops a hidden label from
   wrapping to several 1px-tall lines that a reader would hear as one. */
.sr-only {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; white-space: nowrap; border: 0;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}
/* ---------- Active Project scope (left of the title bar, 2026-09-15) ----------
   Moved here from the top nav on request: it acts on the list the way the filters do, so it sits with them —
   but in the BAR rather than in `.page-title-actions`, which is a full right-aligned group:
   `Issues [Project PMI] ──────────── [filters] [search] [Clear] [+ Add Issue]`.
   Its control is 26px, not the 30px it was in the 48px top nav, so it lines up with the picker chips beside
   it. */
.project-scope { display: inline-flex; align-items: center; gap: 8px; }
.project-scope-label {
  font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--text-muted);
}
.project-scope select {
  height: 26px; max-width: 260px; padding: 0 8px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface);
  font-size: 13px; color: var(--text-primary); cursor: pointer;
}
.project-scope select:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* One assigned Project: text, not a one-option dropdown — there is nothing to choose, and a control that
   cannot change anything is a lie. The code is the Project's identity everywhere else in the shell (the
   popup's chips), and the title carries the full name. */
.project-scope-static {
  font-size: 13px; font-weight: 500; color: var(--text-secondary); white-space: nowrap;
}

/* ---------- User avatar + popup menu ---------- */
.user-menu { position: relative; }
.user-trigger {
  display: inline-flex; align-items: center; gap: 8px; height: 36px;
  padding: 0 8px 0 6px; border: 1px solid transparent; border-radius: 999px;
  background: transparent; color: var(--text-primary); cursor: pointer;
  transition: background 140ms ease, border-color 140ms ease;
}
.user-trigger:hover,
.user-trigger[aria-expanded="true"] { background: var(--surface-elevated); border-color: var(--border); }
.user-trigger:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* One line, and only the name: the trigger is a control in a 36px pill, not a profile card — the role
   lives in the popup's header. nowrap keeps a long display name from breaking the pill over two lines. */
.user-trigger-name { font-size: 13px; font-weight: 500; white-space: nowrap; }

/* The ▾ that says "there is a menu behind this". One definition, two users — the top nav's user trigger
   and the filter bar's pickers — because they are the same affordance and must not drift apart.

   DRAWN rather than the ▾ character, which is what made these hard to see: at 9px in --text-muted it read
   as a smudge, and the glyph is not even the app's own — Noto Sans Thai has no ▾, so the browser
   substitutes a symbol font whose triangle is small and thin and differs per platform. A border triangle
   is sized in pixels, cannot be affected by font fallback, and takes the app's own colour. The ▾ character
   was removed from the markup with it, because text would sit on top of the shape.

   --text-secondary, not --text-primary: clearly legible (≈6.5:1 on the trigger's surface) without
   competing with the label it belongs to. */
.caret,
.filter-pick-caret {
  display: inline-block; width: 0; height: 0; flex-shrink: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 5px solid var(--text-secondary);
}

.avatar {
  display: inline-flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0;
  font-size: 11px; font-weight: 600; letter-spacing: 0.02em; color: #fff;
  background: var(--brand-gradient);
}
.avatar.lg { width: 38px; height: 38px; font-size: 14px; }

.user-pop {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 248px; padding: 6px; border-radius: 12px;
  border: 1px solid var(--border);
  /* Opaque: the menu floats over table content, so it must not be see-through. */
  background: var(--surface-solid);
  backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-saturate));
  box-shadow: 0 16px 40px rgba(0, 0, 0, 0.16), 0 2px 8px rgba(0, 0, 0, 0.06);
}
.user-pop[hidden] { display: none; }
.user-pop-head { display: flex; align-items: center; gap: 10px; padding: 8px 8px 10px; }
.user-pop-name { font-size: 14px; font-weight: 600; }
.user-pop-role { font-size: 11px; color: var(--text-muted); }
.user-pop-section {
  padding: 8px 8px 4px; font-size: 10px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-muted);
}
.user-pop-item {
  display: flex; align-items: center; gap: 10px; width: 100%;
  padding: 8px; border: none; border-radius: 8px; background: transparent;
  font-family: inherit; font-size: 13px; color: var(--text-primary);
  text-align: left; cursor: pointer; transition: background 120ms ease;
}
.user-pop-item:hover { background: var(--surface-elevated); }
.user-pop-item.danger { color: var(--danger); }
.user-pop-item .grow { flex: 1; }
.user-pop-mode { font-size: 11px; color: var(--text-muted); }
/* Material Symbols, the same outline style the rest of the app's icons are drawn in. The glyph is a
   LIGATURE, so the element's text content is the icon's name ("logout") and must not be read out or
   styled as a word: aria-hidden on the markup, and the metrics below reset what a text font would
   otherwise add. Coloured by currentColor, so it follows .user-pop-item's own colour — which is what
   makes Logout's icon red along with its label. */
.mi {
  font-family: 'Material Symbols Outlined';
  font-weight: normal; font-style: normal;
  font-size: 18px; line-height: 1;
  letter-spacing: normal; text-transform: none; white-space: nowrap; word-wrap: normal; direction: ltr;
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
  -webkit-font-feature-settings: 'liga'; font-feature-settings: 'liga';
  -webkit-font-smoothing: antialiased;
  flex-shrink: 0;
}
.user-pop-sep { height: 1px; margin: 6px 4px; background: var(--border); }
.user-pop-chips { display: flex; flex-wrap: wrap; gap: 4px; padding: 0 8px 4px; }
.user-pop form { margin: 0; }

.chip {
  display: inline-flex; align-items: center; padding: 2px 8px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface-elevated);
  font-size: 11px; font-weight: 500; color: var(--text-secondary);
}
.chip-active { border-color: transparent; background: rgba(0, 81, 195, 0.1); color: var(--brand); }

.page-title-bar {
  height: 40px; flex-shrink: 0; display: flex; align-items: center; gap: 10px;
  padding: 0 16px; background: var(--titlebar-bg); border-bottom: 1px solid var(--nav-border);
  backdrop-filter: blur(var(--glass-blur));
  /* Same treatment as .top-nav, and load-bearing: backdrop-filter already makes this a stacking context,
     so without an explicit z-index the bar sits at `auto` and the later-in-DOM .content-area paints over
     anything the bar lets hang out of it — the filter pickers' menus were being covered by the table
     header. 40 keeps it under .overlay (50) and .dialog (51), and under .top-nav (45), whose user popup
     has to draw over this bar. */
  position: relative; z-index: 40;
}
.page-title-bar h1 { font-size: 14px; font-weight: 500; margin: 0; }
.page-title-actions { margin-left: auto; display: flex; align-items: center; gap: 8px; }

.content-area { flex: 1; overflow: auto; padding: 24px; }

@media (max-width: 760px) {
  .top-nav { padding: 0 10px; gap: 8px; }
  .top-nav .brand-name { display: none; }
  .user-trigger-name { display: none; }
  .project-scope-label { display: none; }
  .project-scope select { max-width: 150px; }
  .content-area { padding: 16px; }
  /* .content-area drops to 16px padding, so the shell above the list card is 16px shorter. */
  .list-scroll {
    height: calc(100vh - 124px);
    height: calc(100dvh - 124px);
  }
}

/* ---------- Buttons (ui.md §5.1) ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 6px;
  height: 36px; padding: 0 16px; border-radius: 6px; border: none;
  font-size: 13px; font-weight: 500; cursor: pointer; transition: all 200ms ease;
  /* A control's label must never break mid-label. This also floors the button's automatic minimum size
     (min-width:auto resolves to min-content, which for nowrap is the whole label), so a button in a tight
     flex row keeps its natural width and lets a flexible neighbour take the pressure instead. Before
     this, the filter bar's "✕ Clear" — the only item in that row without a width — was squeezed from
     66px to 50px and broke over two lines inside a 26px-tall button. */
  white-space: nowrap;
}
.btn:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
.btn.sm { height: 26px; padding: 0 10px; font-size: 12px; border-radius: 4px; }
.btn-primary { background: linear-gradient(to right, #3b82f6, #8b5cf6); color: #fff; }
.btn-primary:hover { filter: brightness(1.1); }
.btn-secondary { background: var(--surface-elevated); color: var(--text-primary); }
.btn-secondary:hover { background: var(--border-strong); }
.btn-danger { background: linear-gradient(to right, #ef4444, #f43f5e); color: #fff; }
.btn-danger:hover { filter: brightness(1.1); }
.btn-ghost { background: transparent; color: var(--text-secondary); }
.btn-ghost:hover { background: var(--surface-elevated); color: var(--text-primary); }

/* ---------- Badge (ui.md §5.2) ---------- */
.badge {
  display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 999px;
  font-size: 12px; font-weight: 500;
}
.badge-default { background: var(--surface-elevated); color: var(--text-secondary); }
.badge-brand { background: rgba(0,81,195,0.1); color: var(--brand); }
.badge-success { background: rgba(52,199,89,0.12); color: var(--success); }
.badge-warning { background: rgba(255,149,0,0.12); color: var(--warning); }
.badge-danger { background: rgba(255,59,48,0.12); color: var(--danger); }
.badge-info { background: rgba(0,122,255,0.12); color: var(--info); }

/* ---------- Inputs (ui.md §5.3) ---------- */
.field { display: block; margin-bottom: 12px; }
.field > label { display: block; font-size: 12px; font-weight: 500; color: var(--text-secondary); margin-bottom: 4px; }
.field .hint { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.field .error { font-size: 12px; color: var(--danger); margin-top: 4px; }

.input, select.input, textarea.input {
  height: 36px; width: 100%; padding: 0 12px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface);
  font-size: 14px; color: var(--text-primary);
  font-family: inherit;
}
.input:focus, select.input:focus, textarea.input:focus {
  outline: none; border-color: var(--brand); box-shadow: 0 0 0 2px rgba(0,81,195,0.15);
}
textarea.input { height: auto; min-height: 96px; padding: 8px 12px; resize: vertical; }
.input[readonly] { background: var(--surface-elevated); color: var(--text-secondary); }
.input-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.input-row .span-2 { grid-column: span 2; }
/* 4-column variant — the Add Issue form. .span-2 now means half of the row. */
.input-row.cols-4 { grid-template-columns: repeat(4, 1fr); }
.input-row .span-4 { grid-column: span 4; }
@media (max-width: 820px) {
  .input-row.cols-4 { grid-template-columns: 1fr 1fr; }
  .input-row.cols-4 .span-4 { grid-column: span 2; }
}
@media (max-width: 520px) {
  .input-row.cols-4 { grid-template-columns: 1fr; }
  .input-row.cols-4 .span-2,
  .input-row.cols-4 .span-4 { grid-column: span 1; }
}
/* Optional marker next to a field label. */
.field > label .opt { margin-left: 4px; font-weight: 400; color: var(--text-muted); }
textarea.input.tall { min-height: 120px; }
textarea.input.short { min-height: 76px; }

/* ---------- Tag box (Module / Page, single-value combobox) ---------- */
.tagbox { position: relative; }
.tagbox-control {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  min-height: 36px; padding: 3px 6px 3px 8px;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface);
  cursor: text;
}
.tagbox-control:focus-within { border-color: var(--brand); box-shadow: 0 0 0 2px rgba(0, 81, 195, 0.15); }
.tagbox-chips { display: contents; }
.tagbox-chip {
  display: inline-flex; align-items: center; gap: 6px; flex: none; max-width: 100%;
  padding: 3px 6px 3px 8px; border-radius: 6px;
  background: rgba(0, 81, 195, 0.15); color: var(--brand);
  font-size: 13px; line-height: 1.4;
}
.tagbox-chip > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tagbox-remove {
  flex: none; width: 14px; height: 14px; padding: 0; border: 0; border-radius: 50%;
  background: none; color: inherit; font-size: 11px; line-height: 1; cursor: pointer;
}
.tagbox-remove:hover { background: rgba(255, 255, 255, 0.14); }
.tagbox-input {
  flex: 1; min-width: 110px; height: 28px; padding: 0 4px;
  border: 0; background: none; font: inherit; font-size: 14px; color: var(--text-primary);
}
.tagbox-input:focus { outline: none; }
.tagbox-input::placeholder { color: var(--text-muted); }
.tagbox-menu {
  position: absolute; z-index: 6; left: 0; right: 0; top: calc(100% + 4px);
  max-height: 208px; overflow: auto; margin: 0; padding: 4px; list-style: none;
  border: 1px solid var(--border-strong); border-radius: 8px;
  background: var(--surface-solid); box-shadow: 0 12px 28px rgba(0, 0, 0, 0.35);
}
.tagbox-menu[hidden] { display: none; }
.tagbox-menu li {
  padding: 6px 8px; border-radius: 6px; font-size: 13px; cursor: pointer;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.tagbox-menu li.is-active { background: rgba(0, 81, 195, 0.18); color: var(--brand); }
.tagbox-menu li.is-create { color: var(--text-muted); font-style: italic; }
.tagbox-menu li.tagbox-empty { color: var(--text-muted); font-size: 12px; cursor: default; }
.tagbox-warning { margin: 4px 0 0; font-size: 12px; color: var(--danger); }
.tagbox-warning[hidden] { display: none; }

/* Read-only rendering of a multi-value Module / Page (§29). */
.module-chips { display: flex; flex-wrap: wrap; gap: 4px; }
.module-none { color: var(--text-muted); }

/* ---------- Image dropzone (Add Issue) ---------- */
/* The real input stays in the form for model binding but is never the click target. */
.dropzone-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
.dropzone {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  padding: 18px 14px; text-align: center; cursor: pointer;
  border: 1px dashed var(--border-strong); border-radius: 8px;
  background: var(--surface-elevated);
  transition: border-color 140ms ease, background 140ms ease;
}
.dropzone:hover, .dropzone:focus-visible { border-color: var(--brand); }
.dropzone:focus-visible { outline: none; }
.dropzone.is-dragging { border-color: var(--brand); border-style: solid; background: rgba(0, 81, 195, 0.1); }
.dropzone-icon { width: 26px; height: 26px; margin-bottom: 6px; color: var(--text-muted); }
.dropzone.is-dragging .dropzone-icon { color: var(--brand); }
.dropzone-title { margin: 0; font-size: 13px; font-weight: 500; color: var(--text-primary); }
.dropzone-hint { margin: 2px 0 0; font-size: 12px; color: var(--text-muted); }
.dropzone-browse { color: var(--brand); text-decoration: underline; }
.dropzone-files {
  display: flex; flex-direction: column; gap: 6px;
  width: 100%; margin: 10px 0 0; padding: 0; list-style: none;
}
.dropzone-files:empty { display: none; }
.dropzone-file {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border: 1px solid var(--border); border-radius: 6px;
  background: var(--surface); text-align: left;
}
.dropzone-file img {
  flex: none; width: 34px; height: 34px; object-fit: cover;
  border-radius: 4px; background: var(--surface-elevated);
}
.dropzone-file .meta { flex: 1; min-width: 0; }
.dropzone-file .name {
  display: block; font-size: 12px; color: var(--text-primary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.dropzone-file .size { display: block; font-size: 11px; color: var(--text-muted); }
.dropzone-file .remove {
  flex: none; padding: 2px 6px; border: 0; border-radius: 4px;
  background: none; color: var(--text-muted); font-size: 13px; line-height: 1; cursor: pointer;
}
.dropzone-file .remove:hover { color: var(--danger); background: rgba(255, 255, 255, 0.06); }

/* ---------- Switch ---------- */
.switch { position: relative; display: inline-block; width: 40px; height: 24px; }
.switch input { opacity: 0; width: 0; height: 0; }
.switch .track {
  position: absolute; inset: 0; border-radius: 999px; cursor: pointer;
  background: var(--surface-elevated); border: 1px solid var(--border); transition: 200ms;
}
.switch input:checked + .track { background: linear-gradient(to right, #3b82f6, #8b5cf6); border-color: transparent; }
.switch .thumb {
  position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%;
  background: #fff; transition: transform 200ms;
}
.switch input:checked + .track .thumb { transform: translateX(16px); }

/* ---------- Table (ui.md §6.1) ---------- */
.data-table-wrap { overflow: auto; }
table.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
table.data-table thead th {
  position: sticky; top: 0; z-index: 1;
  text-align: left; font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--text-muted); padding: 10px 12px; border-bottom: 2px solid var(--border);
  background: var(--surface-hover); backdrop-filter: blur(12px);
}
table.data-table tbody td { padding: 10px 12px; border-bottom: 1px solid var(--border); }
table.data-table tbody tr { cursor: pointer; transition: background 120ms; }
table.data-table tbody tr:hover { background: var(--surface-hover); }
table.data-table td.num { text-align: right; font-variant-numeric: tabular-nums; }
table.data-table td.code { font-family: var(--font-mono); color: var(--text-secondary); }

/* ---------- List scroll container (ui.md §6.1) ----------
   The card is a fixed-height shell and the table scrolls INSIDE it, which is what "scroll down to load
   more" needs: the filter bar and the column headers stay put and the page itself never grows. Without
   this the scroll would belong to .content-area and the reader would lose the filters while paging. */
.list-card { padding: 0; overflow: hidden; }
.list-scroll {
  /* 48px top nav + 40px title bar + the 24px padding above and below ≈ 140px, so the card ends at the
     bottom of the viewport instead of the page running past it. dvh so a mobile URL bar cannot swallow
     the last row; the vh line is the fallback for browsers without dvh. */
  height: calc(100vh - 140px);
  height: calc(100dvh - 140px);
  overflow: auto;
  overscroll-behavior: contain;
}
/* Rows that are about to be replaced are dimmed, so a filter change reads as "working", not "slow". */
table.data-table.is-loading { opacity: 0.5; }

/* 1px of nothing — the marker list.js measures the remaining scroll distance to, so the next page is
   requested before the reader actually reaches the bottom (see AHEAD in list.js). */
.list-sentinel { height: 1px; }

/* "Loading…" / "Showing 20 of 137" / "All 137 issues", in the flow right under the last row. Nothing
   is hidden here: with no text, no spinner and no Retry button all that is left is the padding. */
.list-more {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  padding: 10px 12px 16px; font-size: 12px; color: var(--text-muted);
}
/* The UA rule for [hidden] is only `display: none` at user-agent level, so `.btn`'s inline-flex wins and
   a hidden Retry button stays on screen — and in the accessibility tree. Same reason as `.badge-menu`,
   `.tagbox-menu` and `.form-error` above. */
.list-more > [hidden] { display: none; }
.list-spinner {
  width: 16px; height: 16px; flex-shrink: 0; border-radius: 50%;
  border: 2px solid var(--border-strong); border-top-color: var(--brand);
  animation: list-spin 700ms linear infinite;
}
@keyframes list-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .list-spinner { animation-duration: 2.4s; } }

/* The no-JS pager. list.js hides it the moment the scroller takes over, and flex would otherwise keep
   it displayed. */
.list-pager { display: flex; gap: 8px; margin-top: 12px; }
.list-pager[hidden] { display: none; }

/* Module / Page values are short tags ("PR, PO") but the column is 200 characters wide at the top end,
   and a table cell ignores max-width — so the cap has to be on a block inside it, the same reason
   .cell-detail is a div. */
.cell-module { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }

/* Search hits. A <mark> because that is exactly what the element means, with a yellow of our own rather
   than the browser's default — the UA yellow is a light-mode colour that would sit oddly on the dark
   theme, and one stated pair keeps the highlight identical in both themes. Dark text on 300-yellow is
   ~12:1, so the marked word stays the most readable thing in the row. */
mark.hit { background: #FDE047; color: #1F2937; border-radius: 2px; padding: 0 1px; }

/* Issue Detail preview — one line, full text on hover via title="".
   Wrapped in a div because a table cell ignores max-width. */
.cell-detail {
  max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-secondary);
}

/* Two-line cell: a value with something subordinate under it — the Issue Date under the Issue No, the
   Expected Result under the Issue Detail, the moment.js "time ago" under the Status Date. One class for
   the subordinate line so all three read the same, and the cell answers two questions without a second
   column. */
.cell-stack { display: inline-flex; flex-direction: column; line-height: 1.4; white-space: nowrap; }
.cell-stack > .sub { font-size: 11px; color: var(--text-muted); }
/* Nothing to say until moment.js has filled it, so it must not reserve a line of its own. The other
   sub-lines are never empty — a blank Expected Result is rendered as a non-breaking space — because
   a missing second line would make two adjacent rows different heights. */
.cell-stack > .ago:empty { display: none; }

/* Icon + label cells (Type, Severity). */
.cell-ico { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; }
/* The Type value is boxed (§2). It sits next to the Status badge, which is already a box, so unboxed it read
   as loose text beside a glyph while its neighbour looked like a value. Border only, no fill: the colour of
   a Type belongs to its icon, and a tint here would compete with the Status badge's tint one column away.

   The padding is horizontal on purpose. Vertically the box is the text's own line plus its two borders
   (20.2px against an 18.2px line), which keeps the two-line Type/Severity cell at 37px and the rows at
   59px — adding vertical padding here would grow EVERY row in the list. */
.cell-ico.chip-type {
  border: 1px solid var(--border-strong); border-radius: 6px; padding: 0 6px;
}
.ico { width: 14px; height: 14px; flex-shrink: 0; }

/* Type: the icon carries the colour, the label stays neutral. */
.ico-type { color: var(--text-muted); }
.ico-type.ico-bug { color: var(--danger); }
.ico-type.ico-change { color: var(--info); }

/* Date/time values: muted glyph, normal label — same treatment as Type. */
.ico-date { color: var(--text-muted); }

/* Link Found Issue: muted chain glyph in front of the URL. */
.ico-link { color: var(--text-muted); }

/* The URL may be long, so it has to wrap instead of inheriting the cell's nowrap. */
.cell-ico a.cell-ico-link { white-space: normal; overflow-wrap: anywhere; }
.cell-ico a.cell-ico-link:hover { text-decoration: underline; text-underline-offset: 2px; }

/* Severity: the colour applies to the whole cell — icon (currentColor) and label alike.
   Low ↓ yellow · Medium − blue · High ↑ red. */
.sev-low { color: var(--warning); }
.sev-medium { color: var(--info); }
.sev-high { color: var(--danger); }

/* ---------- Empty / error states (ui.md §5.12) ---------- */
.state-block { padding: 48px 16px; text-align: center; }
.state-block .title { font-size: 15px; font-weight: 500; }
.state-block .description { font-size: 13px; color: var(--text-secondary); margin-top: 4px; }
.state-block.error .title { color: var(--danger); }

/* ---------- Modal-ish panel used for confirms ---------- */
.overlay {
  position: fixed; inset: 0; background: rgba(0,0,0,0.4); backdrop-filter: blur(2px); z-index: 50;
  /* Fades rather than pops: the drawer beside it is sliding, and a hard cut reads as a glitch. */
  animation: overlay-in 160ms ease-out;
}
.overlay.is-closing { animation: overlay-out 240ms ease-in forwards; }
@keyframes overlay-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes overlay-out { from { opacity: 1; } to { opacity: 0; } }
.dialog {
  position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: min(480px, calc(100% - 32px)); z-index: 51; padding: 0;
}
.dialog-header {
  padding: 16px 20px; border-bottom: 1px solid var(--border); font-size: 16px; font-weight: 500;
  /* Flex so a dialog can pin secondary content (the drawer's Type / Severity badges) to the right. */
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.dialog-header-meta { display: inline-flex; align-items: center; gap: 12px; font-size: 14px; font-weight: 400; }

/* Carrier for the drawer's Type / Severity badges — list.js moves it into .dialog-header-meta. */
.detail-header-badges { display: inline-flex; align-items: center; gap: 12px; }

/* ---------- Badge picker (Type / Severity / Status mini list) ----------
   The badge is the trigger; the menu hangs off it. Nothing here clips it: .dialog and .glass have no
   overflow, and only the drawer's body scrolls, so a menu opened from the header can overlay the body. */
.badge-pick { position: relative; display: inline-flex; }
.badge-pick-trigger {
  display: inline-flex; align-items: center; padding: 0; border: 0; background: none;
  color: inherit; cursor: pointer; border-radius: 999px;
}
.badge-pick-trigger:hover { filter: brightness(1.18); }
.badge-pick-trigger:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

.badge-menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 60;
  min-width: 168px; padding: 6px;
  display: flex; flex-direction: column; gap: 2px;
  /* --border-strong, not --border: the default 8%-white outline is too faint to separate the panel from
     the drawer behind it, and the black drop shadow below does almost nothing on a dark background.
     The same reasoning as .comment-bubble. */
  background: var(--surface-elevated); border: 1px solid var(--border-strong);
  border-radius: 10px; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.20), 0 14px 32px rgba(0, 0, 0, 0.38);
}
/* display:flex above would otherwise win over the UA rule for [hidden]. */
.badge-menu[hidden] { display: none; }
.badge-menu-item {
  display: flex; align-items: center; padding: 6px 8px; border: 0; border-radius: 7px;
  background: none; cursor: pointer; text-align: left;
  /* Stated rather than inherited: this is the one control the reset above cannot be trusted to cover,
     because a black-on-navy menu is invisible rather than merely off-theme. */
  color: var(--text-primary);
}
.badge-menu-item:hover { background: var(--surface-hover); }
.badge-menu-item:focus-visible { outline: 2px solid var(--brand); outline-offset: -2px; }
/* The current value, marked with fill and weight only — no coloured edge. Two reasons: an inset shadow
   does not follow border-radius, so a brand bar leaves square ends poking out of the rounded corners,
   and colour is already doing a lot of work in this menu (each entry previews its own badge).
   --border-strong rather than --surface-hover, which is the hover fill: on the near-black dark panel the
   softer fill is barely distinguishable from the panel itself. Declared after :hover on purpose, so the
   current entry keeps its stronger fill while the pointer is over it. */
.badge-menu-item.is-current {
  background: var(--border-strong); font-weight: 500;
}
.dialog-body { padding: 16px 20px; }
.dialog-footer { padding: 16px 20px; border-top: 1px solid var(--border); display: flex; justify-content: flex-end; gap: 8px; }

/* Larger, scrollable variant used by the Add Issue popup. Widened so the 4-column
   field grid (below) still leaves room for date/time pickers. */
.dialog-lg {
  width: min(880px, calc(100% - 32px)); max-height: calc(100vh - 48px);
  display: flex; flex-direction: column;
}
.dialog-lg[hidden] { display: none; }
.dialog-lg .dialog-body { overflow: auto; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 12px; }
body.dialog-open { overflow: hidden; }

/* Detail popup — a full-height drawer pinned to the right, sliding in from the edge and back out to it.
   Overrides .dialog's centred placement and .dialog-lg's width/max-height. The animation (not a
   transition) is what makes it replay every time `hidden` is removed. */
.dialog-drawer {
  top: 0; right: 0; bottom: 0; left: auto;
  transform: none;
  width: min(600px, 100%);
  max-height: none;
  border-top: 0; border-right: 0; border-bottom: 0;
  border-top-right-radius: 0; border-bottom-right-radius: 0;
  animation: drawer-in 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* Leaving: the same 240ms, eased the other way so it accelerates out instead of decelerating in.
   `forwards` is load-bearing — list.js hides the panel on `animationend`, and without it the drawer would
   snap back to translateX(0) for a frame before it disappears. */
.dialog-drawer.is-closing { animation: drawer-out 240ms cubic-bezier(0.4, 0, 1, 1) forwards; }
/* Full-height means the body takes the leftover space between header and footer. */
.dialog-drawer .dialog-body { flex: 1; }
/* 500px cannot hold the two-card grid — stack it, or both columns end up unusable. */
.dialog-drawer .detail-grid { grid-template-columns: 1fr; }

@keyframes drawer-in {
  from { transform: translateX(100%); }
  to { transform: translateX(0); }
}

@keyframes drawer-out {
  from { transform: translateX(0); }
  to { transform: translateX(100%); }
}

/* A reduced-motion preference makes the transition briefer, not absent — the same treatment
   `.list-spinner` gets above (it slows rather than stops). A panel that appears and vanishes with no
   transition reads as a glitch, and it is the app's primary navigation: it is what says *where* the
   panel came from and where it went. Note this environment reports `reduce` for the whole browser
   (Windows animation effects off), so switching the animation off here switched it off everywhere. */
@media (prefers-reduced-motion: reduce) {
  .dialog-drawer,
  .dialog-drawer.is-closing { animation-duration: 160ms; }
  .overlay { animation-duration: 120ms; }
  .overlay.is-closing { animation-duration: 160ms; }
}
.detail-grid { display: grid; grid-template-columns: 1.4fr 1fr; gap: 16px; align-items: start; }
@media (max-width: 900px) {
  .detail-grid { grid-template-columns: 1fr; }
}

/* ---------- Filter bar (title bar actions) ---------- */
.filter-bar { display: flex; align-items: center; gap: 6px; }

/* Status / Severity are mini pickers rather than <select>s — the same interaction as the Issue detail
   badges (badgepick.js, .badge-menu). Sized to match the .input they replaced: 26px tall, radius 8px. */
.filter-pick { position: relative; display: inline-flex; }
.filter-pick-trigger {
  display: inline-flex; align-items: center; gap: 6px;
  height: 26px; padding: 0 10px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface); color: var(--text-primary);
  font-size: 14px; cursor: pointer;
}
.filter-pick-trigger:hover { border-color: var(--border-strong); }
.filter-pick-trigger:focus-visible { outline: 2px solid var(--brand); outline-offset: 1px; }
/* Holds the value(s) as flowing text — the trigger is a flex row, so the label must be a single item. */
.filter-pick-label { white-space: nowrap; }
/* .filter-pick-caret is declared with .caret in the top-nav section — one affordance, one definition. */
/* Anchored to the trigger's RIGHT edge, inherited from .badge-menu. Not cosmetic: the filter bar is
   pushed right by .page-title-actions' auto margin, so a trigger's right edge stays put while its left
   edge moves with its own label — anchoring left made the panel jump sideways on every tick as the
   Status label grew and shrank ("All Status" 96px → "New, Reopen" 122px → all five 338px).
   Sized to its own content instead of a fixed 150px: the Module vocabulary is per Project and names like
   "No-draw regression" are ordinary, so the fixed width wrapped every other entry onto two lines while the
   panel still had room to spare — and a wrapped entry reads as two separate values. max-content is what
   buys that; the cap is the other half, because ModulePage is nvarchar(200) and a pathological tag should
   wrap as before rather than stretch a left-growing panel across the window. */
.filter-menu {
  width: max-content;
  max-width: min(320px, calc(100vw - 32px));
}

/* Status colour applied to plain text instead of a chip — the filter trigger lists several selected
   Statuses inline and five tinted pills would stretch the title bar. One tone name per Status, shared
   with the .badge-* variants via IssueDisplay.StatusTone, so the two can never disagree. */
.tone-info { color: var(--info); }
.tone-warning { color: var(--warning); }
.tone-success { color: var(--success); }
.tone-danger { color: var(--danger); }
.tone-default { color: var(--text-secondary); }

/* Multi-select entries (the Status filter): a label wrapping a checkbox plus the value's badge.
   Not .badge-menu-item — that is a button, and these must not submit on click.
   overflow-wrap is the backstop the panel's max-width assumes: a single very long Module name has nothing
   to wrap ON (no spaces), and without this it would simply overflow the panel sideways. */
.filter-menu-option {
  display: flex; align-items: center; gap: 8px;
  padding: 6px 8px; border-radius: 7px; cursor: pointer; color: var(--text-primary);
  overflow-wrap: anywhere;
}
.filter-menu-option:hover { background: var(--surface-hover); }
/* A ticked entry is filled, not merely ticked. The box is a 16px detail in a menu the reader is scanning
   for "which values are on", and on the Status picker every entry already carries a coloured badge that
   pulls the eye — so the fill is what makes the selection readable at a glance. Same fill as Severity's
   current entry, and declared after :hover so a ticked entry keeps it under the pointer.

   :has() is progressive: without it the entry is simply left as it was, ticks and all. */
.filter-menu-option:has(input[type="checkbox"]:checked) { background: var(--border-strong); }

/* Custom checkbox. A native one is drawn by the browser from its own colour scheme, and the app's dark
   theme is a class the browser knows nothing about (it still reports color-scheme: normal) — so in dark
   mode an unstyled checkbox renders as a bright white square on a near-black panel. Drawing it here
   makes it follow the theme like everything else. */
.filter-menu-option input[type="checkbox"] {
  appearance: none; -webkit-appearance: none;
  flex-shrink: 0; width: 16px; height: 16px; margin: 0;
  border: 1px solid var(--border-strong); border-radius: 5px;
  background-color: var(--surface); background-position: center; background-repeat: no-repeat;
  cursor: pointer;
  transition: background-color 120ms ease, border-color 120ms ease;
}
.filter-menu-option input[type="checkbox"]:hover { border-color: var(--text-muted); }
.filter-menu-option input[type="checkbox"]:checked {
  background-color: var(--brand); border-color: var(--brand);
  /* Inline SVG tick: an <input> is a replaced element, so ::before is not dependable across engines. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.6 8.6 6.5 11.5 12.4 4.7'/%3E%3C/svg%3E");
}
.filter-menu-option input[type="checkbox"]:focus-visible { outline: 2px solid var(--brand); outline-offset: 2px; }

/* ---------- Login (ui.md §6.4) ---------- */
.login-shell { min-height: 100vh; display: flex; align-items: center; justify-content: center; background: var(--bg); padding: 16px; }
.login-card { width: 100%; max-width: 380px; padding: 32px; text-align: center; }
.login-card h1 { font-size: 20px; font-weight: 700; margin: 8px 0 2px; }
.login-card p.subtitle { font-size: 12px; color: var(--text-muted); margin: 0 0 20px; }
.login-card form { text-align: left; }

/* ---------- Scrollbars ----------
   The thumb is a translucent grey of its own, not a text colour: it used to be `--text-muted`, which was a
   pale grey until the contrast pass darkened that token — after which every scrollbar became a dark bar
   competing with the content it scrolls. It stays quiet at rest and only firms up while the pointer is
   over the thing being scrolled, so it is still findable. Firefox gets the same intent via
   `scrollbar-color`; Chrome/Edge/Safari via the `::-webkit-` rules. */
.scroll-thin { scrollbar-width: thin; scrollbar-color: var(--scrollbar-thumb) transparent; }
.scroll-thin::-webkit-scrollbar { width: 4px; height: 4px; }
.scroll-thin::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 2px; }
.scroll-thin:hover::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb-hover); }

/* ---------- Attachment thumbnails / annotation canvas ---------- */
.attachment-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; margin-top: 8px; }
.attachment-thumb {
  margin: 0; padding: 6px; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-elevated); cursor: zoom-in; overflow: hidden;
  transition: border-color 140ms ease;
}
.attachment-thumb:hover { border-color: var(--brand); }
.attachment-thumb img {
  display: block; width: 100%; height: 104px; object-fit: cover;
  border-radius: 4px; background: var(--surface);
}
.attachment-thumb figcaption {
  display: flex; align-items: center; justify-content: space-between; gap: 6px;
  margin-top: 6px;
}
.attachment-name {
  font-size: 11px; color: var(--text-secondary);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The blob could not be read (storage unavailable or the file was removed). */
.attachment-broken img { display: none; }
.attachment-broken::before {
  content: '⚠ image unavailable';
  display: flex; align-items: center; justify-content: center;
  height: 104px; font-size: 11px; color: var(--text-muted);
  border: 1px dashed var(--border-strong); border-radius: 4px;
}

/* ---------- Lightbox (full-size attachment) ---------- */
.lightbox {
  position: fixed; inset: 0; z-index: 200;
  display: flex; align-items: center; justify-content: center; flex-direction: column;
  gap: 12px; padding: 32px;
  background: rgba(0, 0, 0, 0.78); backdrop-filter: blur(4px);
}
.lightbox[hidden] { display: none; }
.lightbox-image {
  max-width: 100%; max-height: calc(100vh - 120px);
  border-radius: 8px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
  background: #fff;
}
.lightbox-caption { font-size: 12px; color: #fff; opacity: 0.85; text-align: center; }
.lightbox-close {
  position: absolute; top: 16px; right: 16px;
  width: 34px; height: 34px; border-radius: 50%; border: none; cursor: pointer;
  background: rgba(255, 255, 255, 0.16); color: #fff; font-size: 14px;
}
.lightbox-close:hover { background: rgba(255, 255, 255, 0.28); }

.annotate-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
#annotationCanvas {
  display: block; border: 1px solid var(--border); border-radius: 8px;
  background: var(--surface-elevated); max-width: 100%; height: auto;
  cursor: crosshair; touch-action: none;
}

/* §38 — the history reads as a timeline: a dotted rail with a marker dot on each entry's date.
   Geometry: the rail is a zero-width box whose 2px left border starts at left:3px in the item's
   padding box, so its centre sits 10px in from the item's edge. .meta's dot is pulled back by
   -22px (the item's own padding) to land its centre on that same 10px, and its surface-coloured
   ring masks the rail where they cross. */
.history-item { position: relative; padding: 10px 0 10px 22px; margin-left: 6px; }
.history-item::before {
  content: ""; position: absolute; left: 3px; top: 0; bottom: 0;
  border-left: 2px dotted var(--text-muted); opacity: 0.7;
}
.history-item .meta { position: relative; font-size: 12px; color: var(--text-muted); }
.history-item .meta::before {
  content: ""; position: absolute; left: -22px; top: 50%; transform: translateY(-50%);
  width: 8px; height: 8px; border-radius: 50%;
  background: var(--brand); box-shadow: 0 0 0 3px var(--surface);
}
.history-item .change { font-size: 13px; margin-top: 2px; }

/* §38 — Status values are badges, so an old → new pair must stay on one line and line up. */
.history-item .change .badge { vertical-align: middle; }
.history-item .change .arrow { margin: 0 6px; color: var(--text-muted); }
.value-none { color: var(--text-muted); }

/* ---------- Tabs (detail card: Comments / History) ---------- */
.tablist { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 12px; }
.tab {
  appearance: none; border: 0; background: none; cursor: pointer;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 8px 12px; margin-bottom: -1px;
  font-family: inherit; font-size: 13px; font-weight: 500; color: var(--text-muted);
  border-bottom: 2px solid transparent; border-radius: 4px 4px 0 0;
}
.tab:hover { color: var(--text-primary); }
.tab.is-active { color: var(--text-primary); border-bottom-color: var(--brand); }
.tab:focus-visible { outline: none; box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.35); }
.tabpanel[hidden] { display: none; }
.tab-count {
  display: inline-block; padding: 0 6px; border-radius: 999px;
  font-size: 11px; font-weight: 500;
  background: var(--surface-elevated); color: var(--text-secondary);
}

/* Card heading with its value pushed to the right edge (Status + its badge). */
.heading-split { display: flex; align-items: center; justify-content: space-between; gap: 8px; }

/* ---------- Issue comments (chat layout) ---------- */
/* Neither list caps its own height any more — the drawer body (or the Admin page) is the only
   scroller, so a nested scrollbar here just fights it. */
.comment-list { margin-bottom: 12px; }

/* One chat row: avatar, then the bubble holding the meta line and the text. */
.comment-item { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 14px; }
.comment-avatar { width: 32px; height: 32px; font-size: 12px; }

.comment-bubble {
  position: relative; flex: 1; min-width: 0;
  padding: 8px 12px; border-radius: 8px;
  /* --border-strong, not --border: the default 8%-white outline is too faint for the tail to read
     as an arrow against the card at this size. */
  border: 1px solid var(--border-strong); background: var(--surface-elevated);
}
/* Name / IP / time, inside the bubble above the text. */
.comment-meta { display: flex; align-items: baseline; flex-wrap: wrap; gap: 8px; margin-bottom: 5px; }
.comment-author { font-size: 12px; font-weight: 600; color: var(--text-primary); }
/* Muted and monospaced so a masked address reads as data rather than as part of the name. */
.comment-ip { font-family: var(--font-mono); font-size: 11px; color: var(--text-muted); }
.comment-date { font-size: 11px; color: var(--text-muted); }

/* pre-wrap belongs on the body ONLY — on the bubble it would render the markup's own indentation
   around the meta line as visible blank space. */
.comment-body { font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere; }

/* Speech tail: a rotated square whose left half sticks out of the bubble. Only its two outer edges
   are bordered, so the two edges that fall inside the bubble are invisible, and its fill matches the
   bubble so no seam shows. Deliberately NOT z-index:-1 — that would drop it behind the card's own
   opaque background and it would never paint. top:11px centres the 10px tail on the 32px avatar. */
.comment-bubble::before {
  content: ""; position: absolute; left: -6px; top: 11px;
  width: 10px; height: 10px;
  background: var(--surface-elevated);
  border-left: 1px solid var(--border-strong);
  border-bottom: 1px solid var(--border-strong);
  transform: rotate(45deg);
}

.comment-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; }

/* The same submit row for the popup's other inline forms (the Expected Result editor), where the
   error slot sits to the left of the button instead of under the field. */
.form-actions { display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 8px; }
.form-actions .form-error { margin: 0; flex: 1; }

/* Inline rejection for a form posted from the detail popup (comment composer, resolution check).
   The fetch handler replaces the dialog body with the re-rendered card on success and writes the
   server's message here on a 400. */
.form-error { margin: 8px 0 0; font-size: 12px; color: var(--danger); }
.form-error[hidden] { display: none; }
.comment-actions .form-error { margin: 0; flex: 1; }

/* Page-level notice — a message carried by TempData when a form posted without JS and got
   redirected back to the list. */
.page-notice {
  margin: 0 0 8px; padding: 10px 12px; border-radius: 8px;
  border: 1px solid rgba(255, 59, 48, 0.35); background: rgba(255, 59, 48, 0.08);
  font-size: 13px; color: var(--danger);
}
.page-notice:last-child { margin-bottom: 0; }

/* ---------- Opener resolution check (§33.1) ---------- */
/* Passed / Failed verdict recorded by the person who raised the Issue. */
.verify-actions { margin: 10px 0 0; }
.verify-divider {
  display: flex; align-items: center; gap: 8px; margin: 12px 0 10px;
  font-size: 12px; color: var(--text-muted);
}
.verify-divider::before, .verify-divider::after {
  content: ""; flex: 1; height: 1px; background: var(--border);
}
.verify-result {
  display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
  padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--border); background: var(--surface-elevated);
}
.verify-result.is-passed { border-color: rgba(52, 199, 89, 0.35); }
.verify-result.is-failed { border-color: rgba(255, 59, 48, 0.35); }
.verify-meta { font-size: 12px; color: var(--text-muted); }
.verify-remark {
  margin-top: 6px; padding: 8px 10px; border-radius: 8px;
  background: var(--surface-elevated); font-size: 13px; white-space: pre-wrap;
}
