/* =============================================================
   Autopilot — shared UI primitives
   Requires tokens.css.

   Every screen is on this system now, so the old stylesheet is gone
   and nothing here has to avoid colliding with it.

   A component belongs in this file as soon as a second screen needs
   it. Everything below the primitives was written two, three or five
   times under a per-screen prefix first, and had already drifted:
   the same list showed a name in the UI face on one screen and in
   mono on another, and the same "input plus button" row was laid out
   five times, once with a hand-measured 26px offset inline.
   ============================================================= */

/* -------------------------------------------------------------
   UI COPY STANDARD

   Applies to every string rendered by this system.

   1. A string that is one sentence long carries no terminal period.
      Labels, hints, placeholders, single-line errors, empty states
      and button text are fragments, not prose, and a full stop on a
      three-word label reads as generated filler. A string of two or
      more sentences is punctuated normally, final period included,
      because dropping only the last one looks like a typo.

      The split matters more than which side a given string lands on:
      before this rule was applied, 29 strings ended in a period and
      15 equivalents did not, which is the kind of difference a reader
      registers as sloppiness without being able to name it.

   2. One idea per string. If a message needs a second sentence to
      explain itself, the first sentence was too vague, so rewrite it
      instead of appending. When a remedy has to be attached, join it
      with a colon: "Couldn't reach Jira: check the URL and your
      network".

   3. No em dashes. They currently read as a machine-writing tell to
      a lot of people, and that impression costs more than the small
      amount of rhythm they buy. Use a colon for a remedy, a comma for
      an aside, or a shorter sentence.

   4. Say what to do, not what went wrong. "Add the full address,
      including the https:// prefix" beats "Invalid URL". The raw
      cause belongs in .alert-details, not in the message.

   5. Contractions are fine ("Couldn't", "isn't"). Formal negation
      ("Could not", "is not") reads as machine output.

   6. Never end a string on punctuation that could be mistaken for
      content. A period after "https://" looks like part of the URL.

   7. Sentence case everywhere. No Title Case, no ALL CAPS except the
      2xs eyebrow label style.
   ------------------------------------------------------------- */

/* ---------- Base ---------- */

*,
*::before,
*::after {
  box-sizing: border-box;
}

.ui body,
body.ui {
  margin: 0;
  background: var(--bg-sunken);
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-md);
  line-height: var(--leading-normal);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Numbers in an ops tool are compared column-to-column constantly.
   Proportional digits make that measurably slower. */
.ui table,
.ui code,
.ui kbd,
.ui .tabular {
  font-variant-numeric: tabular-nums;
}

/* ---------- Focus ---------- */

/* The old stylesheet defines no focus styles at all. One visible ring,
   applied once, to everything focusable. :focus-visible so it appears for
   keyboard users without adding a ring to every mouse click. */
.ui :focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: var(--focus-ring-offset);
  border-radius: var(--radius-sm);
}

.ui :focus:not(:focus-visible) {
  outline: none;
}

/* ---------- Screen-reader utility ---------- */

/* Kept because existing app.js toggles this class by name in several
   screens; the new stylesheets must honour the same contract. */
