@charset "UTF-8";
/* ==========================================================================
   yelatix — the component layer
   Hand-built. No framework, no build step: this file is served as written.

   HOW TO READ IT
   Colour is declared once per mode as a set of ROLES, and referenced by role
   everywhere below. Nothing past the token block names a colour, which is what
   makes light and dark one change rather than two stylesheets, and what makes
   a skin a short file rather than a fork.

   Every value in the token block passes tools/contrast.py. Run it after
   touching one.

   THE SHAPE OF THE THING
   Raised surfaces are drawn with a border and a one-pixel top highlight
   rather than a drop shadow. Shadows are kept for what genuinely floats — a
   dialog, a menu, a drag — so that when something does cast one it means
   "this is above the page" instead of "this is a box".
   ========================================================================== */

/* --- tokens: dark (the default) ------------------------------------------- */
:root {
  color-scheme: dark;

  --bg:            #0e1017;
  --surface-1:     #121419;
  --surface-2:     #1a1e29;
  --surface-3:     #232835;
  --border:        #2a3040;
  --border-strong: #3b4254;

  --text-1: #e9ebf2;
  --text-2: #9ba3b8;
  /* Only ever carries information that is repeated elsewhere on the same
     screen, which is why it is held to 3:1 rather than 4.5:1. */
  --text-3: #7a8196;

  /* Two accents, not one. --accent is the text weight: it has to be legible
     on a surface, so it is light. --accent-solid is the fill weight: white
     ink has to be legible on IT, so it is dark. One value cannot do both on a
     dark background, and the usual fudge — light fill, dark ink — makes a
     primary button look like a highlighter. */
  --accent:         #978afa;
  --accent-hi:      #a695ff;
  --accent-solid:   #6450db;
  --accent-solid-hi:#6f5be8;
  --accent-ink:     #ffffff;
  --accent-soft:    rgb(151 138 250 / 15%);
  --accent-line:    rgb(151 138 250 / 34%);

  --good:     #2ec46a;
  --warning:  #f59e0b;
  --critical: #f4676b;
  --info:     #3ba9f0;
  --neutral:  #979fb3;

  --good-soft:     rgb(46 196 106 / 15%);
  --warning-soft:  rgb(245 158 11 / 15%);
  --critical-soft: rgb(244 103 107 / 15%);
  --info-soft:     rgb(59 169 240 / 15%);
  --neutral-soft:  rgb(139 147 168 / 15%);

  /* The one-pixel highlight along the top of anything raised. On a light
     background it would be a shine on white; there it inverts to a hairline
     of the border colour instead. */
  --lift: inset 0 1px 0 rgb(255 255 255 / 4%);

  --font: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Inter", "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  /* --- the type scale ------------------------------------------------------
     Seven steps, and NOTHING outside them. The first version of this file had
     two hundred font-size declarations and seventy-four of them were under
     12px — which is how an interface ends up technically complete and
     genuinely hard to read. A scale referenced by name is the only way the
     smallest step stays a deliberate decision rather than the result of
     somebody shaving half a pixel off a component at a time.

     --t-micro is the floor and it is 11.5px. It is for UPPERCASE group
     headings and nothing else: they are two or three words, they are read
     once, and the letter-spacing carries them. Anything a person actually
     reads is --t-sm or larger.
     ------------------------------------------------------------------------ */
  --t-micro: 11.5px;   /* uppercase eyebrows only */
  --t-xs:    12.5px;   /* counts, timestamps — repeated elsewhere on the page */
  --t-sm:    13.5px;   /* secondary text: meta lines, captions, hints */
  --t-md:    15px;     /* body, and every row title. The default. */
  --t-lg:    16.5px;   /* a panel or section heading */
  --t-xl:    21px;     /* the page title */
  --t-2xl:   28px;     /* a figure on the overview */

  /* --- control sizes -------------------------------------------------------
     WCAG 2.2 AA asks for 24x24 CSS pixels on anything you have to hit. The
     first version of this interface failed that on 153 of its 505 targets —
     13px tickboxes, 16px menu triggers, 3x4px priority bars. These are the
     floors everything interactive is built from, and --tap is the one to
     reach for unless there is a reason not to.

     --tap-sm is for controls that sit inside a dense row and already have the
     whole row as a generous secondary target.
     ------------------------------------------------------------------------ */
  --tap: 40px;         /* the comfortable default for a real control */
  --tap-sm: 32px;      /* inside a row, where the row itself is also a target */
  --tap-min: 24px;     /* the floor. Nothing interactive goes under this. */

  --r-sm: 5px;
  --r-md: 9px;
  --r-lg: 14px;

  --shadow-1: 0 1px 2px rgb(0 0 0 / 40%);
  --shadow-2: 0 10px 30px -12px rgb(0 0 0 / 70%);
  --shadow-3: 0 28px 70px -20px rgb(0 0 0 / 80%);

  /* The contextual rail, and what it shrinks to. Both are tokens because the
     rail's collapsed state is a cookie away and the grid has to be able to
     follow it without a second breakpoint. */
  --side-w: 252px;
  --side-icons: 60px;

  /* The one bar that is always there. Everything else that sticks — the facet
     bar, a group heading — parks under it, so it is a token and they are
     expressed in terms of it rather than in a repeated 48px. */
  --top-h: 56px;
  --facets-h: 56px;

  /* One row of the queue. A token because the keyboard scrolls by it and a
     skin that draws a taller row has to be able to say so once.

     52px, not 38px: a row carries a tickbox and three menus, and at 38px each
     of those was a 16px target stacked against its neighbours. The row is the
     thing being worked down all day; it is worth the pixels. */
  --row-h: 52px;

  --ease: cubic-bezier(.2, .7, .3, 1);
}

/* --- tokens: light -------------------------------------------------------- */
:root[data-theme="light"] {
  color-scheme: light;

  --bg:            #f4f5f9;
  --surface-1:     #ffffff;
  --surface-2:     #f4f5f9;
  --surface-3:     #e9ebf2;
  --border:        #e4e7ef;
  --border-strong: #cfd4e0;

  --text-1: #13151c;
  --text-2: #545b6d;
  --text-3: #7c8397;

  --accent:         #5a46cd;
  --accent-hi:      #5140bb;
  /* The solid is the SAME value in both modes, so a primary button is one
     colour a user learns once. It is the text weight that has to move. */
  --accent-solid:   #6450db;
  --accent-solid-hi:#5a46cd;
  --accent-soft:    rgb(90 70 205 / 10%);
  --accent-line:    rgb(90 70 205 / 26%);

  --good:     #146e36;
  --warning:  #8a5a00;
  --critical: #b92828;
  --info:     #0369a1;
  --neutral:  #5b6377;

  --good-soft:     rgb(20 110 54 / 12%);
  --warning-soft:  rgb(138 90 0 / 12%);
  --critical-soft: rgb(185 40 40 / 12%);
  --info-soft:     rgb(3 105 161 / 12%);
  --neutral-soft:  rgb(91 99 119 / 12%);

  --lift: inset 0 1px 0 rgb(255 255 255 / 90%);

  --shadow-1: 0 1px 2px rgb(19 21 28 / 6%);
  --shadow-2: 0 10px 30px -14px rgb(19 21 28 / 20%);
  --shadow-3: 0 28px 70px -22px rgb(19 21 28 / 28%);
}

/* --- reset ---------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font);
  font-size: var(--t-md);
  line-height: 1.55;
  color: var(--text-1);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

button, input, select, textarea { font: inherit; color: inherit; }
img, svg { display: block; max-width: 100%; }
svg.icon { flex: none; }

/* Figures line up in columns because a ticket list is read by scanning down
   one, not by reading across. */
.num, .mono, table.grid td.num, .figure-n { font-variant-numeric: tabular-nums; }

:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}

::selection { background: var(--accent-soft); }

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

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  padding: 10px 14px; border-radius: var(--r-md);
  background: var(--accent-solid); color: var(--accent-ink);
}
.skip-link:focus { left: 12px; top: 12px; }

/* --- the shell ------------------------------------------------------------
   TWO LEVELS, AND ONLY ONE OF THEM IS ALWAYS THERE.

   A bar across the top carrying the things that belong to the application —
   the mark, the sections, the command bar, your account — and under it a rail
   carrying only what is inside the section you are in. A page with no siblings
   (your profile, your notifications) gets no rail at all and the full width,
   because a navigation column listing one destination is a margin with a
   border on it.

   The rail collapses to its icons through a cookie, which reaches this file as
   data-rail on .app. It is one custom property and no second layout.
   -------------------------------------------------------------------------- */
.app {
  display: grid;
  grid-template-columns: var(--side-w) minmax(0, 1fr);
  grid-template-rows: var(--top-h) minmax(0, 1fr);
  min-height: 100vh;
}
.app[data-rail="icons"] { --side-w: var(--side-icons); }
.app[data-norail] { grid-template-columns: minmax(0, 1fr); }

/* --- the bar across the top ----------------------------------------------- */
.top {
  grid-column: 1 / -1;
  position: sticky;
  top: 0;
  z-index: 30;
  display: flex;
  align-items: center;
  gap: 8px;
  height: var(--top-h);
  padding: 0 10px 0 12px;
  background: var(--surface-1);
  border-bottom: 1px solid var(--border);
}
.top .spacer { margin-left: auto; }

.mark-link { display: flex; align-items: center; flex: none; }
.mark-link:hover { text-decoration: none; }
.brand-mark { width: 24px; height: 24px; flex: none; border-radius: 6px; }

/* The sections. Not a sidebar list turned sideways: there are three of them at
   most, they are the coarsest division in the application, and the current one
   is marked with a line under the text rather than a filled pill — a pill here
   would compete with the accent that means "this row is selected" ten pixels
   below it. */
.sections {
  display: flex;
  align-items: center;
  gap: 2px;
  min-width: 0;
  margin-left: 6px;
}
.section {
  position: relative;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 0 13px;
  height: calc(var(--top-h) - 1px);
  color: var(--text-2);
  font-size: var(--t-md);
  font-weight: 520;
  white-space: nowrap;
}
.section:hover { color: var(--text-1); text-decoration: none; }
.section[aria-current] { color: var(--text-1); font-weight: 580; }
.section[aria-current]::after {
  content: "";
  position: absolute;
  left: 10px; right: 10px; bottom: 0;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--accent);
}

/* A count beside anything. Quiet by default: a number you glance at, not a
   badge that shouts. */
.tally {
  flex: none;
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--surface-3);
  color: var(--text-3);
  font-size: var(--t-micro); font-weight: 600;
  line-height: 16px;
  font-variant-numeric: tabular-nums;
  text-align: center;
}
.tally.is-urgent { background: var(--critical-soft); color: var(--critical); }

/* The command bar. Sized like an input and behaving like a button, because
   what it opens is a dialog — and an input that does not accept typing where
   it sits is a lie about what pressing it does. */
.cmd {
  display: flex;
  align-items: center;
  gap: 9px;
  height: var(--tap);
  min-width: 0;
  flex: 0 1 340px;
  margin-left: 12px;
  padding: 0 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text-3);
  font-size: var(--t-sm);
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.cmd:hover { border-color: var(--border-strong); color: var(--text-2); text-decoration: none; }
.cmd .icon { flex: none; }
.cmd-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cmd kbd { margin-left: auto; }

kbd {
  display: inline-block;
  min-width: 20px;
  padding: 2px 6px;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface-1);
  color: var(--text-3);
  font-family: var(--font);
  font-size: var(--t-micro);
  font-weight: 560;
  line-height: 14px;
  text-align: center;
}

.top-icon {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--tap); height: var(--tap);
  border-radius: var(--r-md);
  color: var(--text-2);
  flex: none;
}
.top-icon:hover { background: var(--surface-2); color: var(--text-1); text-decoration: none; }
.top-icon[aria-current] { color: var(--accent); background: var(--accent-soft); }

/* Unread, as a dot rather than a number. The count is on the page it leads to
   and in the account menu; up here the only question being answered is
   "is there anything". */
.pip {
  position: absolute;
  top: 5px; right: 5px;
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--critical);
  box-shadow: 0 0 0 2px var(--surface-1);
}

/* --- the account menu ------------------------------------------------------
   A <details>, so it opens on a page whose script never arrived exactly as it
   does on one whose did.
   -------------------------------------------------------------------------- */
.menu { position: relative; flex: none; }
.menu > summary {
  display: grid;
  place-items: center;
  width: var(--tap); height: var(--tap);
  border-radius: 50%;
  cursor: pointer;
  list-style: none;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::marker { content: ""; }
.menu[open] > summary, .menu > summary:hover { box-shadow: 0 0 0 2px var(--accent-line); }

.menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 40;
  min-width: 216px;
  padding: 6px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}
.menu-who { padding: 7px 9px 9px; display: grid; }
.menu-name { font-size: var(--t-md); font-weight: 580; }
.menu-role { font-size: var(--t-xs); color: var(--text-3); }
.menu-sep { height: 1px; background: var(--border); margin: 5px 0; }
.menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  min-height: var(--tap);
  padding: 8px 11px;
  border: 0;
  border-radius: var(--r-md);
  background: none;
  color: var(--text-2);
  font-size: var(--t-sm);
  text-align: left;
  cursor: pointer;
}
.menu-item:hover, .menu-item:focus-visible { background: var(--surface-2); color: var(--text-1); text-decoration: none; }
.menu-item .icon { color: var(--text-3); flex: none; }
.menu-item .tally, .menu-item kbd { margin-left: auto; }
.menu-row { display: flex; align-items: center; gap: 8px; padding: 4px 9px 6px; }
.menu-label { font-size: var(--t-xs); color: var(--text-3); margin-right: auto; }

/* --- the contextual rail ---------------------------------------------------
   No brand block and no account foot: both of those belong to the whole
   application rather than to a section, and both are up in the top bar. What
   is left is the inside of one section, which is the only thing this is for.
   -------------------------------------------------------------------------- */
.side {
  grid-row: 2;
  position: sticky;
  top: var(--top-h);
  height: calc(100vh - var(--top-h));
  display: flex;
  flex-direction: column;
  min-width: 0;
  border-right: 1px solid var(--border);
  background: var(--surface-1);
}

.side-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px 8px;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

.side-group + .side-group { margin-top: 16px; }

.side-label {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 8px 4px;
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.side-more { margin-left: auto; color: var(--text-3); display: flex; }
.side-more:hover { color: var(--text-1); }

/* A selected item is a tinted panel with a hairline ring, not a bar in the
   gutter. The ring is an inset shadow rather than a border so that selecting
   an item does not move it by a pixel. */
.side-item {
  position: relative;
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: var(--tap);
  padding: 8px 10px;
  border-radius: var(--r-md);
  color: var(--text-2);
  font-size: var(--t-md);
  font-weight: 500;
  white-space: nowrap;
  transition: background .13s var(--ease), color .13s var(--ease);
}
.side-item:hover { background: var(--surface-2); color: var(--text-1); text-decoration: none; }
.side-item .icon { color: var(--text-3); flex: none; transition: color .13s var(--ease); }
.side-item:hover .icon { color: var(--text-2); }
.side-item .label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.side-item .tally { margin-left: auto; }

.side-item[aria-current] {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--text-1);
  font-weight: 560;
}
.side-item[aria-current] .icon { color: var(--accent); }
.side-item[aria-current] .tally { background: var(--accent-line); color: var(--text-1); }

/* A project's colour, as a dot. */
.dot {
  width: 8px; height: 8px; flex: none;
  border-radius: 50%;
  background: var(--dot, var(--text-3));
}
.dot.sm { width: 6px; height: 6px; }

/* A status's colour, as a square — so that a status and a project are told
   apart by shape and not only by hue. */
.swatch {
  width: 8px; height: 8px; flex: none;
  border-radius: 2px;
  background: var(--tone, var(--text-3));
}

.side-collapse { flex: none; border-top: 1px solid var(--border); padding: 6px; }
.side-collapse button {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--r-md);
  background: none;
  color: var(--text-3);
  font-size: var(--t-sm);
  cursor: pointer;
  white-space: nowrap;
}
.side-collapse button:hover { background: var(--surface-2); color: var(--text-1); }
.side-collapse .icon { flex: none; }

/* Collapsed: the labels go, the icons centre, and every one of them keeps its
   title attribute — which is where the name went, not away. */
.app[data-rail="icons"] .side-item .label,
.app[data-rail="icons"] .side-item .tally,
.app[data-rail="icons"] .side-label span,
.app[data-rail="icons"] .side-more,
.app[data-rail="icons"] .side-collapse .label { display: none; }
.app[data-rail="icons"] .side-item { justify-content: center; padding-inline: 0; }
.app[data-rail="icons"] .side-collapse button { justify-content: center; }
.app[data-rail="icons"] .side-label {
  justify-content: center;
  padding: 6px 0 4px;
}
/* The group heading has nothing left to say when the labels are gone, so it
   becomes the rule it was already acting as. */