.hidden {
  display: none !important;
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- Button ---------- */

.button {
  --_bg: transparent;
  --_fg: var(--text-primary);
  --_border: var(--border-default);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--_border);
  border-radius: var(--radius-md);
  background: var(--_bg);
  color: var(--_fg);
  font-family: inherit;
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  position: relative;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.button:disabled,
.button[aria-disabled="true"] {
  --_bg: var(--bg-subtle);
  --_fg: var(--text-disabled);
  --_border: var(--border-default);

  cursor: not-allowed;
  opacity: 1;
  box-shadow: none;
}

.button-primary {
  --_bg: var(--accent);
  --_fg: var(--accent-text);
  --_border: var(--accent);
}

.button-primary:hover:not(:disabled):not([aria-disabled="true"]) {
  --_bg: var(--accent-hover);
  --_border: var(--accent-hover);
}

.button-secondary {
  --_bg: var(--bg-surface);
  --_fg: var(--text-primary);
  --_border: var(--border-interactive);
}

.button-secondary:hover:not(:disabled):not([aria-disabled="true"]) {
  --_bg: var(--bg-hover);
  --_border: var(--border-strong);
}

/* Destructive actions must never share a silhouette with routine ones.
   In the current UI, Delete and Save are both `.btn ghost`. */
.button-danger {
  --_bg: transparent;
  --_fg: var(--danger);
  --_border: color-mix(in srgb, var(--danger) 40%, transparent);
}

.button-danger:hover:not(:disabled):not([aria-disabled="true"]) {
  --_bg: var(--danger-subtle);
  --_border: var(--danger);
}

.button-ghost {
  --_bg: transparent;
  --_border: transparent;
  --_fg: var(--text-secondary);
}

.button-ghost:hover:not(:disabled):not([aria-disabled="true"]) {
  --_bg: var(--bg-hover);
  --_fg: var(--text-primary);
}

.button-sm {
  min-height: 28px;
  padding: 0 var(--space-2);
  font-size: var(--text-sm);
}

.button-lg {
  min-height: 40px;
  padding: 0 var(--space-4);
}

.button-block {
  width: 100%;
}

.button-icon {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
}

/* Loading: the label stays in place (so the button keeps its width and the
   layout does not jump) and is made invisible, with a spinner centred over it. */
.button[data-loading="true"] {
  cursor: progress;
  pointer-events: none;
}

.button[data-loading="true"] .button-label {
  visibility: hidden;
}

.button[data-loading="true"]::after {
  content: "";
  position: absolute;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid currentColor;
  border-top-color: transparent;
  animation: ui-spin 600ms linear infinite;
}

@keyframes ui-spin {
  to { transform: rotate(360deg); }
}

@media (prefers-reduced-motion: reduce) {
  .button[data-loading="true"]::after {
    animation-duration: 1.6s;
  }
}

/* ---------- Field ---------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field-label {
  display: block;
  color: var(--text-primary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--leading-tight);
}

.field-label .field-optional {
  margin-left: var(--space-1);
  color: var(--text-tertiary);
  font-weight: var(--weight-normal);
}

.input {
  width: 100%;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border-interactive);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--text-md);
  line-height: var(--leading-snug);
  transition:
    border-color var(--duration-fast) var(--ease-out),
    background-color var(--duration-fast) var(--ease-out);
}

.input::placeholder {
  color: var(--text-tertiary);
}

.input:hover:not(:disabled):not([aria-invalid="true"]) {
  border-color: var(--border-strong);
}

.input:disabled {
  background: var(--bg-subtle);
  color: var(--text-disabled);
  cursor: not-allowed;
}

.input[aria-invalid="true"] {
  border-color: var(--danger);
}

.input[aria-invalid="true"]:focus-visible {
  outline-color: var(--danger);
}

/* `.input` sets `padding: 0 var(--space-3)`, which is right for a 36px
   single-line control where line-height does the vertical centring, and wrong
   for a textarea: it left the first line glued to the top border. A textarea
   needs real vertical padding and a line-height that makes several lines of
   JQL or JSON readable. */
textarea.input {
  min-height: 76px;
  padding: var(--space-2) var(--space-3);
  line-height: var(--leading-normal);
  /* Horizontal dragging would push the field out of its grid column, and no
     content here reads better wider than the form. */
  resize: vertical;
}

/* A native <select> draws its own chevron, which differs per OS and never
   matches the one the combobox draws. Two arrow systems on one form is what
   made them look misaligned, so the native one is replaced by the same
   geometry: 12px glyph, 12px from the right edge, identical path. */
select.input {
  appearance: none;
  padding-right: calc(var(--space-3) * 2 + 12px);
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  background-size: 12px 12px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' stroke='%23616873' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  cursor: pointer;
}

/* Same glyph, lighter stroke. A data URI cannot read currentColor, so the
   dark variant is spelled out rather than inherited. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) select.input {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' stroke='%237f8794' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  }
}

:root[data-theme="dark"] select.input {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.5 6 8l3.5-3.5' stroke='%237f8794' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

/* Credentials, JQL, cron and JSON are read character by character. */
.input-mono {
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  letter-spacing: 0;
}

.field-hint {
  margin: 0;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
}

.field-error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-1);
  margin: 0;
  color: var(--danger);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
}