.app[data-rail="icons"] .side-label::before {
  content: "";
  width: 18px;
  height: 1px;
  background: var(--border);
}

/* --- the page column ------------------------------------------------------- */
.frame { grid-row: 2; min-width: 0; display: flex; flex-direction: column; }

/* The page's own heading row. NOT sticky, and that is the change: the old
   masthead stuck to the top and the facet bar stuck under it, so two bars
   followed the reader down a list whose own group headings wanted that room.
   The title is read once on arrival. The facet bar is the one that has to
   stay, because it is the thing a reader checks before believing the list. */
.head {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 52px;
  padding: 10px 18px;
}
.head .spacer { margin-left: auto; }
.head:has(.page-sub) { flex-wrap: wrap; row-gap: 2px; padding-top: 16px; }
/* Under the whole row — title, trail and actions — on a line of its own. */
.head .page-sub { order: 10; flex-basis: 100%; max-width: none; font-size: var(--t-sm); color: var(--text-2); }

.page-title {
  font-size: var(--t-xl); font-weight: 650; letter-spacing: -.025em;
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* The ancestors, where there are any. A sibling of the heading rather than a
   wrapper around it, so a skin can restyle the heading alone. */
.trail { display: flex; align-items: center; gap: 6px; min-width: 0; font-size: var(--t-sm); }
.trail .icon { color: var(--text-3); flex: none; }
.trail a { color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.trail a:hover { color: var(--text-1); text-decoration: none; }

.sheet { padding: 0 18px 24px; max-width: 1500px; width: 100%; }
.sheet.wide { max-width: none; }
/* The queue draws its own edges, so it wants the page's full width. */
.sheet.flush { padding: 0; max-width: none; }
.stack { display: grid; gap: 16px; }
.stack.tight { gap: 10px; }
.stack.loose { gap: 24px; }

/* --- the facet bar --------------------------------------------------------
   The row under the top bar on every list. It is the signature control of
   the application: a ticket system is a filter with some rows attached, and
   burying that behind a "Filters" button would cost a click on every visit
   while hiding the current state, which is the thing a reader most needs to
   know before believing the list.

   Every facet is a plain <form> control that submits on change. No script is
   required for any of it — the enhancement script only adds the debounce on
   the search box.
   -------------------------------------------------------------------------- */
.facets {
  position: sticky;
  top: var(--top-h);
  z-index: 19;
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  min-height: var(--facets-h);
  padding: 7px 18px;
  background: var(--surface-1);
  border-block: 1px solid var(--border);
}
.facets .spacer { margin-left: auto; }

.seek { position: relative; display: flex; align-items: center; flex: 1 1 260px; max-width: 380px; }
.seek .icon { position: absolute; left: 12px; color: var(--text-3); pointer-events: none; }
.seek .input { padding-left: 38px; width: 100%; }
.seek .clear {
  position: absolute; right: 6px;
  display: grid; place-items: center;
  width: 20px; height: 20px;
  border: 0; border-radius: 50%;
  background: var(--surface-3); color: var(--text-2);
  cursor: pointer;
}
.seek .clear:hover { color: var(--text-1); }

/* A facet that is set looks set. The whole point of leaving the controls on
   screen is that their state is readable without opening anything. */
.facet {
  appearance: none;
  min-height: var(--tap);
  padding: 8px 32px 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2) var(--caret) no-repeat right 10px center;
  color: var(--text-2);
  font-size: var(--t-md);
  cursor: pointer;
  max-width: 200px;
  transition: border-color .15s var(--ease), color .15s var(--ease);
}
.facet:hover { border-color: var(--border-strong); color: var(--text-1); }
/* A date is an input, not a menu: it brings its own calendar button, and a
   caret beside that was two affordances for one control. */
input.facet { background-image: none; padding-right: 12px; cursor: text; }
.facet[data-set="1"] {
  border-color: var(--accent-line);
  background-color: var(--accent-soft);
  color: var(--text-1);
  font-weight: 550;
}

/* The four facets that are not on the bar.

   A <details> — so it opens with no script — whose panel FLOATS rather than
   growing the bar. That is not a style preference: --facets-h is what the
   queue's group headings park under when they stick, and a bar that changes
   height when a disclosure opens would leave every heading sticking to the
   wrong place with the bar sitting over it. A panel that floats keeps the
   bar exactly one row tall, always.
   -------------------------------------------------------------------------- */
.facets .more { position: relative; display: flex; }
.facets .more > summary {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  min-height: var(--tap);
  padding: 8px 13px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: var(--t-sm);
  cursor: pointer;
  list-style: none;
  white-space: nowrap;
}
.facets .more > summary::-webkit-details-marker { display: none; }
.facets .more > summary::marker { content: ""; }
.facets .more > summary:hover { border-color: var(--border-strong); color: var(--text-1); }
.facets .more[open] > summary,
.facets .more > summary:has(.tally) {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  color: var(--text-1);
}
.facets .more .icon { flex: none; }

.more-row {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 22;
  display: grid;
  gap: 7px;
  min-width: 200px;
  padding: 9px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}
.more-row .facet { max-width: none; width: 100%; }

/* One SVG caret, declared once, inheriting nothing — a data URI cannot read a
   custom property, so the two colours are the two it may be drawn in and the
   light theme swaps the whole token. */
:root {
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ba3b8' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
}
:root[data-theme="light"] {
  --caret: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23545b6d' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9.5 6 6 6-6'/%3E%3C/svg%3E");
}

/* --- typography ----------------------------------------------------------- */
h1, h2, h3, h4 { font-weight: 620; letter-spacing: -.015em; line-height: 1.3; }

.lede { color: var(--text-2); max-width: 64ch; }
.muted { color: var(--text-2); }
.dim { color: var(--text-3); }
.small { font-size: var(--t-sm); }
.tiny { font-size: var(--t-xs); }
.nowrap { white-space: nowrap; }
.truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mono { font-family: var(--mono); font-size: .93em; letter-spacing: -.01em; }

.eyebrow {
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}

.hint { font-size: var(--t-sm); color: var(--text-3); }
/* An icon leading a hint sits in the line with it; every <svg> is a block by
   default, which put it on a line of its own above the sentence. */
.hint > .icon:first-child { display: inline-block; vertical-align: -2px; margin-right: 4px; }

/* --- buttons -------------------------------------------------------------- */
/* Every button clears --tap. A 23px "Apply" beside a 31px select is not a
   smaller button, it is a button somebody misses. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  min-height: var(--tap);
  padding: 8px 15px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  box-shadow: var(--lift);
  color: var(--text-1);
  font-size: var(--t-md); font-weight: 550;
  line-height: 1.4;
  cursor: pointer;
  white-space: nowrap;
  transition: background .14s var(--ease), border-color .14s var(--ease), color .14s var(--ease);
}
.btn:hover { background: var(--surface-3); border-color: var(--border-strong); text-decoration: none; }
.btn:active { transform: translateY(.5px); }
.btn[disabled], .btn.is-disabled { opacity: .45; cursor: not-allowed; pointer-events: none; }

.btn-primary {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--accent-ink);
}
.btn-primary:hover { background: var(--accent-solid-hi); border-color: var(--accent-solid-hi); }
.btn-primary .icon { color: currentColor; }

.btn-quiet {
  background: transparent; border-color: transparent; box-shadow: none;
  color: var(--text-2); padding: 8px 11px;
}
.btn-quiet:hover { background: var(--surface-2); border-color: transparent; color: var(--text-1); }

.btn-danger { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 38%, transparent); background: transparent; box-shadow: none; }
.btn-danger:hover { background: var(--critical-soft); border-color: var(--critical); color: var(--critical); }

/* The small button is still a real target: --tap-sm is the floor, which is
   32px, which is above the 24px minimum with room to spare. */
.btn-sm { min-height: var(--tap-sm); padding: 6px 11px; font-size: var(--t-sm); }
.btn-icon { min-width: var(--tap); padding: 8px; }
.btn-sm.btn-icon { min-width: var(--tap-sm); padding: 6px; }
.btn-block { width: 100%; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
.btn-row.end { justify-content: flex-end; }

/* A form that is mid-submit says so, so that nobody clicks "Create" twice and
   files two tickets. app.js sets the class; the spinner is CSS. */
.btn.is-busy { opacity: .75; cursor: progress; }
.btn.is-busy::after {
  content: "";
  width: 13px; height: 13px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: spin .7s linear infinite;
}
@keyframes spin { to { rotate: 360deg; } }

/* --- forms ---------------------------------------------------------------- */
.field { display: grid; grid-template-columns: minmax(0, 1fr); gap: 7px; min-width: 0; align-content: start; }
/* align-content: start is load-bearing. In a .form-grid every field in a row
   is as tall as the tallest one, and a grid's auto rows STRETCH into spare
   height by default — so a field with no hint, beside one with a hint, had
   the difference shared out between its label and its input: a fat input,
   pushed down, out of line with its neighbour. */
.field > label, .field > .label {
  font-size: var(--t-sm); font-weight: 560; color: var(--text-2);
}
.field-row { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start; }

.input, .select, .textarea {
  width: 100%;
  min-height: var(--tap);
  padding: 9px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  background: var(--surface-2);
  color: var(--text-1);
  font-size: var(--t-md);
  /* One line-height for every kind of control, so that a date, a number and
     a select beside a text input come out the same height rather than
     whatever each one's user-agent default makes them. */
  line-height: 1.4;
  transition: border-color .14s var(--ease), box-shadow .14s var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--text-3); }
/* Chromium pads the text of a date input by a pixel top and bottom, which
   made every date two pixels taller than the text input beside it. */
input::-webkit-datetime-edit-fields-wrapper { padding: 0; }
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.input:focus, .select:focus, .textarea:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
.input[aria-invalid="true"], .textarea[aria-invalid="true"] {
  border-color: var(--critical);
}
.input[aria-invalid="true"]:focus { box-shadow: 0 0 0 3px var(--critical-soft); }

.textarea { resize: vertical; min-height: 112px; line-height: 1.55; }
.textarea.tall { min-height: 160px; }

.select {
  appearance: none;
  padding-right: 30px;
  background-image: var(--caret);
  background-repeat: no-repeat;
  background-position: right 9px center;
  cursor: pointer;
}

.input.w-xs { max-width: 110px; }
.input.w-sm { max-width: 160px; }
.input.w-md { max-width: 280px; }

/* A colour input is a swatch, not a text box: square, full-height, and the
   browser's own inset frame taken out so the colour fills it. */
.input.color {
  width: var(--tap); height: var(--tap); min-height: 0;
  padding: 4px;
  cursor: pointer;
}
.input.color::-webkit-color-swatch-wrapper { padding: 0; }
.input.color::-webkit-color-swatch { border: 0; border-radius: 4px; }
.input.color::-moz-color-swatch { border: 0; border-radius: 4px; }

/* A file input's button is drawn like our small button, so the one control
   on the form the browser styles is not the one that looks out of place. */
.input[type="file"] { padding: 5px 6px; cursor: pointer; color: var(--text-2); }
.input[type="file"]::file-selector-button {
  margin-right: 10px;
  padding: 5px 11px;
  border: 1px solid var(--border);
  border-radius: calc(var(--r-sm) - 2px);
  background: var(--surface-3);
  color: var(--text-1);
  font: inherit; font-size: var(--t-sm); font-weight: 550;
  cursor: pointer;
}
.input[type="file"]:hover::file-selector-button { border-color: var(--border-strong); }

.check { display: flex; align-items: flex-start; gap: 10px; min-height: var(--tap-sm); padding: 4px 0; font-size: var(--t-md); color: var(--text-2); cursor: pointer; }
.check input { width: 18px; height: 18px; margin-top: 2px; accent-color: var(--accent-solid); flex: none; }
.check strong { display: block; color: var(--text-1); font-weight: 560; font-size: var(--t-md); }
/* A tickbox with a title and a sentence under it: the sentence is a hint, and
   is set like every other hint on the form rather than at body size. */
.check:has(strong) > span { font-size: var(--t-sm); color: var(--text-3); }
.check:has(strong) > span strong { margin-bottom: 2px; }

/* Tickboxes whose labels are chips (labels on a new ticket). The gap inside
   a pair is smaller than the gap between pairs, so each box reads as
   belonging to the chip after it and not the one before. */
.check-chips { display: flex; flex-wrap: wrap; gap: 4px 20px; }
.check-chips .check { align-items: center; gap: 8px; }
.check-chips .check input { margin-top: 0; }

/* A choice of colour. The swatch is the control: the radio is still there,
   under it, taking the clicks and the focus and the arrow keys — it is only
   not drawn, because a grey circle beside every coloured one was noise. */
.swatches { display: flex; flex-wrap: wrap; gap: 4px; }
.swatch-pick {
  position: relative;
  display: grid; place-items: center;
  width: var(--tap-sm); height: var(--tap-sm);
  border-radius: 50%;
  cursor: pointer;
}
.swatch-pick input {
  position: absolute; inset: 0;
  width: 100%; height: 100%; margin: 0;
  opacity: 0; cursor: pointer;
}
.swatch-pick .dot { width: 22px; height: 22px; transition: transform .12s var(--ease); }
.swatch-pick:hover .dot { transform: scale(1.1); }
.swatch-pick:has(input:checked) { box-shadow: inset 0 0 0 2px var(--text-1); }
.swatch-pick:has(input:checked) .dot { width: 18px; height: 18px; }
.swatch-pick:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 2px; }

.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 14px;
}
.form-grid .span-2 { grid-column: 1 / -1; }

.errorlist {
  display: grid; gap: 6px;
  padding: 10px 12px; margin: 0;
  border: 1px solid color-mix(in srgb, var(--critical) 40%, transparent);
  border-radius: var(--r-md);
  background: var(--critical-soft);
  list-style: none;
  font-size: var(--t-sm);
}
.errorlist li { display: flex; gap: 7px; align-items: flex-start; }
.errorlist .icon { color: var(--critical); flex: none; margin-top: 1px; }

/* --- panels --------------------------------------------------------------- */
.panel {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  min-width: 0;
}
.panel-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
  flex-wrap: wrap;
}
.panel-head h2, .panel-head h3 {
  /* Flex, because an icon is an <svg>, and every <svg> is display: block —
     which put the icon on a line of its own above the heading. */
  display: flex; align-items: center; gap: 8px;
  font-size: var(--t-md); font-weight: 600; letter-spacing: -.01em;
}
.panel-head h2 .icon, .panel-head h3 .icon { color: var(--text-3); }
.panel-head .spacer { margin-left: auto; }
.panel-body { padding: 16px; }
.panel-body.flush { padding: 0; }
.panel-foot {
  padding: 11px 16px;
  border-top: 1px solid var(--border);
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
}
.panel-foot .spacer { margin-left: auto; }

/* --- settings pages --------------------------------------------------------
   Every page that is a form — your account, a person, a project, a client,
   the installation's settings — is drawn the same way: a column of sections,
   each with its name and one sentence on the left and its fields on the right.

   WHY THIS SHAPE. The earlier forms poured their fields into one auto-fitting
   grid, so where a field landed was decided by column arithmetic rather than
   by what it was: the theme beside the job title, the colour in a panel of its
   own two screens from the look it colours, a password field alone on a row
   at 280px under two that were 440. Nobody could predict where to look. A
   heading and a sentence per subject says what each group is for before the
   fields ask anything, and a single column of fields is read top to bottom
   without the eye having to zig-zag across a grid to find the next one.

   Fields run the full width of their card by default and are narrowed with a
   width class where the value is short — nobody's handle needs 700px, and a
   box sized to what goes in it says roughly what goes in it.
   -------------------------------------------------------------------------- */
.settings { display: grid; max-width: 1120px; }

.setting-group {
  display: grid;
  grid-template-columns: minmax(0, 290px) minmax(0, 1fr);
  gap: 16px 48px;
  align-items: start;
  padding: 32px 0;
  border-top: 1px solid var(--border);
}
/* No rule above the first section on the page — which is the first child of
   the page's .settings, or the first section inside a form that is. A form's
   first child is its CSRF field, hence of-type there. */
.settings > .setting-group:first-child,
.settings > form:first-child > .setting-group:first-of-type,
form.settings > .setting-group:first-of-type { border-top: 0; padding-top: 12px; }
/* A form that is a run of sections inside the page's column adds no box. */
.settings > form.settings { display: contents; }

/* A field that only means something for a contact: shown once the Contact role
   card is the one picked, by :has() rather than script. */
.for-contact { display: none; }
form:has(input[name="role"][value="contact"]:checked) .for-contact { display: grid; }
/* And the other way round: groups and whole clients are for staff only. */
form:has(input[name="role"][value="contact"]:checked) .for-staff { display: none; }
.pick-list.boxed { border: 1px solid var(--border); border-radius: var(--r-md); max-height: 360px; }