.field-error:empty {
  display: none;
}

/* Checkbox rows shared by rule forms and settings pages. */
.toggle-field {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
}

.toggle-field input[type="checkbox"] {
  width: 16px;
  height: 16px;
  margin: 2px 0 0;
  accent-color: var(--accent);
  flex: 0 0 auto;
}

.toggle-field label {
  color: var(--text-primary);
  font-size: var(--text-md);
  cursor: pointer;
}

.toggle-field-hint {
  display: block;
  margin-top: 2px;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
}

/* A control with a button beside it: add a chat, copy a token, run a search.
   The button sits inside .field, under the label and above the hint, so it
   lines up with the control it belongs to without a hand-measured offset for
   the label's height. Five screens each had their own copy of this grid, one
   of them with `margin-top: 26px` written inline. */
.field-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2);
  align-items: start;
}

/* Below this the two controls cannot both stay usable side by side, so the
   button wraps under the input at its natural width rather than stretching. */
@media (max-width: 560px) {
  .field-row {
    grid-template-columns: minmax(0, 1fr);
  }

  .field-row > .button {
    justify-self: start;
  }
}

/* Search input with a leading glyph. The icon is decorative and must never
   take the click, or the field cannot be focused by aiming at its left edge. */
.search-field {
  position: relative;
  display: flex;
  align-items: center;
}

.search-field .input {
  padding-left: calc(var(--space-3) + 20px);
}

.search-field-icon {
  position: absolute;
  left: var(--space-3);
  width: 14px;
  height: 14px;
  color: var(--text-tertiary);
  pointer-events: none;
}

/* Input with a trailing control (reveal, copy, clear). */
.input-group {
  position: relative;
  display: flex;
  align-items: center;
}

.input-group .input {
  padding-right: 38px;
}

.input-affix {
  position: absolute;
  right: var(--space-1);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
}

.input-affix:hover {
  background: var(--bg-hover);
  color: var(--text-primary);
}

/* ---------- Alert ---------- */

.alert {
  --_tone: var(--border-default);
  --_tone-bg: var(--bg-subtle);

  display: flex;
  gap: var(--space-2);
  margin: 0;
  padding: var(--space-3);
  border: 1px solid color-mix(in srgb, var(--_tone) 35%, transparent);
  border-radius: var(--radius-md);
  background: var(--_tone-bg);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  text-align: left;
}

.alert:empty {
  display: none;
}

.alert-danger  { --_tone: var(--danger);  --_tone-bg: var(--danger-subtle); }
.alert-success { --_tone: var(--success); --_tone-bg: var(--success-subtle); }
.alert-warning { --_tone: var(--warning); --_tone-bg: var(--warning-subtle); }
.alert-info    { --_tone: var(--accent);  --_tone-bg: var(--accent-subtle); }

.alert-icon {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  color: var(--_tone);
}

.alert-body {
  min-width: 0;
  flex: 1 1 auto;
}

/* Both are <span> so the alert can live inside a <p>, so they need to be
   promoted to blocks to stack. */
.alert-title {
  display: block;
  color: var(--text-primary);
  font-weight: var(--weight-medium);
}

.alert-details {
  margin-top: var(--space-2);
}

.alert-details > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  width: fit-content;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  cursor: pointer;
  list-style: none;
}

.alert-details > summary::-webkit-details-marker {
  display: none;
}

/* Disclosure caret drawn in CSS so it rotates with the open state and
   inherits colour, instead of shipping another icon. */
.alert-details > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform var(--duration-fast) var(--ease-out);
}

.alert-details[open] > summary::before {
  transform: rotate(90deg);
}

.alert-details > summary:hover {
  color: var(--text-primary);
}

/* Raw server text: kept verbatim so no diagnostic detail is lost, but
   demoted so the human-readable line above it reads first. */
.alert-detail {
  display: block;
  margin-top: var(--space-1);
  color: var(--text-secondary);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  overflow-wrap: anywhere;
}

/* ---------- Segmented control ---------- */

/* For small sets of mutually exclusive options where all options are worth
   showing at once (theme, density, date range). Above ~4 options this stops
   scanning well and a select is the right control instead. */
.segmented {
  display: inline-flex;
  gap: 1px;
  padding: 2px;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-subtle);
}

.segmented-option {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 26px;
  height: 22px;
  padding: 0 var(--space-1);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-tertiary);
  font-family: inherit;
  font-size: var(--text-xs);
  line-height: 1;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}

.segmented-option:hover {
  color: var(--text-primary);
}

/* The selected segment is raised onto the surface colour rather than tinted
   with the accent: this is a view preference, not a primary action, and an
   accent-filled segment would read as one. */
.segmented-option[aria-checked="true"] {
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-sm);
}

.segmented-option svg {
  width: 14px;
  height: 14px;
}

/* Text-labelled variant, used where the control switches a mode rather than
   a view preference. Sized like an input so it reads as part of the form. */
.segmented-wide {
  width: fit-content;
  max-width: 100%;
  padding: 3px;
}

.segmented-wide .segmented-option {
  min-width: 0;
  height: 28px;
  padding: 0 var(--space-3);
  font-size: var(--text-md);
  white-space: nowrap;
}

@media (max-width: 480px) {
  .segmented-wide {
    width: 100%;
  }

  .segmented-wide .segmented-option {
    flex: 1 1 0;
  }
}

/* ---------- Panel ---------- */

/* .panel is a section of a page; .card, further down, is one saved object
   inside a list. They are the same surface and differ only in what they
   contain, so the surface itself is declared once. */
.panel,
.card {
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background: var(--bg-surface);
}

.panel-header {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
}

/* Wraps the title when the panel also carries a subtitle, so the two stay one
   block and .panel-actions still lands on the far right. */
.panel-heading {
  min-width: 0;
}

.panel-title {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

/* One line saying what the panel is for. Three screens spelled this out
   differently, two of them by borrowing .field-hint, which belongs to a form
   control and not to a section heading. */
.panel-subtitle {
  margin: 2px 0 0;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
}

.panel-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-left: auto;
}

.panel-body {
  padding: var(--space-4);
}

/* Vertical rhythm between the top-level blocks of a page. One value, set in
   one place: the screens used to disagree at 12, 16 and 20px, and some of
   them said so with an inline margin on the section itself. */
.section-stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* `display: flex` outranks the user-agent rule behind the hidden attribute,
   so a stack that a screen toggles has to say so itself. */
.section-stack[hidden] {
  display: none;
}