fieldset.field { border: 0; padding: 0; margin: 0; min-inline-size: 0; }
fieldset.field > legend { padding: 0; margin-bottom: 7px; float: left; width: 100%; }
fieldset.field > legend + * { clear: both; }
.check-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 2px 16px; margin-top: 4px; }

.setting-intro { display: grid; gap: 8px; padding-top: 4px; }
.setting-intro h2 {
  display: flex; align-items: center; gap: 12px;
  font-size: var(--t-lg); font-weight: 640;
}
/* The section's icon sits in a soft tile of the accent. It is the one piece of
   colour on a settings page that is not a control, and it is what lets
   somebody scrolling find "Password" by shape before they have read a word. */
.setting-intro h2 .icon {
  box-sizing: content-box;
  padding: 8px;
  border-radius: var(--r-md);
  background: var(--accent-soft);
  color: var(--accent);
}
.setting-intro p { color: var(--text-2); font-size: var(--t-sm); max-width: 42ch; }

/* The danger zone is the same shape in the critical colour, and it is the
   last thing on the page, where nobody arrives at it by accident. */
.setting-group.is-danger .setting-intro h2 .icon { background: var(--critical-soft); color: var(--critical); }
.setting-group.is-danger .panel { border-color: color-mix(in srgb, var(--critical) 35%, var(--border)); }

.fields { display: grid; gap: 24px; }
.fields > .errorlist { margin-bottom: -4px; }
.panel-body.fields { padding: 22px 22px 24px; }
.setting-group .panel-foot { padding: 14px 22px; background: color-mix(in srgb, var(--surface-2) 55%, transparent); border-radius: 0 0 var(--r-lg) var(--r-lg); }

.input.w-lg, .select.w-lg, .affix.w-lg, .readonly.w-lg { max-width: 460px; }
.select.w-md, .affix.w-md { max-width: 280px; }

/* "(optional)", beside a label rather than inside the placeholder, where it
   would vanish the moment somebody started typing. */
.optional { color: var(--text-3); font-weight: 450; font-size: var(--t-xs); margin-left: 4px; }

/* A fixed prefix drawn as part of the box — the @ before a handle. The prefix
   is not typed and not submitted; it is there so that nobody types one. */
.affix { display: flex; align-items: stretch; width: 100%; }
.affix-pre {
  display: grid; place-items: center;
  min-width: 40px; padding: 0 10px;
  border: 1px solid var(--border); border-right: 0;
  border-radius: var(--r-sm) 0 0 var(--r-sm);
  background: var(--surface-3);
  color: var(--text-3);
  font-family: var(--mono);
}
.affix .input { border-radius: 0 var(--r-sm) var(--r-sm) 0; min-width: 0; }

/* The same, after the field: the .example.com a workspace's address is typed
   in front of. Allowed to shrink (clipped) rather than squeeze the field to
   nothing on a phone — the part being typed matters more. */
.affix-post {
  display: flex; align-items: center; flex: 0 1 auto;
  min-width: 0; max-width: 60%; padding: 0 10px;
  border: 1px solid var(--border); border-left: 0;
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  background: var(--surface-3);
  color: var(--text-3);
  font-family: var(--mono);
  white-space: nowrap; overflow: hidden;
}
.affix .input:has(+ .affix-post) { border-radius: var(--r-sm) 0 0 var(--r-sm); flex: 1 1 8ch; }

/* A value you can see but not change here. Drawn as a box so it lines up with
   the fields around it, dashed and locked so it is plainly not one of them. */
.readonly {
  display: flex; align-items: center; gap: 10px;
  min-height: var(--tap);
  padding: 9px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-sm);
  color: var(--text-2);
}
.readonly .icon { color: var(--text-3); }

/* Your picture, with what you can do to it beside it. */
.avatar-edit { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.avatar-edit-side { display: grid; gap: 8px; min-width: 0; }

/* A list of links, each a whole row — your projects, a client's contacts. */
.link-list { list-style: none; padding: 6px; margin: 0; display: grid; gap: 2px; }
.link-list a {
  display: flex; align-items: center; gap: 12px;
  min-height: var(--tap);
  padding: 8px 12px;
  border-radius: var(--r-md);
  color: var(--text-1);
}
.link-list a:hover { background: var(--surface-2); text-decoration: none; }
.link-list a > .icon { margin-left: auto; color: var(--text-3); flex: none; }
.link-list .dot { flex: none; }

/* A save bar that stays in reach on a long form: it sticks to the bottom of
   the window while the form is taller than it, and settles under the last
   section once the reader gets there. */
.savebar {
  position: sticky;
  bottom: 0;
  z-index: 15;
  display: flex; align-items: center; gap: 10px;
  margin: 8px -18px 0;
  padding: 12px 18px;
  border-top: 1px solid var(--border);
  background: color-mix(in srgb, var(--surface-1) 92%, transparent);
  backdrop-filter: blur(8px);
}
.savebar .spacer { margin-left: auto; }

/* An option that is a tickbox with a title and a sentence, drawn as a row you
   can hit anywhere on. Several stacked read as a list of switches. */
.options { display: grid; gap: 0; border: 1px solid var(--border); border-radius: var(--r-md); overflow: hidden; }
.options > .check {
  padding: 14px 16px;
  gap: 14px;
  background: var(--surface-1);
}
.options > .check + .check { border-top: 1px solid var(--border); }
.options > .check:hover { background: var(--surface-2); }
.options > .check input { margin-top: 3px; }

/* Which changes email whom: a row per kind, a tickbox per audience. The
   whole cell is the tickbox's label, so it is a target you can hit. */
.mail-matrix td { vertical-align: top; }
.mail-matrix th.switch, .mail-matrix td.switch { width: 104px; text-align: center; }
.mail-matrix td.switch { padding: 0; vertical-align: middle; }
.mail-matrix td.switch label { display: flex; align-items: center; justify-content: center; min-height: 56px; cursor: pointer; }
.mail-matrix td.switch input { width: 18px; height: 18px; accent-color: var(--accent-solid); }
.mail-matrix .what strong { display: block; color: var(--text-1); font-weight: 560; }
.mail-matrix .what span { display: block; margin-top: 2px; font-size: var(--t-sm); color: var(--text-3); }

/* A row that adds one thing to the list under it: the fields side by side,
   the button on their baseline, and the hint for all of them underneath. */
.add-row { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px; border-bottom: 1px solid var(--border); }
.add-row .field.grow { flex: 1 1 260px; }
.add-row .field:not(.grow) { flex: 0 1 160px; }
.add-row .hint.full { flex-basis: 100%; }
.add-row.flat { border-bottom: 0; padding: 0; }
.add-row .field:has(.input.color) { flex: none; }
.list-page { max-width: 1120px; }

/* Terms and what they mean, side by side. */
.legend { display: grid; gap: 6px; margin: 0; font-size: var(--t-sm); }
.legend > div { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; }
.legend dt { font-weight: 600; color: var(--text-1); }
.legend dd { margin: 0; color: var(--text-2); }

/* People as a list: face, name and address, what they are, and the one thing
   you can do to them — spelled out, not an unlabelled cross. */
.people-list { list-style: none; margin: 0; padding: 0; }
.people-list li {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 22px;
}
.people-list li + li { border-top: 1px solid var(--border); }
.people-list-who { display: grid; min-width: 0; flex: 1; }
.people-list form { display: flex; }

/* A list of things to tick, each a whole row you can hit anywhere on — the
   people in a group, the clients and projects it works on. */
.pick-list { display: grid; padding: 6px; gap: 2px; max-height: 520px; overflow-y: auto; }
.pick-row {
  display: flex; align-items: center; gap: 14px;
  min-height: 56px;
  padding: 8px 12px;
  border-radius: var(--r-md);
  cursor: pointer;
}
.pick-row:hover { background: var(--surface-2); }
.pick-row:has(input:checked) { background: var(--accent-soft); }
.pick-row > input { width: 18px; height: 18px; margin: 0; flex: none; accent-color: var(--accent-solid); }
.pick-row .nav-card-icon { width: 36px; height: 36px; }
.pick-row-text { display: grid; min-width: 0; flex: 1; }
.pick-row-text strong { font-weight: 580; }
.pick-row-text > span { font-size: var(--t-sm); color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pick-row .tag { flex: none; }
.pick-row .covered-note { display: none; }
.pick-row.is-covered .covered-note { display: inline-flex; }
.pick-row.is-covered:not(:has(input:checked)) { background: color-mix(in srgb, var(--accent-soft) 50%, transparent); }

/* The groups, as cards: who, and what they work on. */
.group-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 14px; max-width: 1400px; }
.group-card {
  display: grid; gap: 12px; align-content: start;
  padding: 18px 20px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  color: var(--text-1);
  transition: border-color .15s var(--ease), transform .15s var(--ease), box-shadow .15s var(--ease);
}
.group-card:hover { border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-2); text-decoration: none; }
.group-card-head { display: flex; align-items: center; gap: 12px; }
.group-card-head > .icon { margin-left: auto; color: var(--text-3); }
.group-card-title { display: grid; }
.group-card-title strong { font-size: var(--t-lg); font-weight: 620; }
.group-card-reach { display: grid; gap: 6px; padding-top: 12px; border-top: 1px solid var(--border); }
.faces { display: flex; }
.faces > .who { box-shadow: 0 0 0 2px var(--surface-1); }
.faces > * + * { margin-left: -6px; }
.faces-more { display: grid; place-items: center; min-width: 26px; height: 26px; padding: 0 6px; border-radius: 999px; background: var(--surface-3); color: var(--text-2); font-size: var(--t-xs); box-shadow: 0 0 0 2px var(--surface-1); }

/* Two short fields that belong on one line — a server and its port. The
   second takes only the room it needs. */
.field-pair { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
.field-pair > .field:first-child { flex: 1 1 260px; max-width: 460px; }
.field-pair > .field:last-child { flex: 0 0 auto; }

/* The log of mail read: what it became, from whom, and why. */
.mail-log { list-style: none; margin: 0; padding: 0; }
.mail-log li { display: flex; align-items: flex-start; gap: 14px; padding: 12px 20px; }
.mail-log li + li { border-top: 1px solid var(--border); }
.mail-log .tag { flex: none; min-width: 92px; justify-content: center; margin-top: 2px; }
.mail-log-text { display: grid; gap: 1px; min-width: 0; flex: 1; }
.mail-log li > a, .mail-log li > .nowrap { flex: none; margin-top: 3px; }

/* The SMTP conversation behind a test send. Scrolls sideways rather than
   wrapping, because a wrapped server reply reads as two replies. */
.transcript > summary { cursor: pointer; display: inline-flex; align-items: center; min-height: var(--tap-min); font-size: var(--t-sm); color: var(--text-2); }
.transcript > pre {
  margin: 8px 0 0;
  padding: 12px 14px;
  max-height: 360px;
  overflow: auto;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-sm);
  font: var(--t-xs)/1.55 var(--mono);
  white-space: pre;
}
.facts dd > .mono.small { flex-basis: 100%; overflow-wrap: anywhere; }

/* A card that is a way somewhere else, with what is there. */
.nav-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 12px; }
.nav-card {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  color: var(--text-1);
  transition: border-color .15s var(--ease), transform .15s var(--ease), box-shadow .15s var(--ease);
}
.nav-card:hover { border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-2); text-decoration: none; }
.nav-card > .icon { margin-left: auto; color: var(--text-3); flex: none; }
.nav-card-icon {
  display: grid; place-items: center; flex: none;
  width: 40px; height: 40px;
  border-radius: var(--r-md);
  background: var(--accent-soft); color: var(--accent);
}
.nav-card-text { display: grid; gap: 2px; min-width: 0; }
.nav-card-text strong { font-weight: 620; }
.nav-card-text > span:not(.dim) { font-size: var(--t-sm); color: var(--text-2); }

/* --- writing a new ticket ------------------------------------------------ */
.compose {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 340px;
  gap: 20px;
  align-items: start;
  max-width: 1240px;
}
.compose-side { display: grid; gap: 12px; position: sticky; top: calc(var(--top-h) + 16px); }
.compose-actions { display: grid; gap: 6px; }
.compose .textarea.tall { min-height: 220px; }
.input.input-lg { font-size: var(--t-lg); font-weight: 560; min-height: 48px; }

/* A choice of a few, all on show: priority. Each option is a radio laid
   under its own face, so it is a real radio to the keyboard and the form. */
.segmented { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 6px; }
.seg { position: relative; display: flex; cursor: pointer; }
.seg input { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
.seg > span {
  display: flex; align-items: center; gap: 8px; width: 100%;
  min-height: var(--tap);
  padding: 8px 12px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: var(--text-2);
  font-size: var(--t-sm); font-weight: 550;
  transition: border-color .14s var(--ease), background .14s var(--ease);
}
.seg:hover > span { border-color: var(--border-strong); color: var(--text-1); }
.seg input:checked + span { border-color: var(--accent); background: var(--accent-soft); color: var(--text-1); box-shadow: inset 0 0 0 1px var(--accent); }
.seg input:focus-visible + span { outline: 3px solid var(--accent); outline-offset: 2px; }

/* Attaching: a target you can see and hit, not the browser's grey button and
   "No file chosen". With script the files picked are listed under it; with
   none, the browser's own input is left showing and does that itself. */
.dropzone {
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px;
  border: 1.5px dashed var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  cursor: pointer;
  transition: border-color .15s var(--ease), background .15s var(--ease);
}
.dropzone:hover, .dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); }
.dropzone-icon {
  display: grid; place-items: center; flex: none;
  width: 40px; height: 40px; border-radius: 50%;
  background: var(--surface-3); color: var(--text-2);
}
.dropzone-text { display: grid; gap: 1px; justify-items: start; text-align: left; }
.dropzone-text span { font-size: var(--t-sm); color: var(--text-3); }
:root.has-js .dropzone-input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
:root:not(.has-js) .dropzone { flex-wrap: wrap; }
.file-chosen { list-style: none; margin: 0; padding: 0; display: grid; gap: 4px; }
.file-chosen li { display: flex; align-items: center; gap: 8px; font-size: var(--t-sm); color: var(--text-2); }
.file-chosen .icon { color: var(--text-3); }

/* --- choice cards ----------------------------------------------------------
   A set of radios drawn as pictures — the looks, the three brightnesses, a
   role. The radio is laid over the whole card and not drawn, so the card is
   the thing you hit, the arrow keys still move between them, and the form
   submits the value like any other field.
   -------------------------------------------------------------------------- */
.choices {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(168px, 1fr));
  gap: 12px;
}
.choices.themes { grid-template-columns: repeat(auto-fill, minmax(168px, 1fr)); max-width: 640px; }

.choice {
  position: relative;
  display: grid;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-2);
  overflow: hidden;
  cursor: pointer;
  transition: border-color .15s var(--ease), box-shadow .15s var(--ease), transform .15s var(--ease);
}
.choice:hover { border-color: var(--border-strong); transform: translateY(-1px); box-shadow: var(--shadow-2); }
.choice-input {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  margin: 0;
  opacity: 0;
  cursor: pointer;
  z-index: 1;
}
.choice:has(.choice-input:checked) {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), var(--shadow-1);
}
.choice:has(.choice-input:focus-visible) { outline: 3px solid var(--accent); outline-offset: 2px; }
.choice:has(.choice-input:disabled) { opacity: .5; cursor: not-allowed; transform: none; box-shadow: none; }
.choice:has(.choice-input:disabled) .choice-input { cursor: not-allowed; }

.choice-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 10px 12px;
  border-top: 1px solid var(--border);
  background: var(--surface-1);
  min-height: 52px;
}
.choice-text { display: grid; min-width: 0; }
.choice-name {
  display: flex; align-items: center; gap: 7px;
  font-size: var(--t-sm); font-weight: 600; color: var(--text-1);
  line-height: 1.3;
}
.choice-name .icon { color: var(--text-3); }
.choice-meta { font-size: var(--t-xs); color: var(--text-3); line-height: 1.35; }
.choice-tick {
  display: grid; place-items: center;
  width: 22px; height: 22px;
  margin-left: auto;
  flex: none;
  border-radius: 50%;
  border: 1px solid var(--border-strong);
  color: transparent;
}
.choice:has(.choice-input:checked) .choice-tick {
  background: var(--accent-solid);
  border-color: var(--accent-solid);
  color: var(--accent-ink);
}
.choice:has(.choice-input:checked) .choice-name .icon { color: var(--accent); }

/* A card whose picture is words — a role. */
.choice-body { display: grid; gap: 4px; padding: 14px 16px 16px; padding-right: 44px; }
.choice-body .choice-name { font-size: var(--t-md); white-space: normal; }
.choice-body .choice-desc { font-size: var(--t-sm); color: var(--text-2); }
.choice-body + .choice-tick,
.choice > .choice-tick { position: absolute; top: 14px; right: 14px; }
.choices.roles { grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); }