.form-actions {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* ---------- Form layout ---------- */

/* A long form is split into titled sections rather than run as one column of
   sixteen fields, so an optional name never carries the same weight as the
   two selects that decide what everything under them means. Auto transitions,
   report rules, tool settings and the webhook editor each had their own copy
   of these four rules under a different prefix. */
.form-sections {
  display: flex;
  flex-direction: column;
}

.form-section {
  padding: var(--space-5) 0;
  border-bottom: 1px solid var(--border-subtle);
}

.form-section:first-child {
  padding-top: 0;
}

.form-section:last-of-type {
  border-bottom: 0;
}

.form-section-head {
  margin-bottom: var(--space-4);
}

.form-section-title {
  margin: 0;
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

/* A section whose title needs no explanatory line skips .form-section-head,
   so the title itself has to open the gap to the first field. */
.form-section > .form-section-title {
  margin-bottom: var(--space-4);
}

/* Capped at 62ch: explanatory text set across the full width of a wide form
   loses the reader between the end of one line and the start of the next. */
.form-section-hint {
  margin: var(--space-1) 0 0;
  max-width: 62ch;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.form-fields {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* Fields that belong together on one line, and stack themselves once a
   column would drop below a usable width. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
  gap: var(--space-4);
}

/* The commit row. Ruled off from the fields so it reads as the end of the
   form and not as one more setting. */
.form-actions-bar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-5);
  border-top: 1px solid var(--border-subtle);
}

/* Secondary or destructive actions sit at the far end, away from the primary
   one, so Delete is never the button next to Save. */
.form-actions-end {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-left: auto;
}

@media (max-width: 640px) {
  .form-actions-end {
    width: 100%;
    margin-left: 0;
  }
}

/* Inline verdicts under the field they belong to, rather than in the page's
   status box at the bottom of the form. Both were written twice, once for
   auto transitions and once for report rules. */
.cron-preview {
  color: var(--text-secondary);
  font-size: var(--text-xs);
}

.cron-preview[data-state="invalid"] {
  color: var(--danger);
}

.jql-result {
  margin: 0;
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
  overflow-wrap: anywhere;
}

.jql-result:empty                    { display: none; }
.jql-result[data-state="pending"]    { color: var(--text-tertiary); }
.jql-result[data-state="valid"]      { color: var(--success); }
.jql-result[data-state="invalid"]    { color: var(--danger); }

/* ---------- Card ---------- */

/* One saved rule: an auto transition, a report rule, a webhook endpoint.
   The three screens drew the same object under three prefixes and had already
   drifted apart in padding. */
.card-list {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.card-head {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
  padding: var(--space-4) var(--space-4) 0;
}

.card-title {
  min-width: 0;
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  overflow-wrap: anywhere;
}

/* Pushed to the far right so the menu never sits between two badges and never
   moves as the badge text changes length. */
.card-menu {
  margin-left: auto;
}

/* The free-text name, once the rule's own content became the headline. */
.card-sub {
  margin-top: var(--space-2);
  padding: 0 var(--space-4);
  color: var(--text-secondary);
  font-size: var(--text-sm);
}

.card-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  margin-top: var(--space-2);
  padding: 0 var(--space-4) var(--space-4);
  color: var(--text-tertiary);
  font-size: var(--text-xs);
}

/* A subtitle already opened the gap, so the meta line tightens against it
   instead of adding a second full step. */
.card-sub + .card-meta {
  margin-top: var(--space-1);
}

.card-meta-item {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* Separator drawn rather than typed, so it never lands at the start of a
   wrapped line. */
.card-meta-item + .card-meta-item::before {
  content: "";
  flex: 0 0 auto;
  width: 3px;
  height: 3px;
  margin-right: var(--space-2);
  border-radius: 50%;
  background: var(--border-strong);
}

.card-meta code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* An alert or a token notice inside a card lines up with the card's own text
   column rather than bleeding to its edges. */
.card-notice {
  margin: 0 var(--space-4) var(--space-4);
}

.card > .disclosure {
  border-top: 1px solid var(--border-subtle);
}

/* ---------- List rows ---------- */

/* A row that navigates somewhere: a Telegram channel, a chat mapping, a saved
   alias. The whole row is the link, rather than a "Manage" button parked at
   the right edge, because the row is what the pointer is already on. */
.list {
  display: flex;
  flex-direction: column;
}

/* Remove sits outside the link: a button inside an anchor is invalid markup
   and unreachable by keyboard. */
.list-item {
  display: flex;
  align-items: stretch;
  border-bottom: 1px solid var(--border-subtle);
}

.list-item:last-child {
  border-bottom: 0;
}

.list-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex: 1 1 auto;
  min-width: 0;
  padding: var(--space-3) var(--space-4);
  color: inherit;
  text-decoration: none;
}

.list-row:hover {
  background: var(--bg-hover);
}

.list-text {
  flex: 1 1 auto;
  min-width: 0;
}

/* The human-readable name is the title and is always set in the UI face; the
   identifier under it is always mono. Notifications and Issue creation used
   to make the opposite choice from each other for the same two values. */
.list-title {
  display: block;
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.list-sub {
  display: block;
  margin-top: 2px;
  color: var(--text-tertiary);
  font-size: var(--text-xs);
}

/* Telegram chat ids are long signed integers that get compared against what
   was pasted from Telegram, so they are never proportionally spaced. */
.list-sub-mono {
  font-family: var(--font-mono);
}

.list-meta {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* Singular and plural labels have different text widths. Keeping the badge in
   one visual column stops a row reading as indented because it says "1 rule". */
.list-meta .badge {
  min-width: 64px;
  justify-content: center;
}

.list-chevron {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  color: var(--text-tertiary);
}

.list-row:hover .list-chevron {
  color: var(--text-primary);
}

.list-item-actions {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  padding-right: var(--space-3);
}

/* ---------- Status grid ---------- */

/* The operational state of one area, read before anything is changed. Grid
   lines are drawn by overlapping cell outlines rather than by a coloured
   container showing through 1px gaps: with auto-fit the last row is usually
   short, and a coloured container paints those empty tracks. */
.status-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 1px;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background: var(--bg-surface);
  box-shadow: 0 0 0 1px var(--border-subtle);
}

.status-cell {
  padding: var(--space-4);
  background: var(--bg-surface);
  box-shadow: 0 0 0 1px var(--border-subtle);
}

.status-label {
  color: var(--text-tertiary);
  font-size: var(--text-xs);
}

.status-value {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
}

/* State is carried by a dot as well as by wording, so it survives greyscale
   and colour blindness. */
.status-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
}

.status-dot[data-tone="ok"]      { background: var(--success); }
.status-dot[data-tone="warning"] { background: var(--warning); }
.status-dot[data-tone="danger"]  { background: var(--danger); }
.status-dot[data-tone="off"]     { background: var(--text-disabled); }

/* ---------- Result summary ---------- */

/* How many rows the current filter matched. Replaces the "Loaded N events"
   success message: loading a page is not an achievement and does not deserve
   a green box, so the count sits quietly above the rows instead. */
.result-summary {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-3) var(--space-4);
  border-bottom: 1px solid var(--border-subtle);
  color: var(--text-tertiary);
  font-size: var(--text-xs);
}

.result-summary-note {
  color: var(--warning);
}

/* ---------- Badge ---------- */

/* Jira statuses, counts and modes. A badge states a fact; it is never
   clickable, which is why it has no hover state and no pointer cursor. */
.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 100%;
  padding: 1px var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  line-height: 18px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.badge-accent  { border-color: color-mix(in srgb, var(--accent) 35%, transparent);  background: var(--accent-subtle);  color: var(--text-accent); }
.badge-success { border-color: color-mix(in srgb, var(--success) 35%, transparent); background: var(--success-subtle); color: var(--success); }
.badge-danger  { border-color: color-mix(in srgb, var(--danger) 35%, transparent);  background: var(--danger-subtle);  color: var(--danger); }
.badge-warning { border-color: color-mix(in srgb, var(--warning) 35%, transparent); background: var(--warning-subtle); color: var(--warning); }

/* A raw identifier standing in for a name Jira did not return. Set in mono so
   it reads as an id: in the UI face, a badge saying "10020" in the slot where
   every other badge says "Ready for QA" looks like a status with a numeric
   name rather than one whose name is missing. */
.badge-id {
  font-family: var(--font-mono);
  font-weight: var(--weight-normal);
}

/* State that must survive greyscale and colour blindness carries a dot as
   well as a hue, so "enabled" is never signalled by colour alone. */
.badge-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: 0 0 auto;
}

/* ---------- Combobox ---------- */

/* Enhances a native <select> that has too many options to scan. The select
   stays in the DOM as the value holder, so everything reading .value keeps
   working; this is purely a different way to pick. */
.combobox {
  position: relative;
}

.combobox-trigger {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid var(--border-interactive);
  border-radius: var(--radius-md);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--text-md);
  text-align: left;
  cursor: pointer;
}

.combobox-trigger:hover {
  border-color: var(--border-strong);
}

.combobox-value {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.combobox-value[data-placeholder="true"] {
  color: var(--text-tertiary);
}

.combobox-caret {
  flex: 0 0 auto;
  width: 12px;
  height: 12px;
  color: var(--text-tertiary);
}

.combobox-panel {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: var(--z-dropdown);
  padding: var(--space-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-raised);
  box-shadow: var(--shadow-lg);
}

.combobox-panel[hidden] {
  display: none;
}

.combobox-search {
  width: 100%;
  min-height: 32px;
  margin-bottom: var(--space-1);
  padding: 0 var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-surface);
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--text-md);
}