/* --- a look, drawn small ---------------------------------------------------
   A bar, a rail, two lines of text and a button, in the palette the look's
   own stylesheet declares — see App\Services\SkinPreview. The colours arrive
   as --pd-* (dark) and --pl-* (light) on the element; which set is used
   follows the page, unless the picture is of one brightness on purpose.
   -------------------------------------------------------------------------- */
.look-art {
  --a-bg: var(--pd-bg); --a-s1: var(--pd-surface-1); --a-s2: var(--pd-surface-2);
  --a-border: var(--pd-border); --a-text: var(--pd-text-1); --a-text3: var(--pd-text-3);
  --a-solid: var(--pd-accent-solid); --a-accent: var(--pd-accent);

  display: grid;
  grid-template-columns: 28% 1fr;
  grid-template-rows: 14px 1fr;
  height: 92px;
  background: var(--a-bg);
  pointer-events: none;
}
:root[data-theme="light"] .look-art:not(.is-dark),
.look-art.is-light {
  --a-bg: var(--pl-bg, var(--pd-bg)); --a-s1: var(--pl-surface-1, var(--pd-surface-1)); --a-s2: var(--pl-surface-2, var(--pd-surface-2));
  --a-border: var(--pl-border, var(--pd-border)); --a-text: var(--pl-text-1, var(--pd-text-1)); --a-text3: var(--pl-text-3, var(--pd-text-3));
  --a-solid: var(--pl-accent-solid, var(--pd-accent-solid)); --a-accent: var(--pl-accent, var(--pd-accent));
}
.la-top {
  grid-column: 1 / -1;
  display: flex; align-items: center;
  padding: 0 7px;
  background: var(--a-s1);
  border-bottom: 1px solid var(--a-border);
}
.la-dot { width: 6px; height: 6px; border-radius: 2px; background: var(--a-solid); }
.la-rail {
  display: grid; align-content: start; gap: 5px;
  padding: 8px 7px;
  background: var(--a-s1);
  border-right: 1px solid var(--a-border);
}
.la-nav { height: 5px; border-radius: 3px; background: var(--a-text3); opacity: .55; }
.la-nav.is-on { background: var(--a-accent); opacity: 1; }
.la-main {
  display: grid; align-content: start; gap: 6px;
  margin: 8px;
  padding: 8px;
  border: 1px solid var(--a-border);
  border-radius: 4px;
  background: var(--a-s2);
}
.la-line { height: 5px; width: 85%; border-radius: 3px; background: var(--a-text); opacity: .85; }
.la-line.is-short { width: 55%; background: var(--a-text3); }
.la-btn { width: 42%; height: 10px; margin-top: 2px; border-radius: 3px; background: var(--a-solid); }

/* The brightnesses are pictures of the look being worn, in each. "Match my
   computer" is both at once, split on the diagonal, because that is what it
   means: one or the other, depending on the machine. */
.theme-art { position: relative; display: grid; height: 92px; }
.theme-art > .look-art { grid-area: 1 / 1; }
.theme-art.is-system > .look-art.is-light { clip-path: polygon(62% 0, 100% 0, 100% 100%, 38% 100%); }

/* --- figures ---------------------------------------------------------------
   The numbers at the top of the overview.

   These were four bordered tiles with a 25px figure in each. The information
   was worth keeping and the furniture was not: four cards is the look of a
   reporting dashboard, and every one of the four is a link to a list that the
   rail two inches to the left already offers. So: one strip, hairline
   separators, no boxes, and the number leads because the number is the answer.
   -------------------------------------------------------------------------- */