.combobox-list {
  max-height: 260px;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}

.combobox-option {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: var(--space-2);
  border-radius: var(--radius-sm);
  cursor: pointer;
}

.combobox-option[aria-selected="true"] {
  background: var(--bg-selected);
}

.combobox-option.is-active {
  background: var(--bg-hover);
}

.combobox-option-name {
  flex: 1 1 auto;
  min-width: 0;
  color: var(--text-primary);
  font-size: var(--text-md);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The identifier stays visible at all times. Jira admins routinely create
   statuses whose names differ only by punctuation or a trailing space, and
   the id is the only thing that tells them apart. */
.combobox-option-meta {
  flex: 0 0 auto;
  color: var(--text-tertiary);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* Makes an otherwise invisible trailing space visible, so "тест" and
   "тест " can be told apart by eye and not only by id. */
.combobox-ws {
  color: var(--warning);
  font-family: var(--font-mono);
}

.combobox-empty {
  padding: var(--space-3) var(--space-2);
  color: var(--text-tertiary);
  font-size: var(--text-sm);
}

.combobox-empty[hidden] {
  display: none;
}

/* ---------- Chip ---------- */

/* A removable selection. Distinct from .badge, which states a fact and is
   never interactive. */
.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chips:empty {
  display: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  max-width: 100%;
  padding: 2px 2px 2px var(--space-2);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-sm);
  background: var(--bg-subtle);
  color: var(--text-primary);
  font-size: var(--text-xs);
  line-height: 20px;
}

.chip > span {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The remove control is a 20px target inside the chip rather than a bare
   glyph. It only gains contrast on hover, so a row of ten chips reads as
   ten labels instead of ten crosses. */
.chip > button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-tertiary);
  cursor: pointer;
}

.chip > button:hover {
  background: var(--danger-subtle);
  color: var(--danger);
}

.chip > button[data-field-up]:hover,
.chip > button[data-field-down]:hover {
  background: var(--accent-subtle);
  color: var(--accent);
}

.chip > button:disabled {
  opacity: 0.35;
  cursor: default;
  background: transparent;
  color: var(--text-tertiary);
}

.chip > button svg {
  width: 10px;
  height: 10px;
}

/* ---------- Disclosure ---------- */

.disclosure {
  border-top: 1px solid var(--border-subtle);
}

.disclosure > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-4);
  color: var(--text-secondary);
  font-size: var(--text-xs);
  cursor: pointer;
  list-style: none;
}

.disclosure > summary::-webkit-details-marker {
  display: none;
}

.disclosure > summary::before {
  content: "";
  width: 0;
  height: 0;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform var(--duration-fast) var(--ease-out);
}

.disclosure[open] > summary::before {
  transform: rotate(90deg);
}

.disclosure > summary:hover {
  color: var(--text-primary);
}

.disclosure-body {
  padding: 0 var(--space-4) var(--space-4);
}

/* ---------- Key/value rows ---------- */

/* For the technical detail behind a disclosure. A grid rather than
   "Label: value" text so the values line up and can be scanned down a
   column instead of read sentence by sentence. */
.kv {
  display: grid;
  grid-template-columns: minmax(120px, max-content) minmax(0, 1fr);
  gap: var(--space-2) var(--space-4);
  font-size: var(--text-xs);
}

.kv dt {
  color: var(--text-tertiary);
}

.kv dd {
  margin: 0;
  color: var(--text-secondary);
  overflow-wrap: anywhere;
}

.kv code {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

/* ---------- Menu ---------- */

.menu-wrap {
  position: relative;
  display: inline-flex;
}

.menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: var(--z-dropdown);
  min-width: 180px;
  padding: var(--space-1);
  border: 1px solid var(--border-default);
  border-radius: var(--radius-md);
  background: var(--bg-raised);
  box-shadow: var(--shadow-lg);
}