.figures {
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  background: var(--border);
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.figure {
  flex: 1 1 170px;
  display: grid;
  gap: 2px;
  padding: 16px 20px;
  background: var(--surface-1);
  color: inherit;
  min-width: 0;
}
a.figure:hover { background: var(--surface-2); text-decoration: none; }
.figure-n {
  font-size: var(--t-2xl); font-weight: 640; letter-spacing: -.035em; line-height: 1.15;
  font-variant-numeric: tabular-nums;
}
.figure-k { font-size: var(--t-xs); color: var(--text-2); }
.figure-note { font-size: var(--t-xs); color: var(--text-3); }
.figure[data-tone="critical"] .figure-n { color: var(--critical); }
.figure[data-tone="warning"] .figure-n { color: var(--warning); }
.figure[data-tone="good"] .figure-n { color: var(--good); }

/* --- lanes ------------------------------------------------------------------
   A short list of tickets under a heading. What used to be a bordered card
   with a header bar and a table in it; now a heading, a rule, and rows drawn
   as the queue draws them — so that a ticket looks like a ticket wherever in
   the application it turns up.
   -------------------------------------------------------------------------- */
.lane { min-width: 0; }
.lane-head {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 0 2px 10px;
  border-bottom: 1px solid var(--border);
}
.lane-head .icon { color: var(--text-3); flex: none; }
.lane-head h2 { font-size: var(--t-sm); font-weight: 620; letter-spacing: -.005em; }
.lane-head .spacer { margin-left: auto; }
.lane-n {
  font-size: var(--t-xs); font-weight: 600;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.lane-more { display: inline-flex; align-items: center; gap: 3px; min-height: var(--tap-sm); padding: 0 6px; font-size: var(--t-sm); color: var(--text-3); }
.lane-more:hover { color: var(--text-1); text-decoration: none; }
.lane-blank { padding: 14px 2px; font-size: var(--t-sm); color: var(--text-3); }
.lane-rows { display: flex; flex-direction: column; }

/* One row of a lane. The queue's line, without the controls. */
.mini {
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: 46px;
  padding: 0 10px 0 12px;
  border-bottom: 1px solid var(--border);
  color: var(--text-1);
  box-shadow: inset 3px 0 0 var(--tone, transparent);
}
.lane-rows .mini:last-child { border-bottom: 0; }
.mini:hover { background: var(--surface-2); text-decoration: none; }
.mini-key {
  flex: none;
  font-family: var(--mono);
  font-size: var(--t-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.mini-title {
  min-width: 0;
  font-size: var(--t-md);
  font-weight: 500;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.mini .tally { flex: none; }

/* --- tags: status, label, any short piece of state -------------------------
   A tag carries its colour in a custom property rather than in a class,
   because a status's colour is a row in the database and not a thing this
   file can know. The classes below are for the fixed roles — the ones the
   application itself reasons about.
   -------------------------------------------------------------------------- */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
  color: var(--text-2);
  font-size: var(--t-xs); font-weight: 550;
  line-height: 1.6;
  white-space: nowrap;
  max-width: 100%;
}
.tag > span { overflow: hidden; text-overflow: ellipsis; }
a.tag:hover { text-decoration: none; border-color: var(--border-strong); }

/* The status tag. --tone is set inline from the status row's colour; the
   colour-mix is what turns one stored hex into a readable tint and a readable
   border without the database having to store three. */
.tag.is-status {
  color: var(--tone, var(--text-2));
  border-color: color-mix(in srgb, var(--tone, var(--border)) 38%, transparent);
  background: color-mix(in srgb, var(--tone, var(--surface-2)) 14%, var(--surface-1));
}
.tag.is-status::before {
  content: "";
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--tone, currentColor);
  flex: none;
}

.tag.is-good     { color: var(--good);     border-color: color-mix(in srgb, var(--good) 38%, transparent);     background: var(--good-soft); }
.tag.is-warning  { color: var(--warning);  border-color: color-mix(in srgb, var(--warning) 40%, transparent);  background: var(--warning-soft); }
.tag.is-critical { color: var(--critical); border-color: color-mix(in srgb, var(--critical) 40%, transparent); background: var(--critical-soft); }
.tag.is-info     { color: var(--info);     border-color: color-mix(in srgb, var(--info) 38%, transparent);     background: var(--info-soft); }
.tag.is-accent   { color: var(--accent);   border-color: var(--accent-line); background: var(--accent-soft); }

/* A label on a ticket. Squarer than a status so the two are distinguishable
   at a glance in a row that carries both. */
.tag.is-label {
  border-radius: var(--r-sm);
  padding: 2px 8px;
  font-size: var(--t-xs);
  font-weight: 500;
  color: var(--tone, var(--text-2));
  border-color: color-mix(in srgb, var(--tone, var(--border)) 34%, transparent);
  background: color-mix(in srgb, var(--tone, var(--surface-2)) 12%, var(--surface-1));
}

/* --- priority -------------------------------------------------------------
   Four bars, of which N are filled. Ordered data drawn as an ordered shape,
   so that "more urgent" is visible without having learnt what amber means —
   including to a reader who cannot tell amber from green, and on a printed
   page. Colour is carried as reinforcement on the top two steps only:
   colouring the majority is how a list stops meaning anything.
   -------------------------------------------------------------------------- */
.prio {
  display: inline-flex;
  align-items: flex-end;
  gap: 2px;
  height: 12px;
  vertical-align: -1px;
}
.prio i {
  display: block;
  width: 4px;
  border-radius: 1px;
  background: var(--border-strong);
}
.prio i:nth-child(1) { height: 7px; }
.prio i:nth-child(2) { height: 6.5px; }
.prio i:nth-child(3) { height: 9px; }
.prio i:nth-child(4) { height: 12px; }
.prio i.on { background: var(--text-2); }
.prio[data-tone="warning"] i.on  { background: var(--warning); }
.prio[data-tone="critical"] i.on { background: var(--critical); }

.prio-cell { display: inline-flex; align-items: center; gap: 6px; }
.prio-cell .name { font-size: var(--t-sm); color: var(--text-2); }

/* --- avatars --------------------------------------------------------------
   Twelve tints, picked by the account's id so that renaming somebody does not
   change the colour their colleagues have learnt to recognise. Initials are
   the fallback, not a placeholder to be replaced later: most accounts in most
   installations never upload a picture, so the initials ARE the design.
   -------------------------------------------------------------------------- */
.who {
  display: grid;
  place-items: center;
  width: 24px; height: 24px;
  flex: none;
  border-radius: 50%;
  background: var(--who-bg, var(--surface-3));
  color: var(--who-fg, var(--text-2));
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .01em;
  text-transform: uppercase;
  overflow: hidden;
  user-select: none;
}
.who img { width: 100%; height: 100%; object-fit: cover; }
.who.sm { width: 26px; height: 26px; font-size: var(--t-xs); }
.who.lg { width: 40px; height: 40px; font-size: var(--t-md); }
.who.xl { width: 72px; height: 72px; font-size: var(--t-2xl); }
.who.is-empty { background: transparent; box-shadow: inset 0 0 0 1px var(--border); color: var(--text-3); }

/* The twelve. Each is a tinted disc with its own hue's text on it, both mixed
   from ONE stored value, so adding a tint is one line rather than three.

   The mix ratios are not taste. Initials are the only thing identifying an
   account wherever a name is not printed beside the disc, so they are held to
   4.5:1 against the disc they sit on — and the ratios below are the ones at
   which all twelve clear it, in both modes. tools/contrast.py checks them.
   On a light background the tint has to be darkened by half to get there;
   leaving it at full strength gives a pastel disc with pastel letters on it,
   which measures at 2.5:1 and reads as an empty circle. */
.who[data-tint] {
  background: color-mix(in srgb, var(--tint) 16%, var(--surface-1));
  color: var(--tint);
}
:root[data-theme="light"] .who[data-tint] {
  background: color-mix(in srgb, var(--tint) 16%, var(--surface-1));
  color: color-mix(in srgb, var(--tint) 50%, #000);
}
.who[data-tint="0"]  { --tint: #9687fa; }
.who[data-tint="1"]  { --tint: #3ba9f0; }
.who[data-tint="2"]  { --tint: #2dd4bf; }
.who[data-tint="3"]  { --tint: #2ec46a; }
.who[data-tint="4"]  { --tint: #a3d63b; }
.who[data-tint="5"]  { --tint: #e2c231; }
.who[data-tint="6"]  { --tint: #f59e0b; }
.who[data-tint="7"]  { --tint: #f97b46; }
.who[data-tint="8"]  { --tint: #f4676b; }
.who[data-tint="9"]  { --tint: #f472b6; }
.who[data-tint="10"] { --tint: #c084fc; }
.who[data-tint="11"] { --tint: #7c8cf8; }

.who-line { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.who-line .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the queue -------------------------------------------------------------
   The ticket list, which is the page rather than a table on it.

   WHAT CHANGED AND WHY
   This was a <table class="grid"> of seven columns inside a rounded panel,
   under a row of four stat tiles. That is how you draw a report — something
   read once, top to bottom, to find out a number. A queue is not read that
   way. It is WORKED: scanned at speed for "what is this, whose is it, how bad
   is it, is it late", and then changed on the spot. So:

     - no card around it. The list runs to both edges of the page, because a
       16px gutter either side of a list is 32px not spent on titles.
     - no column headings. A row is one line, and the sort moved to two named
       controls in the page head that say what they do — which a heading you
       have to know to click never did.
     - one line per ticket at var(--row-h), so a screenful is twenty-five rows
       rather than eleven.
     - the group heading sticks. Scrolling through "In progress" you can always
       see that you are in it.

   The left edge of every row is a three-pixel band in the status's own colour,
   which is what makes a screen of forty tickets scannable by state before a
   single word has been read.
   -------------------------------------------------------------------------- */
.queue { display: flex; flex-direction: column; }
.queue-blank { padding: 40px 18px; }

/* --- a group --------------------------------------------------------------- */
.qgroup { border-bottom: 1px solid var(--border); }
.qgroup:last-child { border-bottom: 0; }

.qgroup-head {
  position: sticky;
  /* Under the top bar and the facet bar, which are the two that stay. */
  top: calc(var(--top-h) + var(--facets-h));
  z-index: 12;
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--tap);
  padding: 0 18px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  cursor: pointer;
  list-style: none;
  user-select: none;
}
.qgroup-head::-webkit-details-marker { display: none; }
.qgroup-head::marker { content: ""; }
.qgroup-head:hover { background: var(--surface-3); }
.qgroup-head .icon { color: var(--text-3); transition: transform .15s var(--ease); }
/* Closed: the chevron points at what it would open. */
.qgroup:not([open]) .qgroup-head .icon { transform: rotate(-90deg); }

.qgroup-name {
  font-size: var(--t-md); font-weight: 640; letter-spacing: -.005em;
  color: var(--text-1);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.qgroup-count {
  font-size: var(--t-xs); font-weight: 600;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}

/* --- a row ----------------------------------------------------------------- */
.qrow {
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--row-h);
  padding: 0 18px;
  border-bottom: 1px solid var(--border);
  /* The status band. An inset shadow rather than a border-left so that it does
     not add to the row's width and shift every row that has one by 3px. */
  box-shadow: inset 3px 0 0 var(--tone, transparent);
  transition: background .1s var(--ease);
}
.qgroup-rows .qrow:last-child { border-bottom: 0; }
.qrow:hover { background: var(--surface-2); }

/* Where the keyboard is. A class and not :focus-within, because the focusable
   thing is the title link inside and the ROW is what should look current. */
.qrow.is-cursor { background: var(--surface-2); }
.qrow.is-cursor::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  box-shadow: inset 0 0 0 1px var(--accent);
}
.qrow.is-ticked { background: var(--accent-soft); }

/* j/k focuses the title link so that Enter and a screen reader land on the
   same thing the highlight points at — which would otherwise draw the link's
   focus ring INSIDE a row that is already ringed, two indicators for one
   position. The row's ring is the indicator while the row is the cursor; the
   link keeps its own everywhere else, including on Tab. */
.qrow.is-cursor .qtitle:focus-visible { outline: none; }

/* The tickbox, and the target around it.
   It used to be a 13px box that was invisible until you hovered the row. Two
   separate mistakes: 13px is half the 24px minimum, and a control you cannot
   see is a feature nobody finds. It is now always visible, 18px, and sits in a
   40px-wide label so the thing you have to hit is the size of a fingertip
   rather than the size of the glyph. */
.qtick {
  display: grid;
  place-items: center;
  flex: none;
  width: var(--tap);
  height: var(--tap);
  margin-left: -10px;
  border-radius: var(--r-sm);
  cursor: pointer;
}
.qtick:hover { background: var(--surface-3); }
.qtick input {
  width: 17px; height: 17px;
  accent-color: var(--accent-solid);
  cursor: pointer;
  margin: 0;
  /* Visible always — hiding it until hover was the original mistake — but
     quiet until it is relevant. This is a long way from opacity: 0: the box
     is on screen, it is 17px, and its 40px label is hittable. */
  opacity: .55;
  transition: opacity .12s var(--ease);
}
.qrow:hover .qtick input,
.qrow.is-cursor .qtick input,
.qtick input:checked,
.qtick input:focus-visible { opacity: 1; }

/* The three links on a row STRETCH to the row's full height.

   They were 19-23px tall inside a 52px row, which meant a person aiming at a
   ticket had a thin band to hit with two dead strips above and below it. The
   row is the target; these just have to fill it. */
.qkey {
  display: flex;
  align-items: center;
  align-self: stretch;
  flex: none;
  width: 9ch;
  font-family: var(--mono);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  white-space: nowrap;
  overflow: hidden;
}
a.qkey:hover { color: var(--accent); text-decoration: none; }

.qtitle {
  display: flex;
  align-items: center;
  align-self: stretch;
  min-width: 0;
  color: var(--text-1);
  font-size: var(--t-md);
  font-weight: 500;
}
.qtitle:hover { text-decoration: none; }
.qtitle:hover .qtext { text-decoration: underline; }
.qtext { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.qlabels { display: flex; gap: 4px; flex: none; }
/* The gap is what pushes everything after it to the right edge. A named
   element rather than margin-left:auto on the next thing, because which
   element IS next depends on what the row happens to carry. */
.qgap { flex: 1 1 auto; min-width: 8px; }

.qproject {
  display: flex; align-items: center; gap: 6px;
  align-self: stretch;
  flex: none;
  padding: 0 4px;
  font-size: var(--t-xs);
  font-family: var(--mono);
  color: var(--text-3);
}
.qproject:hover { color: var(--text-1); text-decoration: none; }

.qcounts {
  display: flex; align-items: center; gap: 8px;
  flex: none;
  font-size: var(--t-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.qcounts span { display: inline-flex; align-items: center; gap: 3px; }

.qdue { flex: none; min-width: 52px; text-align: right; }
.qage {
  flex: none;
  width: 44px;
  text-align: right;
  font-size: var(--t-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.qstatus, .qprio, .qwho { flex: none; display: flex; }

/* The priority label on a row. Fixed width so the four possible words do not
   make four different column positions down the page — the whole point of the
   ramp beside it is that a column can be scanned. */
.qrow .pick-prio .prio-cell { gap: 8px; }
.qrow .pick-prio .name {
  width: 6ch;
  font-size: var(--t-sm);
  color: var(--text-2);
  white-space: nowrap;
}

/* Grouped by status the word repeats down the group, and that repetition is
   worth paying for: the pill is the only thing on the row that says what the
   control under your cursor will change. A bare dot saved twenty words and
   cost the row its one label. */

/* --- a picker on a row ------------------------------------------------------
   A <details> holding a one-field form of buttons. It opens with no script;
   queue.js adds closing on Escape, arrow keys, and not leaving six of them
   hanging open at once.
   -------------------------------------------------------------------------- */
/* A picker on a row.

   These were 16px-tall triggers with no border, no background and no chevron —
   they looked like output, not like something you could press, and they were
   the smallest targets in the interface. Now each one is a real control: at
   least --tap-sm tall, a hairline that appears on hover, and a chevron that
   says "this opens". */
.pick { position: relative; flex: none; }
.pick > summary {
  display: flex;
  align-items: center;
  gap: 5px;
  min-height: var(--tap-sm);
  padding: 2px 5px 2px 6px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  cursor: pointer;
  list-style: none;
}
.pick > summary::-webkit-details-marker { display: none; }
.pick > summary::marker { content: ""; }

/* The chevron. Drawn here rather than put in every picker's markup, because
   it is a statement about the control and not about the ticket. */
.pick > summary::after {
  content: "";
  width: 12px; height: 12px;
  flex: none;
  background: var(--caret) no-repeat center / 12px 12px;
  opacity: .55;
}
/* The chevron is not on screen seventy-five times at once.

   Three pickers per row across twenty-five rows is seventy-five chevrons, and
   a hint repeated that many times stops being a hint and becomes texture. It
   appears when the row is under the pointer, when the control has focus, and
   while the menu is open — which is every moment at which it is telling
   somebody something they did not already know.

   Except on a touch screen. There is no hover there, so there is no "moment
   of interaction" to reveal it at, and the affordance has to be permanent —
   see the pointer: coarse block below. */
.pick > summary::after {
  opacity: 0;
  transition: opacity .12s var(--ease);
}
.qrow:hover .pick > summary::after,
.qrow.is-cursor .pick > summary::after,
.pick > summary:hover::after,
.pick > summary:focus-visible::after,
.pick[open] > summary::after { opacity: .75; }

.pick > summary:hover {
  background: var(--surface-3);
  border-color: var(--border-strong);
}
.pick > summary:hover::after { opacity: 1; }
.pick[open] > summary {
  background: var(--surface-3);
  border-color: var(--accent);
}
.pick[open] > summary::after { opacity: 1; transform: rotate(180deg); }

.pick-menu {
  position: absolute;
  top: calc(100% + 4px);
  right: 0;
  z-index: 25;
  min-width: 190px;
  max-height: 320px;
  overflow-y: auto;
  padding: 5px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}
/* A menu hangs from the edge its trigger is nearest. The default is right —
   the priority and assignee pickers live at the right end of the row, and a
   menu opening rightward from there goes off the screen. The status picker is
   at the LEFT end, so it hangs the other way; anchoring it right sent it back
   across the row and out over the rail. */
.pick-status .pick-menu { left: 0; right: auto; }

/* On the bulk bar the menu opens upwards, because the bar is at the bottom of
   the window and a menu below it would be off the screen. */
.pick-up .pick-menu { top: auto; bottom: calc(100% + 6px); }
.pick-up .pick-menu { left: 0; right: auto; }

.pick-label {
  padding: 4px 8px 6px;
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.pick-opt {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  min-height: var(--tap);
  padding: 7px 10px;
  border: 0;
  border-radius: var(--r-sm);
  background: none;
  color: var(--text-2);
  font-size: var(--t-md);
  text-align: left;
  cursor: pointer;
}
.pick-opt:hover, .pick-opt:focus-visible { background: var(--surface-2); color: var(--text-1); }
.pick-opt[aria-current] { color: var(--text-1); font-weight: 560; }
.pick-opt .icon { margin-left: auto; color: var(--accent); flex: none; }
.pick-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the bulk bar -----------------------------------------------------------
   One change applied to everything ticked. It floats over the list rather than
   pushing it, because appearing must not move the row the reader is aiming at.

   Without script it simply sits there permanently, which is honest: the
   tickboxes work, the menus work, and the bar is how you use them. With
   script it stays out of the way until something is actually ticked.
   -------------------------------------------------------------------------- */
.bulkbar {
  position: fixed;
  bottom: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 28;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  background: var(--surface-1);
  box-shadow: var(--shadow-3);
}
:root.has-js .bulkbar {
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(8px);
  transition: opacity .16s var(--ease), transform .16s var(--ease), visibility .16s;
}
:root.has-js .bulkbar.is-live {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%) translateY(0);
}
.bulk-count {
  padding: 0 6px 0 8px;
  font-size: var(--t-sm); font-weight: 560;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- group and sort ---------------------------------------------------------
   Where the column headings were. Two named controls in the page head.
   -------------------------------------------------------------------------- */
.arrange { display: flex; align-items: center; gap: 6px; }

/* --- the command palette ----------------------------------------------------
   The entries are server-rendered links; the script filters them. What it
   cannot render ahead of time is the ticket search, which is why that is a
   group of its own at the top rather than mixed into the fixed list.
   -------------------------------------------------------------------------- */
.palette {
  width: min(560px, calc(100vw - 32px));
  max-height: min(560px, calc(100vh - 96px));
  margin: 12vh auto auto;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  color: var(--text-1);
  box-shadow: var(--shadow-3);
  overflow: hidden;
}
/* The column layout belongs to the OPEN state and only to it.
   A closed <dialog> is hidden by `display: none` in the user-agent
   stylesheet, so declaring `display: flex` on .palette itself overrides that
   and leaves the whole palette sitting in the page — invisible above the fold,
   and adding several hundred pixels to the height of every single screen. */
.palette[open] { display: flex; flex-direction: column; }
.palette::backdrop { background: rgb(0 0 0 / 55%); backdrop-filter: blur(2px); }

.palette-head {
  display: flex;
  align-items: center;
  gap: 11px;
  padding: 12px 14px;
  border-bottom: 1px solid var(--border);
  flex: none;
}
.palette-head .icon { color: var(--text-3); flex: none; }
.palette-input {
  flex: 1;
  min-width: 0;
  height: var(--tap);
  border: 0;
  background: none;
  font-size: var(--t-xl);
  color: var(--text-1);
  outline: 0;
}
.palette-input::placeholder { color: var(--text-3); }

.palette-body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 6px;
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}
.palette-group + .palette-group { margin-top: 4px; }
.palette-label {
  padding: 7px 9px 4px;
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--text-3);
}

.palette-item {
  display: flex;
  align-items: center;
  gap: 11px;
  min-height: var(--tap);
  padding: 9px 11px;
  border-radius: var(--r-md);
  color: var(--text-2);
  font-size: var(--t-md);
}
.palette-item:hover { text-decoration: none; }
.palette-item .icon { color: var(--text-3); flex: none; }
.palette-item > span:not(.palette-note):not(.palette-key):not(.dot) {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The highlight is a class the arrow keys move, not :hover — the mouse and the
   keyboard must not be able to point at two different entries at once. */
.palette-item.is-active {
  background: var(--accent-soft);
  box-shadow: inset 0 0 0 1px var(--accent-line);
  color: var(--text-1);
}
.palette-item.is-active .icon { color: var(--accent); }
.palette-key {
  flex: none;
  min-width: 7ch;
  font-size: var(--t-xs);
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.palette-note { margin-left: auto; flex: none; font-size: var(--t-xs); color: var(--text-3); }
.palette-blank { padding: 26px 12px; text-align: center; color: var(--text-3); font-size: var(--t-md); }

.palette-foot {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px 12px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--t-xs);
  color: var(--text-3);
  flex: none;
}
.palette-foot .spacer { margin-left: auto; }
.palette-foot span { display: inline-flex; align-items: center; gap: 4px; }
.palette-help {
  border: 0; background: none; cursor: pointer;
  color: var(--text-3); font-size: var(--t-xs);
  display: inline-flex; align-items: center; gap: 4px;
}
.palette-help:hover { color: var(--text-1); }

/* --- the shortcut card ----------------------------------------------------- */
.keys { display: grid; grid-template-columns: 1fr 1fr; gap: 4px 20px; }
.key-row { display: flex; align-items: center; gap: 10px; padding: 3px 0; }
.key-combo { display: inline-flex; gap: 3px; flex: none; min-width: 58px; }
.key-what { font-size: var(--t-sm); color: var(--text-2); }

/* --- the ticket table -----------------------------------------------------
   Not a generic data table. The left edge of every row is a three-pixel band
   in the status's own colour, which is what makes a screen of forty tickets
   scannable by state before a single word has been read.
   -------------------------------------------------------------------------- */
/* NOT overflow-x: auto at this width, and that is load-bearing.
   `overflow-x: auto` computes overflow-y to auto as well, which makes this
   element a scroll container — and a sticky <th> inside one is positioned
   against THAT container's scrollport rather than the viewport's. The header
   then parks 104px below the top of the table instead of under the two bars,
   with the first row of data stranded above it.
   The table shrinks to fit instead, and drops columns below 620px. */
.grid-wrap { min-width: 0; }

table.grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  font-size: var(--t-md);
}
table.grid th {
  position: sticky;
  /* Under the one bar that is always there. The offset is a token because a
     skin that grows it has to be able to move this with it in one line. */
  top: var(--top-h);
  z-index: 10;
  padding: 8px 12px;
  background: var(--surface-2);
  border-bottom: 1px solid var(--border);
  text-align: left;
  font-size: var(--t-xs); font-weight: 620; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
table.grid th.num, table.grid td.num { text-align: right; }
table.grid th a { color: inherit; display: inline-flex; align-items: center; gap: 4px; }
table.grid th a:hover { color: var(--text-1); text-decoration: none; }
table.grid th[aria-sort] a { color: var(--text-1); }
table.grid th .icon { opacity: .75; }

table.grid td {
  padding: 9px 12px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
table.grid tbody tr { transition: background .12s var(--ease); }
table.grid tbody tr:hover { background: var(--surface-2); }
table.grid tbody tr:last-child td { border-bottom: 0; }

/* The status band. A box-shadow rather than a border-left so it does not add
   to the row's width and shift every column by three pixels. */
table.grid tbody tr td:first-child {
  box-shadow: inset 3px 0 0 var(--tone, transparent);
  padding-left: 14px;
}

/* --- the statuses list ------------------------------------------------------
   Every status is editable in place, and every one is its own form. A form
   cannot span table cells, so this is a grid with the same columns in every
   row, the heading row included. On a phone it folds to three lines. */
.status-list { display: grid; }
.status-row {
  display: grid;
  /* Every column but the name is a fixed width. Each row is its own grid,
     so an `auto` column would be sized row by row and the heading would
     drift out of line with the controls under it. */
  grid-template-columns: var(--tap) minmax(120px, 1fr) 132px 68px 60px 80px 88px;
  gap: 8px;
  align-items: center;
  padding: 10px 14px 10px 17px;
  border-top: 1px solid var(--border);
  box-shadow: inset 3px 0 0 var(--tone, transparent);
}
.status-head {
  padding-block: 8px;
  background: var(--surface-2);
  font-size: var(--t-xs); font-weight: 620; letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-3);
  white-space: nowrap;
}
.status-row .num { text-align: right; }
.status-row .check { min-height: var(--tap); align-items: center; padding: 0; }
.status-row .check input { margin-top: 0; }
/* The long label is for the phone, where there is no heading above it. */
.status-row .check span { font-size: var(--t-sm); }
.status-count { color: var(--text-2); white-space: nowrap; }
.status-actions { display: flex; gap: 4px; align-items: center; justify-content: flex-end; }

/* A name per language (partials/name-fields): the inputs stacked, each with
   its language code drawn in front. One language is one input and the stack
   is invisible. */
.names { display: grid; gap: 6px; min-width: 0; }
.names .affix-pre { min-width: 40px; font-size: var(--t-xs); letter-spacing: .04em; }

/* The labels list edits in place: colour and names in the first cell, the
   row's Save and Delete in the last. */
.label-edit { display: flex; gap: 8px; align-items: flex-start; min-width: 0; }
.label-edit .names { flex: 1 1 220px; }
.btn-row.nowrap { flex-wrap: nowrap; justify-content: flex-end; }

@media (min-width: 621px) {
  .status-default span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
}

@media (max-width: 620px) {
  .status-head { display: none; }
  .status-row {
    grid-template-columns: var(--tap) minmax(0, 1fr) minmax(0, 1fr) 76px;
    grid-template-areas:
      "colour name name name"
      "cat cat cat order"
      "default default default default"
      "count count actions actions";
  }
  .status-row > .color { grid-area: colour; }
  .status-row > .names { grid-area: name; }
  .status-row > .select { grid-area: cat; }
  .status-row > [name="position"] { grid-area: order; }
  .status-row > .status-default { grid-area: default; }
  .status-row > .status-count { grid-area: count; text-align: left; }
  .status-row > .status-actions { grid-area: actions; }
}

/* The key: ACME-142. Mono, tabular, and the width of the column is the width
   of the longest key rather than of the heading. */
.tkey {
  font-family: var(--mono);
  font-size: var(--t-sm);
  font-variant-numeric: tabular-nums;
  color: var(--text-2);
  white-space: nowrap;
}
a.tkey:hover { color: var(--accent); text-decoration: none; }

.tsubject { display: grid; gap: 3px; min-width: 240px; }
.tsubject .title {
  color: var(--text-1); font-weight: 520;
  display: inline-flex; align-items: center; gap: 7px;
}
.tsubject a.title:hover { color: var(--accent); text-decoration: none; }
.tsubject .meta {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  font-size: var(--t-xs); color: var(--text-3);
}
.tsubject .meta .icon { opacity: .8; }
.tsubject .tags { display: inline-flex; gap: 4px; flex-wrap: wrap; }

/* The due date, which is the one cell that changes colour on its own. */
.due { font-size: var(--t-sm); color: var(--text-2); white-space: nowrap; }
.due.is-soon { color: var(--warning); font-weight: 560; }
.due.is-overdue { color: var(--critical); font-weight: 600; }

/* --- pagination ----------------------------------------------------------- */
.pager {
  display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
  padding: 14px 18px;
  border-top: 1px solid var(--border);
}
.pager .count { font-size: var(--t-sm); color: var(--text-3); margin-right: auto; font-variant-numeric: tabular-nums; }
.pager .size { display: flex; align-items: center; gap: 6px; }
.pager .size .select { width: auto; min-width: 90px; padding-block: 4px; font-size: var(--t-sm); }

/* --- empty states --------------------------------------------------------- */
.blank { display: grid; justify-items: center; gap: 7px; padding: 52px 24px; text-align: center; }
.blank .icon-wrap {
  display: grid; place-items: center;
  width: 42px; height: 42px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-3);
  margin-bottom: 3px;
}
.blank h3 { font-size: var(--t-md); font-weight: 600; }
.blank p { color: var(--text-2); max-width: 46ch; font-size: var(--t-md); }
.blank .btn-row { margin-top: 8px; }

/* --- the ticket page ------------------------------------------------------
   Two columns: the conversation, and a rail of properties that stays put
   while it is scrolled. This is the one screen that is not a list, and it is
   deliberately shaped nothing like one — the thread is the page, and
   everything that can be changed about the ticket is in one place at the
   right rather than scattered through a form.
   -------------------------------------------------------------------------- */
.ticket {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 300px;
  gap: 20px;
  align-items: start;
}

.ticket-head { display: grid; gap: 10px; margin-bottom: 4px; }
.ticket-head .line { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.ticket-head h1 {
  font-size: var(--t-xl); font-weight: 640; letter-spacing: -.025em; line-height: 1.3;
}
.ticket-head .tkey { font-size: var(--t-md); }

/* The opening description, and every reply, share this shape. */
.entry {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  overflow: hidden;
}
.entry-head {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px 14px;
  border-bottom: 1px solid var(--border);
  background: var(--surface-2);
  font-size: var(--t-sm);
}
.entry-head .who-line .name { font-weight: 570; color: var(--text-1); }
.entry-head .when { color: var(--text-3); font-size: var(--t-xs); }
.entry-head .spacer { margin-left: auto; }
.entry-body { padding: 14px; }

/* Editing a reply swaps its text for the form, in the same place and at the
   same width. Script toggles .is-editing; without it the edit button is a
   link to the form and :target does the same job. */
.entry-edit { display: none; gap: 10px; }
.entry.is-editing .entry-edit,
.entry-edit:target { display: grid; }
.entry.is-editing .entry-body > .prose,
.entry:has(.entry-edit:target) .entry-body > .prose { display: none; }
.entry-edit .textarea { min-height: 96px; }
.entry-edit .hint { margin-right: auto; }
.entry-edit:target { scroll-margin-top: calc(var(--top-h) + 64px); }
.entry.is-editing [data-edits] { background: var(--surface-3); }

/* The prose of a comment. Whitespace is preserved because people type lists
   and stack traces into these boxes and expect them to survive. */
.prose {
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  line-height: 1.6;
  color: var(--text-1);
  font-size: var(--t-md);
}
.prose a { text-decoration: underline; text-underline-offset: 2px; }
.prose code {
  font-family: var(--mono); font-size: var(--t-sm);
  padding: 1px 4px; border-radius: 4px;
  background: var(--surface-3);
}
/* A mention of you is not the same as a mention of somebody else, and the
   difference is worth a colour: it is how you find your name in a long
   thread. */
.mention {
  padding: 0 3px; border-radius: 4px;
  background: var(--surface-3); color: var(--text-1);
  font-weight: 560;
}
.mention.is-me { background: var(--accent-soft); color: var(--accent); }

/* An internal note. It has to be unmistakable — the entire risk of a shared
   thread is somebody typing into the wrong box — so it gets a warning tint,
   a striped edge and a label, and not one of those three on its own. */
.entry.is-internal { border-color: color-mix(in srgb, var(--warning) 38%, transparent); }
.entry.is-internal .entry-head {
  background: var(--warning-soft);
  border-bottom-color: color-mix(in srgb, var(--warning) 30%, transparent);
}
.entry.is-internal .entry-body {
  background: color-mix(in srgb, var(--warning) 4%, var(--surface-1));
}
.entry.is-internal::before {
  content: "";
  display: block;
  height: 3px;
  background: repeating-linear-gradient(
    -45deg,
    var(--warning) 0 6px,
    transparent 6px 12px
  );
  opacity: .8;
}
.note-flag {
  display: inline-flex; align-items: center; gap: 4px;
  color: var(--warning); font-size: var(--t-xs); font-weight: 620;
  letter-spacing: .04em; text-transform: uppercase;
}

/* --- the timeline ---------------------------------------------------------
   Comments and history in one column, because they happened in one order and
   splitting them into two tabs makes the reader reconstruct it.
   -------------------------------------------------------------------------- */
.thread { display: grid; gap: 12px; }

.event {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 2px 0 2px 4px;
  font-size: var(--t-sm);
  color: var(--text-2);
}
.event .icon-wrap {
  display: grid; place-items: center;
  width: 24px; height: 24px; flex: none;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-3);
  box-shadow: inset 0 0 0 1px var(--border);
}
.event.is-internal .icon-wrap { background: var(--warning-soft); color: var(--warning); box-shadow: none; }
.event .body { padding-top: 2px; min-width: 0; }
.event strong { color: var(--text-1); font-weight: 560; }
.event .when { color: var(--text-3); font-size: var(--t-xs); white-space: nowrap; }
.event del { color: var(--text-3); text-decoration-thickness: 1px; }

/* A run of events between two comments collapses to a hairline group so the
   conversation still reads as a conversation. */
.event-group {
  display: grid; gap: 5px;
  padding: 8px 12px;
  border-left: 2px solid var(--border);
  margin-left: 11px;
}

/* --- the composer ---------------------------------------------------------- */
.composer {
  position: relative;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  /* NOT overflow: hidden. The @-menu is a child of this element positioned
     above it, and hiding the overflow clips it out of existence — the menu
     opens, the DOM says so, and nothing appears on screen. The corners are
     kept square-free by rounding the two children that have a background of
     their own instead. */
}
.composer .textarea {
  border: 0; border-radius: var(--r-lg) var(--r-lg) 0 0; background: transparent;
  min-height: 90px;
}
.composer .textarea:focus { box-shadow: none; }
.composer-foot {
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  padding: 9px 12px;
  border-top: 1px solid var(--border);
  border-radius: 0 0 var(--r-lg) var(--r-lg);
  background: var(--surface-2);
}
.composer-foot .spacer { margin-left: auto; }

/* Arming the internal switch recolours the whole composer, before a word is
   typed. The switch is a checkbox; this is what makes it impossible to miss. */
.composer:has(.internal-switch input:checked) {
  border-color: color-mix(in srgb, var(--warning) 40%, transparent);
}
.composer:has(.internal-switch input:checked) .composer-foot {
  background: var(--warning-soft);
  border-top-color: color-mix(in srgb, var(--warning) 30%, transparent);
}
.composer:has(.internal-switch input:checked) .textarea { background: color-mix(in srgb, var(--warning) 4%, transparent); }
.internal-switch { display: inline-flex; align-items: center; gap: 6px; min-height: var(--tap-sm); font-size: var(--t-sm); color: var(--text-2); cursor: pointer; }
.internal-switch input { accent-color: var(--warning); }
.composer:has(.internal-switch input:checked) .internal-switch { color: var(--warning); font-weight: 600; }

/* Who Send will email. Two versions in the markup; the internal switch picks
   which one shows, with the same :has() that recolours the composer. */
.composer-mail {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 8px;
  padding: 9px 12px;
  border-top: 1px solid var(--border);
  font-size: var(--t-sm); color: var(--text-2);
}
.composer-mail > .icon { align-self: center; flex: none; color: var(--text-3); }
.composer-mail strong { color: var(--text-1); font-weight: 560; }
.composer-mail .for-internal { display: none; }
.composer:has(.internal-switch input:checked) .composer-mail .for-internal { display: inline; }
.composer:has(.internal-switch input:checked) .composer-mail .for-public { display: none; }

/* --- who was emailed ---------------------------------------------------------
   Under a reply or a change: who it emailed, who is still waiting, and who
   it could not reach. Quiet unless something failed. */
.mailed {
  display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 12px;
  margin-top: 12px;
  font-size: var(--t-sm); color: var(--text-3);
}
.mailed > .icon { align-self: center; flex: none; }
.mailed-who { color: var(--text-2); cursor: help; }
.mailed-group.is-waiting, .mailed-group.is-waiting .mailed-who { color: var(--warning); }
.mailed-group.is-failed, .mailed-group.is-failed .mailed-who { color: var(--critical); }
.mailed-why { flex-basis: 100%; }
.mailed-why summary { cursor: pointer; color: var(--critical); min-height: var(--tap-sm); display: inline-flex; align-items: center; }
.mailed-why ul { margin: 4px 0 0; padding-left: 18px; color: var(--text-2); overflow-wrap: anywhere; }
.event .mailed { margin-top: 4px; }

/* --- the properties rail --------------------------------------------------- */
/* The right-hand column stays in view as the thread scrolls — the whole
   column, not only the properties, and never taller than the window. Pinning
   the properties alone let the panels below them scroll up underneath it,
   and a pinned column taller than the window hides its own foot. When it
   does not fit it scrolls on its own. The padding keeps the panels' shadows
   and focus rings clear of the edge the overflow clips at. */
.ticket-rail {
  position: sticky;
  top: calc(var(--top-h) + 16px);
  max-height: calc(100vh - var(--top-h) - 32px);
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
  padding: 3px;
  margin: -3px;
}
.props {
  display: grid;
  gap: 1px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--border);
  box-shadow: var(--lift);
  overflow: hidden;
}
.prop {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr);
  gap: 12px;
  align-items: center;
  padding: 10px 14px;
  background: var(--surface-1);
  font-size: var(--t-md);
}
.prop > .k { color: var(--text-3); font-size: var(--t-sm); }
.prop > .v { min-width: 0; color: var(--text-1); }
.prop.stacked { grid-template-columns: 1fr; gap: 8px; }
/* A stacked property has no value column to line its text up with, so its
   control sits square in the rail rather than pulled 10px out of it. */
.prop.stacked .select, .prop.stacked .input { margin-left: 0; }
/* The rail's second half: what the ticket is, rather than what can be set. */
.prop-group {
  padding: 14px 14px 6px;
  background: var(--surface-1);
  font-size: var(--t-micro); font-weight: 650; letter-spacing: .1em;
  text-transform: uppercase; color: var(--text-3);
}
.prop-more { display: block; }
.prop-more > summary {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--tap-sm);
  color: var(--text-2); font-size: var(--t-sm);
  cursor: pointer; list-style: none;
}
.prop-more > summary::-webkit-details-marker { display: none; }
.prop-more > summary .icon:last-child { margin-left: auto; transition: rotate .15s var(--ease); }
.prop-more[open] > summary .icon:last-child { rotate: 180deg; }
.prop-more > summary:hover { color: var(--text-1); }
.prop-more[open] > summary { margin-bottom: 8px; }
.prop-move { display: flex; gap: 8px; align-items: center; margin-bottom: 6px; }
.prop-more .prop-move .select { margin-left: 0; }
.prop-move .select { flex: 1; min-width: 0; }
.prop-move .btn { flex: none; }

/* A PROPERTY THAT CAN BE CHANGED LOOKS LIKE IT CAN BE CHANGED.

   This used to read: "a form control that looks like text until it is
   hovered", with a transparent border and — because `background: transparent`
   also wiped out the .select's background-image — no caret either. The result
   was a rail in which Status, Assignee and Priority were indistinguishable
   from Project, Client and Reporter, which genuinely cannot be changed. The
   only way to find out which was which was to move the mouse across all nine
   and watch for one to light up.

   They are quiet — a hairline and a caret, not a filled box — but they are
   never invisible. Quiet is a weight. It is not a disappearing act. */
.prop .select, .prop .input {
  min-height: var(--tap-sm);
  padding: 6px 30px 6px 10px;
  margin-left: -10px;
  border-color: var(--border);
  background-color: transparent;
  font-size: var(--t-md);
}
.prop .input { padding-right: 10px; }
.prop .select:hover, .prop .input:hover { border-color: var(--border-strong); background-color: var(--surface-2); }
.prop .select:focus, .prop .input:focus { border-color: var(--accent); background-color: var(--surface-2); }

.prop-tags { display: flex; flex-wrap: wrap; gap: 5px; }

/* A value that is a dot plus a name is one line. .row wraps by default, which
   put the project's colour on a line of its own above the project. */
.prop > .v > .row { flex-wrap: nowrap; }
.prop > .v > .row .truncate { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* --- the board ------------------------------------------------------------
   The secondary view. One column per status, in the administrator's order,
   and the columns scroll sideways as one rather than each on its own.
   -------------------------------------------------------------------------- */
.board {
  display: flex;
  gap: 12px;
  align-items: flex-start;
  overflow-x: auto;
  padding-bottom: 8px;
  scrollbar-width: thin;
}
.column {
  flex: 0 0 290px;
  display: grid;
  gap: 8px;
  align-content: start;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--lift);
  max-height: calc(100vh - var(--top-h) - 80px);
  overflow-y: auto;
}
.column-head {
  display: flex; align-items: center; gap: 7px;
  padding: 2px 2px 8px;
  border-bottom: 1px solid var(--border);
  font-size: var(--t-sm); font-weight: 600;
  position: sticky; top: 0; z-index: 2;
  background: var(--surface-1);
}
.column-head .n { margin-left: auto; color: var(--text-3); font-variant-numeric: tabular-nums; font-weight: 500; }

.tile {
  display: grid; gap: 6px;
  padding: 10px;
  border: 1px solid var(--border);
  border-left: 3px solid var(--tone, var(--border));
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: inherit;
  transition: border-color .14s var(--ease), transform .14s var(--ease);
}
.tile:hover { text-decoration: none; border-color: var(--border-strong); border-left-color: var(--tone, var(--border-strong)); }
.tile .title { font-size: var(--t-sm); font-weight: 530; line-height: 1.4; color: var(--text-1); }
.tile .foot { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.tile .foot .spacer { margin-left: auto; }

/* --- attachments ---------------------------------------------------------- */
.files { display: grid; gap: 6px; }
.file {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 10px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  color: inherit;
  font-size: var(--t-sm);
  min-width: 0;
}
a.file:hover { text-decoration: none; border-color: var(--border-strong); }
.file .icon { color: var(--text-3); flex: none; }
.file .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 520; }
.file .size { margin-left: auto; color: var(--text-3); font-size: var(--t-xs); white-space: nowrap; font-variant-numeric: tabular-nums; }
.file form { display: contents; }

/* Image attachments get a thumbnail strip instead of a list of filenames: a
   screenshot is the commonest attachment on a ticket by a wide margin, and
   its filename is almost never the useful thing about it. */
.shots { display: flex; gap: 8px; flex-wrap: wrap; }
.shot {
  display: block;
  width: 116px; height: 78px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
  background: var(--surface-3);
}
.shot:hover { border-color: var(--accent); }
.shot img { width: 100%; height: 100%; object-fit: cover; }

/* The drop zone on the composer. Only meaningful with script, which is why it
   carries .needs-js. */
.dropzone {
  padding: 10px 12px;
  border: 1px dashed var(--border-strong);
  border-radius: var(--r-md);
  color: var(--text-3);
  font-size: var(--t-sm);
  text-align: center;
}
.dropzone.is-over { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }

/* --- meters (a ratio against a limit) ------------------------------------- */
.meter { display: grid; gap: 4px; min-width: 120px; }
.meter-head { display: flex; align-items: baseline; gap: 6px; font-size: var(--t-xs); color: var(--text-2); }
.meter-head .pct { margin-left: auto; font-variant-numeric: tabular-nums; font-weight: 600; color: var(--text-1); }
.meter-track {
  position: relative;
  height: 6px;
  border-radius: 999px;
  /* Track and fill are two steps of ONE ramp, so a part-filled bar reads as a
     single object rather than as two colours meeting. */
  background: color-mix(in srgb, var(--accent-solid) 26%, var(--surface-3));
  overflow: hidden;
}
.meter-fill { position: absolute; inset: 0 auto 0 0; border-radius: 999px; background: var(--accent-solid-hi); min-width: 3px; }
.meter[data-state="warning"]  .meter-fill { background: var(--warning); }
.meter[data-state="critical"] .meter-fill { background: var(--critical); }
.meter[data-state="warning"]  .pct { color: var(--warning); }
.meter[data-state="critical"] .pct { color: var(--critical); }

/* --- bars (magnitude across a set, one hue) ------------------------------- */
.bars { display: grid; gap: 9px; }
.bar-row { display: grid; grid-template-columns: minmax(90px, 1fr) 2fr auto; gap: 12px; align-items: center; font-size: var(--t-sm); }
.bar-row .name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-2); }
.bar-track { height: 7px; border-radius: 999px; background: var(--surface-3); overflow: hidden; }
.bar-fill { height: 100%; border-radius: 999px; background: var(--accent-solid-hi); min-width: 2px; }
.bar-row .value { color: var(--text-2); font-variant-numeric: tabular-nums; }

/* --- tabs ------------------------------------------------------------------
   One thing seen from several sides — a client's overview, its people, its
   projects, its settings — each side its own address. Drawn like the
   sections in the top bar, a line under the current one, because it is the
   same idea one level down. Scrolls sideways on a phone rather than wrapping
   into a second row nobody reads as tabs.
   -------------------------------------------------------------------------- */
.tabs {
  display: flex;
  gap: 2px;
  /* The first tab's text lines up with the title above it. */
  padding: 0 4px;
  margin-bottom: 20px;
  border-bottom: 1px solid var(--border);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tab {
  position: relative;
  display: inline-flex; align-items: center; gap: 8px;
  min-height: var(--tap);
  padding: 0 14px;
  color: var(--text-2);
  font-size: var(--t-md); font-weight: 520;
  white-space: nowrap;
}
.tab .icon { color: var(--text-3); flex: none; }
.tab:hover { color: var(--text-1); text-decoration: none; }
.tab[aria-current] { color: var(--text-1); font-weight: 600; }
.tab[aria-current] .icon { color: var(--accent); }
.tab[aria-current]::after {
  content: "";
  position: absolute;
  left: 10px; right: 10px; bottom: -1px;
  height: 2px;
  border-radius: 2px 2px 0 0;
  background: var(--accent);
}
.tab:focus-visible { outline: 3px solid var(--accent); outline-offset: -3px; border-radius: var(--r-sm); }

/* Label and value, one pair a line — the facts about a thing, read down. */
.facts { display: grid; margin: 0; }
.facts > div { display: grid; gap: 6px; padding: 14px 16px; }
.facts > div + div { border-top: 1px solid var(--border); }
.facts dt { font-size: var(--t-xs); font-weight: 620; letter-spacing: .05em; text-transform: uppercase; color: var(--text-3); }
.facts dd { margin: 0; display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

/* A row about somebody who can no longer sign in, or a project that is
   archived: still there, still reachable, visibly set aside. */
table.grid tbody tr.is-dim > td { color: var(--text-3); }
table.grid tbody tr.is-dim .who { opacity: .6; }

/* A person in a table row: face, then a name that is the link to them, and
   what they do under it. */
.who-text { display: grid; min-width: 0; gap: 1px; justify-items: start; }
.who-name { font-weight: 580; color: var(--text-1); }
/* A link that is a row's way in clears the target floor, not just its text. */
a.who-name, a.cell-link, .facts dd a { display: inline-flex; align-items: center; gap: 6px; min-height: var(--tap-min); }
a.who-name:hover { color: var(--accent); }
.who-sub { font-size: var(--t-sm); color: var(--text-3); }
.who-text .tag { margin-top: 2px; }
.who-line:has(.who.lg) { gap: 12px; }

/* An add-row that is the last thing in its panel needs no rule under it. */
.panel > form.add-row:last-child { border-bottom: 0; }
/* The last line of an add-row, under its fields: an option, or the errors. */
.add-row > .full { flex-basis: 100%; }
.add-row > .check.full { margin-top: -4px; }

/* The box that narrows a long pick list, added by app.js. */
.pick-filter { display: flex; align-items: center; gap: 12px; margin-bottom: 8px; }
.pick-filter .input { max-width: 320px; }
.panel > .pick-filter { margin: 12px 12px 0; }
.pick-row[hidden] { display: none; }

/* What a client still needs, on its pages, each with the button that does it. */
.notice.setup-notice { align-items: center; font-size: var(--t-md); padding: 12px 14px; }
.notice.setup-notice .btn { flex: none; }

/* --- notices -------------------------------------------------------------- */
.notice {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 10px 13px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
  font-size: var(--t-sm);
  color: var(--text-2);
}
.notice .icon { flex: none; margin-top: 1px; }
.notice strong { color: var(--text-1); font-weight: 600; }
.notice.info { border-color: var(--accent-line); background: var(--accent-soft); }
.notice.info .icon { color: var(--accent); }
.notice.warn { border-color: color-mix(in srgb, var(--warning) 40%, transparent); background: var(--warning-soft); }
.notice.warn .icon { color: var(--warning); }
.notice.bad { border-color: color-mix(in srgb, var(--critical) 40%, transparent); background: var(--critical-soft); }
.notice.bad .icon { color: var(--critical); }
.notice .spacer { margin-left: auto; }

/* --- toasts --------------------------------------------------------------- */
.toasts {
  position: fixed;
  top: 14px; left: 50%; translate: -50% 0;
  z-index: 200;
  display: grid; gap: 8px;
  width: min(460px, calc(100vw - 24px));
}
.toast {
  display: flex; gap: 9px; align-items: flex-start;
  padding: 10px 13px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
  font-size: var(--t-md);
}
.toast p { flex: 1; }
.toast .icon { flex: none; margin-top: 1px; }
.toast.ok .icon { color: var(--good); }
.toast.ok { border-color: color-mix(in srgb, var(--good) 45%, transparent); }
.toast.warn .icon { color: var(--warning); }
.toast.warn { border-color: color-mix(in srgb, var(--warning) 45%, transparent); }
.toast.bad .icon { color: var(--critical); }
.toast.bad { border-color: color-mix(in srgb, var(--critical) 45%, transparent); }
.toast .close { border: 0; background: none; color: var(--text-3); cursor: pointer; padding: 0 2px; }
.toast .close:hover { color: var(--text-1); }
.toast.leaving { opacity: 0; translate: 0 -8px; transition: opacity .2s, translate .2s; }

/* --- dialogs -------------------------------------------------------------- */
.modal {
  width: min(520px, calc(100vw - 24px));
  max-height: calc(100vh - 48px);
  /* The global `* { margin: 0 }` removes the user-agent's `margin: auto`,
     which is the only thing that centres a modal <dialog>. */
  margin: auto;
  padding: 0;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  color: var(--text-1);
  box-shadow: var(--shadow-3);
}
.modal::backdrop { background: rgb(0 0 0 / 55%); backdrop-filter: blur(2px); }
.modal-head {
  display: flex; align-items: center; gap: 9px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--border);
}
.modal-head h2 { flex: 1; font-size: var(--t-md); }
.modal-body { padding: 16px; display: grid; gap: 12px; }
.modal-foot {
  display: flex; align-items: center; gap: 8px;
  padding: 11px 16px;
  border-top: 1px solid var(--border);
  background: var(--surface-2);
}
.modal-foot .spacer { margin-left: auto; }

/* Type-to-confirm, for the handful of things that cannot be undone. The
   button stays disabled until the typed text matches, which is enforced in
   script — and the form is refused on the server too, because a disabled
   attribute is a suggestion. */
.confirm-target {
  font-family: var(--mono); font-size: var(--t-sm);
  padding: 2px 6px; border-radius: 4px;
  background: var(--surface-3);
  user-select: all;
}

/* --- disclosure (inline panels that open in place) ------------------------ */
.disclose > summary { list-style: none; cursor: pointer; display: inline-flex; }
.disclose > summary::-webkit-details-marker { display: none; }
.disclose[open] > summary .btn { background: var(--surface-3); }
.disclose-panel {
  margin-top: 10px;
  padding: 14px;
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--surface-2);
}

/* --- the sign-in screens --------------------------------------------------- */
.gate {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  /* One soft wash of the accent behind the card, so the sign-in page is not a
     grey rectangle on a grey field. Fixed so it does not travel on a short
     scroll. */
  background:
    radial-gradient(80ch 60ch at 50% -10%, var(--accent-soft), transparent 70%),
    var(--bg);
  background-attachment: fixed;
}
/* The card and the theme switch under it, centred as one. The switch used to
   follow .gate, which is 100vh — so it was reliably below the fold. */
.gate-stack { display: grid; gap: 14px; justify-items: center; width: min(400px, 100%); }

.gate-card {
  width: 100%;
  padding: 26px;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface-1);
  box-shadow: var(--shadow-2), var(--lift);
}
.gate-brand { display: flex; align-items: center; gap: 9px; margin-bottom: 20px; }
.gate-card h1 { font-size: var(--t-xl); letter-spacing: -.025em; }
.gate-card .lede { margin-top: 5px; font-size: var(--t-md); }
.gate-form { display: grid; gap: 13px; margin-top: 20px; }
.gate-foot { margin-top: 16px; text-align: center; font-size: var(--t-sm); color: var(--text-3); }
.gate-foot a { color: var(--text-2); }
.gate-aside { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; }

/* --- theme switch ----------------------------------------------------------
   A segmented control of three, because "follow the system" is a real
   preference and a two-way switch cannot express it.

   Styled from the ELEMENT, not from .btn. Every skin restyles .btn from
   `:root[data-skin="x"] .btn`, which is a class deeper than anything this file
   can write and lands after it — so a segmented control built on .btn would
   lose the very thing that marks the chosen one.

   Two sizes. The bare one is icons only, for the account menu, where the row
   it sits in is already titled "Theme". `.is-labelled` puts the name beside
   the icon and is what the sign-in page uses, because that is where somebody
   meets the control for the first time.
   -------------------------------------------------------------------------- */
.theme-switch {
  display: inline-flex;
  /* A grid item stretches by default, and a row of three buttons stretched to
     the width of a settings panel reads as a toolbar rather than as a
     choice. */
  justify-self: start;
  gap: 2px;
  padding: 3px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface-2);
}
.theme-switch button {
  display: grid; place-items: center;
  /* Above --tap-min, not at it. This is a control people press by eye in a
     menu that is already floating over the page. */
  width: 28px; height: 28px;
  border: 1px solid transparent; border-radius: 999px;
  background: transparent; color: var(--text-3);
  font: inherit;
  cursor: pointer;
  transition: background .14s var(--ease), color .14s var(--ease);
}
.theme-switch button:hover:not([disabled]) { background: var(--surface-3); color: var(--text-1); }

/* The current one is disabled — pressing it would be a round trip to nowhere —
   so it has to read as CHOSEN rather than as unavailable, which is the
   opposite of what a disabled button usually means. Hence the explicit
   opacity and the default cursor: the browser's own disabled styling says the
   wrong thing here. */
.theme-switch button[aria-current="true"] {
  background: var(--surface-1);
  border-color: var(--border-strong);
  color: var(--accent);
  box-shadow: var(--shadow-1);
  opacity: 1;
  cursor: default;
}

.theme-switch.is-labelled { border-radius: calc(var(--r-md) + 4px); }
.theme-switch.is-labelled button {
  display: inline-flex; align-items: center; gap: 7px;
  width: auto; min-height: var(--tap-sm);
  padding: 5px 12px;
  border-radius: var(--r-md);
  font-size: var(--t-sm); font-weight: 550;
  white-space: nowrap;
}
.theme-switch.is-labelled button[aria-current="true"] { color: var(--text-1); font-weight: 650; }
.theme-switch.is-labelled button[aria-current="true"] .icon { color: var(--accent); }

/* Three labelled choices do not always fit the sign-in card, which is 400px at
   most and narrower on a phone. Wrapping is the honest answer; overflowing is
   not. */
.gate-aside .theme-switch.is-labelled { flex-wrap: wrap; justify-content: center; max-width: 100%; }

/* The language switch is the same control with words and no icons — a
   language is recognised by its own name, and no glyph says "Français".
   Compact in the account menu, where it shares a row with its title; still
   28px tall there, above --tap-min, like the theme buttons beside it. */
.lang-switch.is-compact button { min-height: 28px; padding: 3px 10px; }

/* --- the one-press light/dark switch ---------------------------------------
   At the foot of the contextual rail. Both wordings are in the markup and this
   is what makes only one of them real — see partials/theme-toggle.blade.php
   for why it is not done in script.
   -------------------------------------------------------------------------- */
.theme-toggle { gap: 9px; }
.theme-toggle .when-dark, .theme-toggle .when-light { display: contents; }
:root[data-theme="light"] .theme-toggle .when-dark,
:root:not([data-theme="light"]) .theme-toggle .when-light { display: none; }

/* In the rail it spans the foot and reads as a row, like the collapse under
   it. `display: contents` on the form so the button itself is the flex/grid
   child and the wrapper adds no box of its own. */
.side .theme-toggle-form { display: contents; }
.side-theme { flex: none; border-top: 1px solid var(--border); padding: 6px; }
.side .theme-toggle {
  width: 100%;
  justify-content: flex-start;
  min-height: var(--tap);
  padding: 6px 8px;
  font-size: var(--t-sm);
  color: var(--text-3);
}
.side .theme-toggle:hover { background: var(--surface-2); color: var(--text-1); }
.side .theme-toggle .icon { flex: none; }
/* Collapsed to icons, the word goes and the icon centres — the same move the
   rail's own items make. The button keeps its title from the markup. */
.app[data-rail="icons"] .side .theme-toggle { justify-content: center; }
.app[data-rail="icons"] .side .theme-toggle span span { display: none; }

/* --- the hue picker --------------------------------------------------------
   Offered only by the handful of looks that are a scheme rather than a
   picture; see the tintable note in config/yelatix.php.

   THE TRACK IS NOT A RAINBOW. It is handed in as --hue-track, a gradient of
   the colours THIS look actually takes at each hue, built on the server by the
   same code that rewrites the stylesheet — so it goes dark where the DOS
   screen is dark and flattens where sRGB runs out, instead of promising a
   saturation the page will not have.

   A range input has to be styled twice over: ::-webkit-slider-runnable-track
   and ::-moz-range-track are separate pseudo-elements, and a rule naming both
   in one selector list is dropped whole by both engines — so every pair below
   is written out twice on purpose. `appearance: none` is what stops each of
   them drawing its own groove over the gradient.
   -------------------------------------------------------------------------- */
.hue-switch { display: grid; gap: 12px; justify-items: start; }
.hue-row { display: flex; align-items: center; gap: 14px; width: 100%; max-width: 520px; }

.hue-range {
  appearance: none; -webkit-appearance: none;
  flex: 1 1 auto; min-width: 140px;
  /* The element is --tap-min tall even though the groove is 14px, so the
     thing you have to hit clears the floor rather than the thing you can
     see. */
  height: var(--tap-min); margin: 0; padding: 0;
  background: transparent;
  cursor: pointer;
}
.hue-range:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

.hue-range::-webkit-slider-runnable-track {
  height: 14px; border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background-image: var(--hue-track);
}
.hue-range::-moz-range-track {
  height: 14px; border-radius: var(--r-sm);
  border: 1px solid var(--border);
  background-image: var(--hue-track);
}

/* The thumb rides ON the track rather than beside it, so it reads as a
   position on the palette. Two rings — the page colour then the ink — so it
   stays visible over both the dark end of a track and the bright end. */
.hue-range::-webkit-slider-thumb {
  appearance: none; -webkit-appearance: none;
  width: 14px; height: 24px;
  margin-top: -6px;
  border: 0; border-radius: var(--r-sm);
  background: var(--text-1);
  box-shadow: 0 0 0 2px var(--surface-1);
}
.hue-range::-moz-range-thumb {
  width: 14px; height: 24px;
  border: 0; border-radius: var(--r-sm);
  background: var(--text-1);
  box-shadow: 0 0 0 2px var(--surface-1);
}

.hue-value {
  min-width: 4.5ch;
  font-size: var(--t-sm); color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.hue-switch .hint { max-width: 62ch; }

/* --- what exists only with script, and only without -----------------------
   .has-js is set by assets/theme.js in the <head>, before first paint, so
   neither of these ever flashes the other state.
   -------------------------------------------------------------------------- */
:root.has-js .no-js { display: none !important; }
:root:not(.has-js) .needs-js { display: none !important; }

/* --- layout helpers ------------------------------------------------------- */
.row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.row.tight { gap: 5px; }
.row .spacer { margin-left: auto; }
.split { display: grid; grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr); gap: 16px; align-items: start; }
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
.plain { list-style: none; padding: 0; margin: 0; display: grid; gap: 6px; }

/* --- responsive ------------------------------------------------------------
   Four steps, each undoing one structural assumption:
     1120  the ticket rail goes under the thread
      980  the split column stacks
      860  the contextual rail becomes a strip of icons, then a bar
      620  the queue sheds the columns a phone cannot use
   -------------------------------------------------------------------------- */
@media (max-width: 1340px) {
  /* The priority word goes before anything else: the ramp of bars beside it
     still ranks two rows against each other, which is what the column is
     scanned for. */
  .qrow .pick-prio .name { display: none; }
}

@media (max-width: 1240px) {
  /* Then the project on every row: the rail and the facet bar both still say
     which project is being looked at. */
  .qrow .qproject .hide-md { display: none; }
}

@media (max-width: 1120px) {
  .ticket { grid-template-columns: 1fr; }
  .ticket-rail { position: static; max-height: none; overflow: visible; padding: 0; margin: 0; }
}

@media (max-width: 980px) {
  .split { grid-template-columns: 1fr; }
  .compose { grid-template-columns: minmax(0, 1fr); }
  .compose-side { position: static; }
  /* Narrow enough that the labels are costing the title its room. The rail is
     still there, still current, still reachable — it is wearing the collapsed
     state the cookie can also ask for. */
  .app { --side-w: var(--side-icons); }
  .side-item .label, .side-item .tally, .side-label span, .side-more { display: none; }
  .side-item { justify-content: center; padding-inline: 0; }
  .side-collapse { display: none; }
}

@media (max-width: 900px) {
  /* The intro goes above its card rather than beside it. */
  .setting-group { grid-template-columns: minmax(0, 1fr); gap: 14px; padding: 24px 0; }
  .setting-intro p { max-width: none; }
}

@media (max-width: 860px) {
  .app { grid-template-columns: minmax(0, 1fr); grid-template-rows: var(--top-h) auto minmax(0, 1fr); }

  /* The rail stops being on the side. It keeps its name because what it is
     has not changed — only where it is. */
  .side {
    grid-row: 2;
    position: static;
    height: auto;
    flex-direction: row;
    align-items: center;
    border-right: 0;
    border-bottom: 1px solid var(--border);
    overflow-x: auto;
  }
  .side-scroll {
    display: flex;
    align-items: center;
    gap: 4px;
    overflow: visible;
    padding: 6px 10px;
  }
  .side-group { display: flex; align-items: center; gap: 4px; }
  .side-group + .side-group { margin-top: 0; }
  .side-group + .side-group::before {
    content: "";
    width: 1px; height: 18px;
    background: var(--border);
    margin: 0 4px;
  }
  .side-label { display: none; }
  .frame { grid-row: 3; }

  .cmd { flex-basis: auto; }
  .cmd-text, .cmd kbd { display: none; }

  /* The heading and the page's actions stop sharing a line. The spacer that
     pushed them apart becomes the line break instead, so the title gets its
     full width rather than being ellipsised down to "Tick…". */
  .head { flex-wrap: wrap; }
  .head .spacer { flex-basis: 100%; height: 0; margin: 0; }
  .arrange { flex-wrap: wrap; }
  .head, .facets { padding-inline: 12px; }
  .sheet { padding: 0 12px 40px; }
  .savebar { margin-inline: -12px; padding-inline: 12px; }
  .sheet.flush { padding: 0; }
  .qrow { padding-inline: 12px; }
  .qgroup-head { padding-inline: 12px; }

  /* Below this width the bars are no longer stacked above the table, so the
     header has nothing to park under and the nested scroll container costs
     nothing. */
  .grid-wrap { overflow-x: auto; }
  table.grid th { top: 0; }
}

@media (max-width: 620px) {
  /* Two pictures a row: one a row made the list of looks eight screens long. */
  .choices, .choices.themes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .look-art, .theme-art { height: 76px; }
  table.grid .hide-sm { display: none; }
  .section .tally, .hide-sm { display: none; }

  /* The top bar's sections scroll rather than collide with the buttons to
     their right. Without this "Manage" simply sat underneath the New-ticket
     button on a phone. */
  .sections { min-width: 0; overflow-x: auto; scrollbar-width: none; }
  .sections::-webkit-scrollbar { display: none; }
  /* Sized so that all three section names fit a 400px screen rather than the
     last one being sliced down the middle, which reads as a bug and not as a
     scrollable strip. */
  .section { padding: 0 8px; font-size: var(--t-sm); }
  .sections { margin-left: 2px; }
  .top { padding-inline: 8px; gap: 3px; }
  .top .btn-primary { padding-inline: 11px; }

  /* THE ROW BECOMES TWO LINES.

     One line does not fit a phone, and the single-line version did not
     degrade — it just gave the title whatever was left after the status pill
     and the assignee had taken theirs, which was about three characters
     ("Inv…", "Ad…", "Ra…"). A title nobody can read is not a denser list, it
     is a list with no content in it.

     So: the title takes the full width on its own line and is allowed to wrap
     to two, and everything that qualifies it sits underneath. */
  .qrow {
    flex-wrap: wrap;
    align-items: center;
    row-gap: 8px;
    column-gap: 10px;
    padding: 12px 14px;
  }
  .qtitle {
    order: -1;
    flex: 1 1 100%;
    align-self: auto;
  }
  .qtext {
    white-space: normal;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    line-height: 1.35;
  }
  .qkey { align-self: auto; width: auto; }
  .qgap { display: none; }
  .qrow .qcounts, .qrow .qage, .qrow .qlabels, .qrow .qproject { display: none; }
  /* Bulk selection is not a phone gesture, and the box is the widest thing on
     the line that nobody there will use. */
  .qrow .qtick { display: none; }
  .qrow .qdue { min-width: 0; margin-left: auto; }

  .figure { flex-basis: 50%; }
  .arrange { width: 100%; }
  .arrange .facet { flex: 1 1 120px; min-width: 0; max-width: none; }
}

/* --- touch -----------------------------------------------------------------
   A coarse pointer has no hover, so every affordance that reveals itself on
   hover has to be permanent here — otherwise a phone gets an interface whose
   controls never announce themselves at all. This is the one place the
   chevrons and the tickboxes are unconditional.
   -------------------------------------------------------------------------- */
@media (pointer: coarse) {
  .pick > summary::after { opacity: .75; }
  .qtick input { opacity: 1; }

  /* Fingers, not cursors: the comfortable target becomes the floor for the
     things that were allowed to be --tap-sm inside a dense row. */
  .pick > summary,
  .btn-sm,
  .check { min-height: var(--tap); }
}

/* --- accessibility --------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* More contrast, for readers who have asked their system for it. Everything
   quiet moves up one step; nothing moves position. */
@media (prefers-contrast: more) {
  :root {
    --text-2: #c2c8d6;
    --text-3: #9aa1b3;
    --border: #3a4152;
    --border-strong: #4d5568;
  }
  :root[data-theme="light"] {
    --text-2: #3a4050;
    --text-3: #565d70;
    --border: #c3c9d6;
    --border-strong: #9aa2b4;
  }
  .btn, .input, .select, .textarea, .tag { border-width: 1px; }
  .side-item[aria-current] { box-shadow: inset 0 0 0 1.5px var(--accent); }
  .section[aria-current]::after { height: 3px; }
}

/* Windows high contrast. The system supplies every colour, so the job here is
   to put back the boundaries that were being drawn with colour alone — an
   active nav item, a filled priority bar, a status band. */
@media (forced-colors: active) {
  .panel, .entry, .figures, .tile, .column, .composer, .props, .modal,
  .palette, .pick-menu, .menu-panel, .bulkbar {
    border: 1px solid CanvasText;
  }
  .side-item[aria-current] { outline: 2px solid Highlight; outline-offset: -2px; }
  .side-item[aria-current] .icon { color: Highlight; }
  .section[aria-current] { outline: 2px solid Highlight; outline-offset: -2px; }
  /* The band down a row and the tint behind a ticked one are both colour
     alone, and colour alone is exactly what this mode takes away. */
  .qrow { border-left: 3px solid CanvasText; }
  .qrow.is-cursor { outline: 2px solid Highlight; outline-offset: -2px; }
  .qrow.is-ticked { outline: 2px dotted CanvasText; outline-offset: -2px; }
  .palette-item.is-active { outline: 2px solid Highlight; outline-offset: -2px; }
  .prio i { background: Canvas; border: 1px solid CanvasText; }
  .prio i.on { background: CanvasText; }
  .tag { border: 1px solid CanvasText; }
  .tag.is-status::before { outline: 1px solid CanvasText; }
  table.grid tbody tr td:first-child { box-shadow: none; border-left: 3px solid CanvasText; }
  .status-row:not(.status-head) { box-shadow: none; border-left: 3px solid CanvasText; }
  .swatch-pick:has(input:checked) { outline: 2px solid Highlight; }
  .entry.is-internal { outline: 2px dashed CanvasText; outline-offset: -2px; }
  .meter-track, .bar-track { border: 1px solid CanvasText; }
  .meter-fill, .bar-fill { background: Highlight; }
  .btn-primary { border: 1px solid ButtonText; }
}

/* Printing a ticket is a real thing people do when they take one into a
   meeting. The chrome goes; the thread stays. */
@media print {
  .top, .side, .facets, .composer, .toasts, .btn, .pager,
  .palette, .bulkbar, .qtick, .arrange { display: none !important; }
  .app { grid-template-columns: 1fr; grid-template-rows: auto; }
  .ticket { grid-template-columns: 1fr; }
  .ticket-rail { position: static; max-height: none; overflow: visible; }
  .entry-edit { display: none !important; }
  .sheet { padding: 0; max-width: none; }
  .panel, .entry, .props { break-inside: avoid; box-shadow: none; }
  a[href]::after { content: ""; }
}

/* --- the @-menu -----------------------------------------------------------
   Anchored to the composer rather than to the caret: following the caret
   means measuring text in a textarea, which needs a mirror element and gets
   it wrong on every wrap. The bottom-left corner is where the menu is in
   every chat application anyone has used, and it never covers what is being
   typed.
   -------------------------------------------------------------------------- */
.mention-menu {
  position: absolute;
  left: 8px;
  bottom: calc(100% + 6px);
  z-index: 40;
  min-width: 260px;
  max-width: min(360px, calc(100vw - 32px));
  max-height: 260px;
  overflow-y: auto;
  padding: 4px;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
  background: var(--surface-1);
  box-shadow: var(--shadow-2);
}

.mention-option {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--r-sm);
  background: transparent;
  color: var(--text-1);
  font-size: var(--t-md);
  text-align: left;
  cursor: pointer;
}
.mention-option:hover, .mention-option.is-active { background: var(--accent-soft); }
.mention-option.is-active { box-shadow: inset 0 0 0 1px var(--accent-line); }
.mention-name { font-weight: 540; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mention-handle { margin-left: auto; font-family: var(--mono); font-size: var(--t-xs); color: var(--text-3); }

/* ==========================================================================
   API keys, and the API reference
   --------------------------------------------------------------------------
   Two pages behind the account menu. The keys page is a list of cards — a
   key is a handful of facts, not a row of columns — and the reference is one
   long document, searched with the browser's own find, that reads like the
   rest of the application rather than like a different product bolted on.
   ========================================================================== */
.page-notice { margin-bottom: 16px; }

/* The new key's secret: the one moment it exists in the page. */
.secret { margin-bottom: 20px; border-color: var(--accent-line); }
.secret-intro h2 { display: flex; align-items: center; gap: 8px; font-size: var(--t-lg); font-weight: 600; }
.secret-intro h2 .icon { color: var(--accent); }
.secret-intro p { margin-top: 4px; max-width: 70ch; }
.secret-row { display: flex; gap: 8px; align-items: center; }
.secret-row .input { flex: 1; min-width: 0; }

/* Something to paste into a terminal, with the button that copies it. */
.code-block {
  display: flex; align-items: flex-start; gap: 8px;
  padding: 10px 10px 10px 14px;
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
}
.code-block .code {
  flex: 1; min-width: 0; margin: 0;
  font-family: var(--mono); font-size: var(--t-sm); line-height: 1.6;
  color: var(--text-1);
  white-space: pre; overflow-x: auto;
}
.code-block .btn { flex: none; }

.key-list { display: grid; gap: 12px; }
.key-card { overflow: hidden; }
.key-card.is-fresh { border-color: var(--accent-line); }
.key-card.is-expired .key-card-title strong { color: var(--text-3); }
.key-card-head { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; padding: 14px 16px; }
.key-card-title { display: grid; gap: 2px; flex: 1; min-width: 12rem; }
.key-card-title strong { font-size: var(--t-md); font-weight: 600; }
.key-card-meta { display: flex; flex-wrap: wrap; gap: 2px 6px; font-size: var(--t-sm); color: var(--text-3); }
.key-card-body { display: grid; gap: 10px; margin: 0; padding: 12px 16px 14px; border-top: 1px solid var(--border); }
.key-card-body > div { display: grid; grid-template-columns: 9rem 1fr; gap: 8px; align-items: start; }
.key-card-body dt {
  padding-top: 5px;
  font-size: var(--t-xs); font-weight: 620; letter-spacing: .05em;
  text-transform: uppercase; color: var(--text-3);
}
.key-card-body dd { margin: 0; }
@media (max-width: 640px) {
  .key-card-body > div { grid-template-columns: 1fr; }
}

/* The permissions on the key form, a group per thing they are about. */
.scope-groups { display: grid; gap: 18px; }
.scope-group { min-width: 0; margin: 0; padding: 0; border: 0; }
.scope-group legend { margin-bottom: 6px; padding: 0; }
.scope-code { margin-left: 6px; font-family: var(--mono); font-size: var(--t-xs); font-weight: 450; color: var(--text-3); }
.check strong .tag { margin-left: 6px; vertical-align: 1px; }

/* --- the reference ---------------------------------------------------------- */
.ref { display: grid; gap: 32px; }

.ref-toc-list, .ref-toc-list ul { list-style: none; margin: 0; padding: 0; }
.ref-toc-list { margin-top: 10px; columns: 2 18rem; column-gap: 32px; }
.ref-toc-list > li { break-inside: avoid; margin-bottom: 12px; }
.ref-toc-list > li > a { display: inline-flex; min-height: var(--tap-min); align-items: center; font-weight: 600; }
.ref-toc-list ul a {
  display: flex; align-items: center; gap: 8px;
  min-height: var(--tap-min); padding: 2px 0;
  font-size: var(--t-sm); color: var(--text-2);
}
.ref-toc-list ul a:hover { color: var(--text-1); }

.ref-section { display: grid; gap: 14px; scroll-margin-top: calc(var(--top-h) + 16px); }
.ref-heading { display: flex; align-items: center; gap: 8px; font-size: var(--t-xl); font-weight: 650; }
.ref-heading .icon { color: var(--accent); }
.ref .facts dd.ref-prose { display: grid; gap: 10px; }

.endpoint { overflow: hidden; scroll-margin-top: calc(var(--top-h) + 16px); }
.endpoint-head { display: grid; gap: 8px; padding: 16px 18px 14px; border-bottom: 1px solid var(--border); }
.endpoint-title { font-size: var(--t-lg); font-weight: 600; }
.endpoint-title a { color: inherit; }
.endpoint-line { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.endpoint-line .spacer { margin-left: auto; }
.endpoint-path { font-family: var(--mono); font-size: var(--t-md); color: var(--text-1); overflow-wrap: anywhere; }
.path-param { color: var(--accent); }
.endpoint-body { display: grid; gap: 10px; padding: 16px 18px 18px; }
.endpoint-body > .eyebrow { margin-top: 10px; }

/* The verb, coloured AND spelled out: the colour helps a reader scanning a
   long page, the word is what they actually read. */
.method {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 4.4em; padding: 2px 8px;
  border: 1px solid transparent; border-radius: 6px;
  font-family: var(--mono); font-size: var(--t-xs); font-weight: 700; letter-spacing: .03em;
}
.method.is-get    { color: var(--info);     background: var(--info-soft);     border-color: color-mix(in srgb, var(--info) 38%, transparent); }
.method.is-post   { color: var(--good);     background: var(--good-soft);     border-color: color-mix(in srgb, var(--good) 38%, transparent); }
.method.is-patch,
.method.is-put    { color: var(--warning);  background: var(--warning-soft);  border-color: color-mix(in srgb, var(--warning) 40%, transparent); }
.method.is-delete { color: var(--critical); background: var(--critical-soft); border-color: color-mix(in srgb, var(--critical) 40%, transparent); }

.status-code {
  display: inline-flex; margin-right: 6px; padding: 1px 7px;
  border-radius: 6px;
  font-family: var(--mono); font-size: var(--t-xs); font-weight: 700;
  color: var(--good); background: var(--good-soft);
}
.status-code.is-error { color: var(--critical); background: var(--critical-soft); }

.ref-table { border: 1px solid var(--border); border-radius: var(--r-md); }
.ref-table th { position: static; }
.ref-table td { vertical-align: top; }
.ref-table tbody tr:hover { background: transparent; }
table.grid td.ref-name { padding-left: calc(12px + var(--depth, 0) * 18px); white-space: nowrap; }
.ref-name code, .ref-type code, .ref-url { font-family: var(--mono); font-size: var(--t-sm); }
.ref-name code { color: var(--text-1); font-weight: 560; }
.ref-type code { color: var(--text-3); white-space: nowrap; }
.ref-required { margin-left: 6px; font-size: var(--t-xs); font-weight: 600; color: var(--critical); }
.ref-located { margin-left: 6px; font-size: var(--t-xs); color: var(--text-3); }
.ref-desc { color: var(--text-2); }
.ref-enum { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 6px; margin-top: 4px; }
.ref-enum code {
  padding: 0 6px; border: 1px solid var(--border); border-radius: 5px;
  background: var(--surface-2);
  font-family: var(--mono); font-size: var(--t-xs);
}
.ref-prose p + p { margin-top: 8px; }
.ref-prose code { padding: 0 5px; border-radius: 5px; background: var(--surface-2); font-family: var(--mono); font-size: .92em; }
.ref-responses { display: grid; gap: 12px; }
.ref-response { display: grid; gap: 8px; }
.ref-errors { display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 0; padding: 0; list-style: none; }
.ref-errors li { display: flex; align-items: center; gap: 4px; font-size: var(--t-sm); }

/* A table wider than a phone scrolls inside itself rather than pushing the
   whole page sideways. */
@media (max-width: 760px) {
  .ref-table { display: block; overflow-x: auto; }
  table.grid td.ref-name { white-space: normal; }
}