.menu[hidden] {
  display: none;
}

.menu-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  width: 100%;
  min-height: 30px;
  padding: 0 var(--space-2);
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-primary);
  font-family: inherit;
  font-size: var(--text-md);
  text-align: left;
  cursor: pointer;
}

.menu-item:hover {
  background: var(--bg-hover);
}

.menu-item-danger {
  color: var(--danger);
}

.menu-item-danger:hover {
  background: var(--danger-subtle);
}

.menu-separator {
  height: 1px;
  margin: var(--space-1) 0;
  background: var(--border-subtle);
}

/* ---------- Dialog ---------- */

/* Native <dialog>: focus trapping, Escape and inertness of the page behind
   are handled by the platform rather than reimplemented. Replaces the
   window.confirm() calls, which cannot say what is about to happen. */
.dialog {
  width: min(420px, calc(100vw - 2 * var(--space-4)));
  padding: 0;
  border: 1px solid var(--border-default);
  border-radius: var(--radius-xl);
  background: var(--bg-surface);
  color: var(--text-primary);
  box-shadow: var(--shadow-lg);
}

.dialog::backdrop {
  background: rgba(0, 0, 0, 0.45);
}

/* A dialog that collects an answer needs room for controls; one that only
   asks for consent does not, and staying narrow keeps it readable. */
.dialog-form {
  width: min(520px, calc(100vw - 2 * var(--space-4)));
}

.dialog-body {
  padding: var(--space-5);
}

.dialog-fields {
  margin-top: var(--space-4);
}

.dialog-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.dialog-text {
  margin: 0;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

.dialog-footer {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  padding: var(--space-4) var(--space-5);
  border-top: 1px solid var(--border-subtle);
  background: var(--bg-subtle);
  border-radius: 0 0 var(--radius-xl) var(--radius-xl);
}

/* ---------- Empty state ---------- */

.empty-state {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-2);
  padding: var(--space-10) var(--space-4);
}

.empty-state-title {
  color: var(--text-primary);
  font-size: var(--text-md);
  font-weight: var(--weight-medium);
}

/* Empty is a state to explain, not an apology. It says what will appear
   here and what to do to make it appear. */
.empty-state-body {
  max-width: 52ch;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

/* An empty state may offer more than one way out, so the actions are a row
   in their own right rather than a single button with a margin. */
.empty-state-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* ---------- Legacy status line ---------- */

/* setStatus() in app.js replaces className outright with "status ok" or
   "status bad", so .alert is stripped. These rules give the same box to the
   shared helper without changing it, which keeps every not-yet-migrated
   screen working. pre-line matters: the webhook token message is two lines
   separated by \n. */
.status:empty {
  display: none;
}

.status.ok,
.status.bad {
  display: block;
  margin-top: var(--space-4);
  padding: var(--space-3);
  border: 1px solid;
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  white-space: pre-line;
  overflow-wrap: anywhere;
}

.status.ok {
  border-color: color-mix(in srgb, var(--success) 35%, transparent);
  background: var(--success-subtle);
  color: var(--success);
}

.status.bad {
  border-color: color-mix(in srgb, var(--danger) 35%, transparent);
  background: var(--danger-subtle);
  color: var(--danger);
}

/* Defensive fallback for page-specific status helpers that may leave their
   tone class behind while clearing the message. Empty feedback has no UI. */
.status.ok:empty,
.status.bad:empty {
  display: none;
}

/* Inside a card, .card-notice already owns the inset that lines the message
   up with the card's text column. The legacy top margin would double it. */
.card-notice.status.ok,
.card-notice.status.bad {
  margin-top: 0;
}

/* ---------- Link ---------- */

.link {
  color: var(--text-accent);
  text-decoration: none;
  border-radius: var(--radius-sm);
}

.link:hover {
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* ---------- Text helpers ---------- */

.text-secondary { color: var(--text-secondary); }
.text-tertiary  { color: var(--text-tertiary); }
.text-sm        { font-size: var(--text-sm); }
.text-xs        { font-size: var(--text-xs); }
