/* =========================================================================
   ShipThat.io UI
   A dense workspace shell: two-tone plum navigation on the left, white grid
   on the right. The dark nav is the product's signature — the work surface
   stays paper-white so task rows read fast.
   ========================================================================= */

:root {
  /* Brand */
  --tf-violet: #7b68ee;
  --tf-violet-hover: #6350e2;
  --tf-violet-press: #5341c4;
  --tf-violet-tint: #f3f1fe;
  --tf-violet-tint-2: #e7e3fd;

  /* Navigation (two-tone: rail is the deepest layer, sidebar sits on top) */
  --tf-rail: #17121f;
  --tf-rail-hover: #241d31;
  --tf-rail-fg: #b6aecb;
  --tf-rail-fg-dim: #8b83a0;
  --tf-nav: #221b2e;
  --tf-nav-hover: rgba(255, 255, 255, 0.06);
  --tf-nav-line: rgba(255, 255, 255, 0.09);
  --tf-nav-fg: #ddd8ea;
  --tf-nav-fg-dim: #948ca9;
  --tf-nav-active: rgba(140, 122, 255, 0.20);
  --tf-nav-fg-strong: #ffffff;
  --tf-nav-field: rgba(255, 255, 255, 0.05);
  --tf-nav-field-focus: rgba(255, 255, 255, 0.09);
  --tf-nav-menu: #2c2439;
  --tf-violet-light: #b3a4ff;

  /* Text */
  --tf-ink: #292d34;
  --tf-ink-2: #5f6875;
  --tf-ink-3: #8f97a3;

  /* Surfaces */
  --tf-canvas: #ffffff;
  --tf-shell: #fafafb;
  --tf-hover: #f5f6f8;
  --tf-row-hover: #fbfbfd;
  --tf-chip-base: #ffffff;
  --tf-chip-ink: #191c22;
  --tf-line: #e8e9ee;
  --tf-line-soft: #f0f1f4;

  /* Semantic */
  --tf-red: #e5484d;
  --tf-red-tint: #fdf0f0;
  --tf-amber: #d98c14;
  --tf-green: #2eab6a;
  --tf-blue: #3b82f6;

  /* Type */
  --tf-ui: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  --tf-mono: "IBM Plex Mono", ui-monospace, "SF Mono", monospace;

  /* Metrics */
  --tf-rail-w: 64px;
  --tf-side-w: 256px;
  --tf-topbar-h: 46px;
  --tf-imp-h: 34px;
  --tf-row-h: 38px;
  --tf-radius: 8px;
  --tf-radius-sm: 6px;
  --tf-radius-lg: 12px;

  --tf-shadow-1: 0 1px 2px rgba(41, 45, 52, 0.06);
  --tf-shadow-2: 0 6px 16px rgba(41, 45, 52, 0.10);
  --tf-shadow-3: 0 18px 44px rgba(22, 25, 31, 0.20);

  --tf-ease: 120ms cubic-bezier(0.2, 0, 0.2, 1);

  /* Bootstrap bridge */
  --bs-body-font-family: var(--tf-ui);
  --bs-body-color: var(--tf-ink);
  --bs-border-radius: var(--tf-radius);
  --bs-border-color: var(--tf-line);
  --bs-link-color-rgb: 99, 80, 226;
  --bs-link-hover-color-rgb: 83, 65, 196;
}

/* ---------------------------------------------------------------- base ---- */

html { font-size: 14px; }

body {
  font-family: var(--tf-ui);
  font-size: 0.9286rem; /* 13px */
  line-height: 1.5;
  color: var(--tf-ink);
  background: var(--tf-canvas);
  -webkit-font-smoothing: antialiased;
  letter-spacing: -0.003em;
}

h1, h2, h3, h4, h5 { letter-spacing: -0.018em; font-weight: 600; color: var(--tf-ink); }
h1 { font-size: 1.5rem; }
h2 { font-size: 1.15rem; }
a { color: var(--tf-violet-hover); text-decoration: none; }
a:hover { color: var(--tf-violet-press); text-decoration: underline; }
code, .tf-mono { font-family: var(--tf-mono); font-size: 0.85em; }
hr { border-color: var(--tf-line); opacity: 1; }

:focus-visible {
  outline: 2px solid var(--tf-violet);
  outline-offset: 1px;
  border-radius: 4px;
}

.tf-icon {
  width: 16px;
  height: 16px;
  flex: 0 0 auto;
  stroke-width: 1.75;
  vertical-align: -3px;
}
.tf-icon-sm { width: 14px; height: 14px; }
.tf-icon-lg { width: 19px; height: 19px; }

.tf-muted { color: var(--tf-ink-2); }
.tf-dim { color: var(--tf-ink-3); }
.tf-label {
  font-size: 0.7857rem; /* 11px */
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--tf-ink-2);
  margin-bottom: 0.3rem;
}
.tf-eyebrow {
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
  letter-spacing: 0.01em;
}

/* --------------------------------------------------------------- shell ---- */

.tf-app-frame { display: flex; height: 100vh; overflow: hidden; background: var(--tf-canvas); }
body.tf-drawer-open { overflow: hidden; }

.tf-stage {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

/* Icon rail */
.tf-rail {
  width: var(--tf-rail-w);
  flex: 0 0 var(--tf-rail-w);
  background: var(--tf-rail);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 10px 0 8px;
  gap: 2px;
  position: sticky;
  top: 0;
  height: 100vh;
  /* Visible overflow painted the last items over the sidebar foot — Sign
     out sat under Settings the moment the rail gained another destination. */
  overflow-x: hidden;
  overflow-y: auto;
}

.tf-rail-logo {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  overflow: hidden;
  display: block;
  margin-bottom: 10px;
  background: #0a2a5a;
  box-shadow: 0 2px 8px rgba(10, 30, 70, 0.45);
}
.tf-rail-logo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 32%;
  display: block;
}
.tf-rail-logo:hover { filter: brightness(1.08); }

.tf-rail-item {
  width: 52px;
  padding: 7px 2px 5px;
  border: 0;
  background: none;
  border-radius: var(--tf-radius);
  color: var(--tf-rail-fg);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  font-size: 0.6429rem; /* 9px */
  font-weight: 500;
  letter-spacing: 0.005em;
  text-align: center;
  transition: background var(--tf-ease), color var(--tf-ease);
}
.tf-rail-item span { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-rail-item:hover { background: var(--tf-rail-hover); color: #fff; text-decoration: none; }
.tf-rail-item.is-active { background: var(--tf-rail-hover); color: #fff; }
.tf-rail-item.is-active { background: var(--tf-nav); color: #fff; }
.tf-rail-item.is-active .tf-icon { color: var(--tf-violet-light); }
.tf-rail-spacer { flex: 1; }
.tf-rail-divider { width: 26px; height: 1px; background: rgba(255, 255, 255, 0.10); margin: 6px 0; }

/* Sidebar */
.tf-side {
  background: var(--tf-nav);
  color: var(--tf-nav-fg);
  --bs-offcanvas-bg: var(--tf-nav);
  --bs-offcanvas-color: var(--tf-nav-fg);
  --bs-offcanvas-width: min(300px, 86vw);
}
@media (min-width: 992px) {
  .tf-side {
    width: var(--tf-side-w);
    flex: 0 0 var(--tf-side-w);
    height: 100vh;
    display: flex;
    flex-direction: column;
  }
  /* Bootstrap clears the offcanvas background above the breakpoint. */
  .tf-side.offcanvas-lg { background-color: var(--tf-nav) !important; }
  [data-theme="light"] .tf-side.offcanvas-lg { border-right: 1px solid var(--tf-line); }
}
.tf-side-inner { display: flex; flex-direction: column; height: 100%; min-height: 0; }

.tf-ws-row { display: flex; align-items: center; gap: 4px; padding: 8px 8px 4px; }
.tf-ws {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  padding: 8px 10px;
  border: 0;
  background: none;
  border-radius: var(--tf-radius);
  color: var(--tf-nav-fg-strong);
  font-weight: 600;
  font-size: 0.9286rem;
  text-align: left;
  transition: background var(--tf-ease);
}
.tf-ws { color: var(--tf-nav-fg-strong); }
.tf-ws:hover { background: var(--tf-nav-hover); color: var(--tf-nav-fg-strong); }
.tf-ws::after { display: none; }
.tf-ws-mark {
  width: 22px;
  height: 22px;
  border-radius: var(--tf-radius-sm);
  background: var(--tf-violet);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: 0.7857rem;
  font-weight: 700;
  flex: 0 0 auto;
}
/* ------ github links ------ */
.tf-gh-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; }
.tf-gh-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--tf-line);
  min-width: 0;
}
.tf-gh-row:last-child { border-bottom: 0; }
.tf-gh-kind { color: var(--tf-ink-3); display: inline-flex; flex: 0 0 auto; }
.tf-gh-title {
  color: var(--tf-ink);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
.tf-gh-title:hover { color: var(--tf-violet); text-decoration: underline; }
.tf-gh-side { margin-left: auto; display: inline-flex; align-items: center; gap: 6px; flex: 0 0 auto; }

/* Where a batch's own branch and pull request are named on a task in it: the
   task's own rows first, then a line saying whose these are. */
.tf-gh-from {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--tf-line);
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
}
.tf-gh-from:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.tf-gh-from .tf-icon { color: var(--tf-violet); flex: none; }
.tf-gh-glyph { display: inline-flex; }
.tf-gh-row.is-gone .tf-gh-title { text-decoration: line-through; color: var(--tf-ink-3); }
/* Where a rewritten commit went. Quiet, because the row it sits on is the one
   somebody was looking for and this is only the forwarding address. */
.tf-gh-landed {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
}
.tf-gh-landed a { color: var(--tf-ink-2); font-family: var(--tf-mono); }
.tf-gh-landed a:hover { color: var(--tf-violet-hover); }
.tf-gh-unlink { opacity: 0; transition: opacity var(--tf-ease); }
.tf-gh-row:hover .tf-gh-unlink, .tf-gh-unlink:focus-visible { opacity: 1; }
.tf-gh-add { display: flex; gap: 8px; margin-top: 10px; }
.tf-gh-add input { flex: 1; min-width: 0; }

.tf-ws-icon { background: none; object-fit: cover; }
.tf-ws-chev { margin-left: auto; color: var(--tf-nav-fg-dim); flex: 0 0 auto; }
.tf-ws-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tf-side-search { padding: 4px 12px 10px; }
.tf-side-search .tf-search-wrap { position: relative; }
.tf-side-search .tf-icon {
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--tf-nav-fg-dim);
  pointer-events: none;
}
.tf-side-search input {
  height: 32px;
  padding-left: 30px;
  font-size: 0.9286rem;
  background: var(--tf-nav-field);
  border-color: var(--tf-nav-line);
  color: var(--tf-nav-fg);
}
.tf-side-search input::placeholder { color: var(--tf-nav-fg-dim); }
.tf-side-search input:focus {
  background: var(--tf-nav-field-focus);
  border-color: var(--tf-violet);
  box-shadow: 0 0 0 3px rgba(123, 104, 238, 0.25);
  color: var(--tf-nav-fg-strong);
}

.tf-side-scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 0 8px 8px; }
.tf-side-foot { border-top: 1px solid var(--tf-nav-line); padding: 8px; }

.tf-side-copy {
  margin: 6px 0 2px;
  padding: 0 8px;
  font-size: 0.6786rem;
  line-height: 1.3;
  color: var(--tf-nav-fg-dim);
  opacity: 0.8;
}

.tf-side-head {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 10px 8px 4px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-nav-fg-dim);
}
.tf-side-head .tf-side-add { margin-left: auto; }

.tf-side-link {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  background: none;
  border-radius: var(--tf-radius-sm);
  color: var(--tf-nav-fg);
  font-size: 0.9286rem;
  font-weight: 500;
  text-align: left;
  transition: background var(--tf-ease), color var(--tf-ease);
}
.tf-side-link:hover { background: var(--tf-nav-hover); color: var(--tf-nav-fg-strong); text-decoration: none; }
.tf-side-link .tf-icon { color: var(--tf-nav-fg-dim); }
.tf-side-link.is-active { background: var(--tf-nav-active); color: var(--tf-nav-fg-strong); font-weight: 600; }
.tf-side-link.is-active .tf-icon { color: var(--tf-violet-light); }
.tf-side-link .tf-count {
  margin-left: auto;
  font-size: 0.7857rem;
  font-weight: 500;
  color: var(--tf-nav-fg-dim);
  font-variant-numeric: tabular-nums;
}
/* Your share of a list reads ahead of the total it is part of, so a glance
   down the tree finds the work waiting on you rather than the size of each
   list. */
.tf-side-link .tf-count-mine { color: var(--tf-nav-fg-strong); font-weight: 600; }
.tf-side-note { margin-left: auto; font-size: 0.7143rem; color: var(--tf-nav-fg-dim); }
/* A number in the nav is a claim that something is waiting, so it only ever
   appears when something is. */
.tf-side-badge {
  margin-left: auto;
  min-width: 18px;
  padding: 1px 6px;
  border-radius: 999px;
  background: var(--tf-violet);
  color: #fff;
  font-size: 0.7143rem;
  font-weight: 600;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.tf-side-add {
  border: 0;
  background: none;
  color: var(--tf-nav-fg-dim);
  padding: 2px;
  border-radius: 4px;
  line-height: 0;
  opacity: 0;
  transition: opacity var(--tf-ease), background var(--tf-ease);
}
.tf-side-add:hover { background: var(--tf-nav-hover); color: var(--tf-nav-fg-strong); }
.tf-side-head:hover .tf-side-add,
.tf-tree-summary:hover .tf-side-add,
.tf-side-add:focus-visible { opacity: 1; }

/* Project tree */
.tf-tree { margin-bottom: 2px; }
.tf-tree-summary {
  list-style: none;
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 6px 8px;
  border-radius: var(--tf-radius-sm);
  cursor: pointer;
  font-size: 0.9286rem;
  font-weight: 500;
  color: var(--tf-nav-fg);
  transition: background var(--tf-ease);
}
.tf-tree-summary::-webkit-details-marker { display: none; }
.tf-tree-summary:hover { background: var(--tf-nav-hover); color: var(--tf-nav-fg-strong); }
.tf-tree-summary .tf-icon { color: var(--tf-nav-fg-dim); }
.tf-tree-chev { color: var(--tf-nav-fg-dim); transition: transform var(--tf-ease); }
.tf-tree[open] > .tf-tree-summary .tf-tree-chev { transform: rotate(90deg); }
.tf-tree-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-tree-kids { padding-left: 14px; border-left: 1px solid var(--tf-nav-line); margin-left: 15px; }
.tf-tree-kids .tf-side-link { font-weight: 400; }
.tf-tree-kids .tf-side-link.is-active { font-weight: 600; }
.tf-dot {
  display: inline-block;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--tf-c, var(--tf-ink-3));
  flex: 0 0 auto;
}
.tf-side-empty { padding: 6px 9px; color: var(--tf-nav-fg-dim); font-size: 0.8571rem; }

/* Topbar */
.tf-topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--tf-topbar-h);
  flex: 0 0 var(--tf-topbar-h);
  padding: 0 12px 0 14px;
  background: var(--tf-canvas);
  border-bottom: 1px solid var(--tf-line);
}
.tf-crumbs {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  font-size: 0.9286rem;
}
.tf-crumb { color: var(--tf-ink-2); white-space: nowrap; }
.tf-crumb:hover { color: var(--tf-ink); text-decoration: none; }
/* A separator drawn in the border colour all but vanished between two words.
   The dim ink is a step short of the crumbs themselves, so the chevrons read
   as punctuation without competing with what they separate. */
.tf-crumb-sep { color: var(--tf-ink-3); }
.tf-crumb-now {
  font-weight: 600;
  color: var(--tf-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-user-btn {
  display: flex;
  align-items: center;
  gap: 7px;
  border: 0;
  background: none;
  padding: 4px 6px;
  border-radius: var(--tf-radius-sm);
  color: var(--tf-ink);
  font-size: 0.9286rem;
  font-weight: 500;
}
.tf-user-btn:hover { background: var(--tf-hover); }
.tf-user-btn::after { display: none; }

.tf-main { flex: 1; min-width: 0; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; }
.tf-main-flush { overflow: hidden; }
.tf-page-pad { padding: 22px 26px 48px; }
.tf-page-narrow { max-width: 44rem; }
.tf-page-mid { max-width: 66rem; }
/* Settings pages whose first card is a table: the rows need the room. */
.tf-page-wide { max-width: 82rem; }

.tf-page-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 18px;
}
.tf-page-head h1 { margin: 0; }
.tf-page-sub { color: var(--tf-ink-2); margin: 4px 0 0; max-width: 46rem; }

/* -------------------------------------------------------------- buttons --- */

.btn {
  --bs-btn-font-size: 0.9286rem;
  --bs-btn-font-weight: 500;
  --bs-btn-border-radius: var(--tf-radius);
  --bs-btn-padding-y: 0.375rem;
  --bs-btn-padding-x: 0.75rem;
  transition: background var(--tf-ease), border-color var(--tf-ease), color var(--tf-ease);
}
.btn-sm { --bs-btn-font-size: 0.8571rem; --bs-btn-padding-y: 0.25rem; --bs-btn-padding-x: 0.6rem; }

.btn-primary {
  --bs-btn-bg: var(--tf-violet);
  --bs-btn-border-color: var(--tf-violet);
  --bs-btn-hover-bg: var(--tf-violet-hover);
  --bs-btn-hover-border-color: var(--tf-violet-hover);
  --bs-btn-active-bg: var(--tf-violet-press);
  --bs-btn-active-border-color: var(--tf-violet-press);
  --bs-btn-disabled-bg: var(--tf-violet-tint-2);
  --bs-btn-disabled-border-color: var(--tf-violet-tint-2);
  --bs-btn-font-weight: 600;
  box-shadow: var(--tf-shadow-1);
}

.btn-outline-secondary,
.tf-btn-quiet {
  --bs-btn-color: var(--tf-ink);
  --bs-btn-border-color: var(--tf-line);
  --bs-btn-bg: var(--tf-canvas);
  --bs-btn-hover-color: var(--tf-ink);
  --bs-btn-hover-bg: var(--tf-hover);
  --bs-btn-hover-border-color: #dcdee5;
  --bs-btn-active-color: var(--tf-ink);
  --bs-btn-active-bg: var(--tf-line-soft);
  --bs-btn-active-border-color: #dcdee5;
}
.tf-btn-quiet {
  border: 1px solid var(--bs-btn-border-color);
  background: var(--bs-btn-bg);
  color: var(--bs-btn-color);
}

.btn-outline-danger {
  --bs-btn-color: var(--tf-red);
  --bs-btn-border-color: #f3cdce;
  --bs-btn-hover-bg: var(--tf-red);
  --bs-btn-hover-border-color: var(--tf-red);
  --bs-btn-hover-color: #fff;
}
.btn-link { --bs-btn-color: var(--tf-ink-2); --bs-btn-hover-color: var(--tf-ink); text-decoration: none; }

/* Icon-only button */
.tf-icon-btn {
  display: inline-grid;
  place-items: center;
  width: 28px;
  height: 28px;
  border: 0;
  background: none;
  border-radius: var(--tf-radius-sm);
  color: var(--tf-ink-2);
  transition: background var(--tf-ease), color var(--tf-ease);
}
.tf-icon-btn:hover { background: var(--tf-hover); color: var(--tf-ink); }
/* At the end of the list there is nowhere further to step. The button stays
   where it is rather than disappearing, so the pair does not shuffle. */
.tf-icon-btn:disabled { opacity: 0.35; cursor: default; }
.tf-icon-btn:disabled:hover { background: none; color: inherit; }

/* Toolbar chip */
.tf-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 30px;
  padding: 0 9px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  background: var(--tf-canvas);
  color: var(--tf-ink);
  font-size: 0.8571rem;
  font-weight: 500;
  white-space: nowrap;
  transition: background var(--tf-ease), border-color var(--tf-ease);
}
.tf-chip:hover { background: var(--tf-hover); color: var(--tf-ink); text-decoration: none; }
.tf-chip::after { display: none; }
.tf-chip .tf-icon { color: var(--tf-ink-2); }
.tf-chip.is-on {
  background: var(--tf-violet-tint);
  border-color: var(--tf-violet-tint-2);
  color: var(--tf-violet-press);
}
.tf-chip.is-on .tf-icon { color: var(--tf-violet-hover); }
.tf-chip-key { color: var(--tf-ink-3); font-weight: 500; }

/* ---------------------------------------------------------------- forms --- */

.form-control, .form-select {
  font-size: 0.9286rem;
  border-radius: var(--tf-radius-sm);
  border-color: var(--tf-line);
  color: var(--tf-ink);
  padding: 0.375rem 0.6rem;
  box-shadow: none;
  transition: border-color var(--tf-ease), box-shadow var(--tf-ease);
}
.form-control::placeholder { color: var(--tf-ink-3); }
.form-control:focus, .form-select:focus {
  border-color: var(--tf-violet);
  box-shadow: 0 0 0 3px var(--tf-violet-tint);
}
.form-control-sm, .form-select-sm { font-size: 0.8571rem; border-radius: var(--tf-radius-sm); }
.form-label { font-size: 0.8571rem; font-weight: 500; color: var(--tf-ink-2); margin-bottom: 0.25rem; }
.form-text { font-size: 0.8571rem; color: var(--tf-ink-3); }
.form-check-input { border-color: #ccd0d8; }
.form-check-input:checked { background-color: var(--tf-violet); border-color: var(--tf-violet); }
.form-check-input:focus { border-color: var(--tf-violet); box-shadow: 0 0 0 3px var(--tf-violet-tint); }
.form-check-label { font-size: 0.9286rem; }
.form-control-color { width: 3rem; padding: 0.25rem; }

.dropdown-menu {
  --bs-dropdown-font-size: 0.9286rem;
  --bs-dropdown-border-color: var(--tf-line);
  --bs-dropdown-border-radius: var(--tf-radius);
  --bs-dropdown-link-hover-bg: var(--tf-hover);
  --bs-dropdown-link-active-bg: var(--tf-violet-tint);
  --bs-dropdown-link-active-color: var(--tf-violet-press);
  --bs-dropdown-padding-y: 0.3rem;
  --bs-dropdown-item-padding-y: 0.35rem;
  --bs-dropdown-item-padding-x: 0.7rem;
  box-shadow: var(--tf-shadow-2);
  min-width: 12rem;
}
.dropdown-menu .dropdown-item { border-radius: var(--tf-radius-sm); width: auto; }
.dropdown-menu form { margin: 0; }
.dropdown-header {
  font-size: 0.7857rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--tf-ink-3);
  font-weight: 600;
}

.alert {
  --bs-alert-border-radius: var(--tf-radius);
  font-size: 0.9286rem;
  border-color: var(--tf-line);
  box-shadow: var(--tf-shadow-1);
}
.alert-success { background: #eefaf3; color: #1b6e46; border-color: #cceedd; }
.alert-danger { background: var(--tf-red-tint); color: #a3282c; border-color: #f6d5d6; }
.alert-warning { background: #fdf6e7; color: #8a5c07; border-color: #f5e4bc; }

/* Flash toast */
.tf-toasts {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 22px;
  margin: 0 auto;
  z-index: 60;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  width: max-content;
  max-width: min(30rem, 92vw);
  pointer-events: none;
}
.tf-toast {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 14px;
  border-radius: 10px;
  background: #23262e;
  color: #f4f5f7;
  font-size: 0.9286rem;
  box-shadow: var(--tf-shadow-3);
  animation: tf-rise 180ms cubic-bezier(0.2, 0, 0.2, 1);
}
.tf-toast .tf-icon { flex: 0 0 auto; }
.tf-toast-ok .tf-icon { color: #6ee7a8; }
.tf-toast-bad .tf-icon { color: #ff9ea1; }
@keyframes tf-rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }

/* ---------------------------------------------------------------- cards --- */

.tf-card {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
  box-shadow: var(--tf-shadow-1);
}
.tf-card-head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--tf-line);
}
.tf-card-head h2 { font-size: 0.9286rem; font-weight: 600; margin: 0; }
.tf-card-body { padding: 16px; }
.tf-empty { padding: 34px 20px; text-align: center; color: var(--tf-ink-2); }
.tf-empty-title { font-weight: 600; color: var(--tf-ink); margin-bottom: 3px; }

/* Plain data table (settings screens) */
.tf-table { width: 100%; margin: 0; }
.tf-table th {
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
  padding: 9px 16px;
  border-bottom: 1px solid var(--tf-line);
  white-space: nowrap;
}
.tf-table td {
  padding: 10px 16px;
  border-bottom: 1px solid var(--tf-line-soft);
  vertical-align: middle;
}
.tf-table tbody tr:last-child td { border-bottom: 0; }
.tf-table tbody tr:hover td { background: var(--tf-row-hover); }
.tf-table .tf-cell-count { width: 11rem; }
.tf-actions { display: flex; gap: 6px; justify-content: flex-end; align-items: center; }
.tf-actions form { margin: 0; }

/* Per-member permissions, folded away until somebody wants them */
/* A fold with a one-line summary. Named after the permission boxes it was
   built for and since borrowed by anything else that wants "the usual answer
   is fine, open this if it is not" — the scheduling modal's start time, say —
   so the general name answers to the same rules. */
.tf-perms, .tf-disclose { min-width: 17rem; }
.tf-perms-summary, .tf-disclose-summary {
  display: flex;
  align-items: center;
  gap: 5px;
  cursor: pointer;
  list-style: none;
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
  padding: 2px 0;
}
.tf-perms-summary::-webkit-details-marker,
.tf-disclose-summary::-webkit-details-marker { display: none; }
.tf-perms-summary:hover, .tf-disclose-summary:hover { color: var(--tf-ink); }
.tf-perms-summary svg,
.tf-disclose-summary svg { flex: none; transition: transform var(--tf-ease); }
.tf-perms[open] > .tf-perms-summary svg,
.tf-disclose[open] > .tf-disclose-summary svg { transform: rotate(90deg); }
.tf-perms-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(13.5rem, 100%), 1fr));
  gap: 6px 14px;
  margin-top: 9px;
  padding: 10px 12px;
  border: 1px solid var(--tf-line);
  border-radius: 8px;
}
.tf-perm { display: flex; align-items: flex-start; gap: 8px; cursor: pointer; }
.tf-perm .form-check-input { margin: 2px 0 0; flex: none; }
.tf-perm-label { display: block; font-size: 0.8571rem; line-height: 1.3; }
.tf-perm-hint { display: block; font-size: 0.7857rem; color: var(--tf-ink-3); line-height: 1.3; }


/* ------------------------------------------------------------ task types --- */

.tf-type {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  white-space: nowrap;
}
.tf-type i {
  color: var(--tf-c, var(--tf-ink-3));
  font-size: 0.8571rem;
  width: 14px;
  text-align: center;
}
.tf-type-bug { color: var(--tf-red); font-weight: 500; }
.dropdown-item .fa-solid, .tf-chip .fa-solid { width: 14px; text-align: center; }

/* ------------------------------------------------------------- sub-tasks --- */

.tf-subtask-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--tf-radius-sm);
  background: none;
  color: var(--tf-ink-2);
  transition: transform var(--tf-ease), color var(--tf-ease), background var(--tf-ease);
}
.tf-subtask-toggle:hover { background: var(--tf-violet-tint); color: var(--tf-violet-press); }
/* A folded caret is the only sign that a parent is holding work out of sight,
   so it is drawn heavier than an open one. On a board that starts folded it is
   the way in to everything below the top level. */
.tf-subtask-toggle.is-collapsed { transform: rotate(-90deg); color: var(--tf-ink); }

/* One chip, two carets: which one shows says which way the board starts. */
.tf-subtask-mode .tf-mode-open,
.tf-subtask-mode .tf-mode-fold { display: inline-flex; }
.tf-subtask-mode .tf-mode-fold,
.tf-subtask-mode.is-on .tf-mode-open { display: none; }
.tf-subtask-mode.is-on .tf-mode-fold { display: inline-flex; }
.tf-subtask-mark { display: inline-flex; color: #d5d9e0; }

.tf-cell-title.is-sub { padding-left: 18px; }
.tf-subrow > td:first-child { box-shadow: inset 2px 0 0 var(--tf-violet-tint-2); }
.tf-subrow .tf-task-name { font-weight: 400; }

.tf-subtask-count {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  height: 18px;
  padding: 0 6px 0 4px;
  border-radius: 999px;
  background: var(--tf-line-soft);
  color: var(--tf-ink-2);
  font-size: 0.7143rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

/* Epics. The bar is the epic's own count of the work it gathers from other
   lists; the chip on a row of that work names the epic it belongs to. */
.tf-epic-progress { display: inline-flex; align-items: center; gap: 6px; vertical-align: middle; }
.tf-epic-progress-track {
  position: relative;
  width: 64px;
  height: 6px;
  border-radius: 999px;
  background: var(--tf-line-soft);
  overflow: hidden;
}
.tf-epic-progress-fill { position: absolute; inset: 0 auto 0 0; background: #0f8a6c; border-radius: inherit; }
.tf-epic-progress-count { font-size: 0.7143rem; font-weight: 600; color: var(--tf-ink-2); font-variant-numeric: tabular-nums; }
.tf-epic-empty { font-size: 0.7857rem; }
.tf-section-head .tf-epic-progress { margin-left: auto; }
.tf-epic-pill { text-decoration: none; }
.tf-epic-pill:hover .tf-epic-progress-count { color: var(--tf-ink); }
.tf-epic-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 180px;
  height: 18px;
  padding: 0 7px 0 5px;
  border-radius: 999px;
  background: color-mix(in srgb, #0f8a6c 12%, transparent);
  color: var(--tf-ink-2);
  font-size: 0.7143rem;
  font-weight: 600;
  text-decoration: none;
}
.tf-epic-chip span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-epic-chip:hover { color: var(--tf-ink); }

.tf-add-row.is-sub .tf-add-form input[type="text"] { padding-left: 64px; font-size: 0.8571rem; }

.tf-subtask-list { list-style: none; margin: 0 0 10px; padding: 0; }
.tf-subtask {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 2px;
  border-bottom: 1px solid var(--tf-line-soft);
}
.tf-subtask:last-child { border-bottom: 0; }
.tf-subtask-tick { margin: 0; display: flex; }
.tf-tick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid #d5d9e0;
  border-radius: 6px;
  background: var(--tf-canvas);
  color: transparent;
  transition: all var(--tf-ease);
}
.tf-tick:hover { border-color: var(--tf-violet); color: var(--tf-violet-light); }
.tf-tick.is-on { background: var(--tf-green); border-color: var(--tf-green); color: #fff; }
.tf-subtask-name { color: var(--tf-ink); font-size: 0.9286rem; min-width: 0; flex: 1 1 auto; }
.tf-subtask-name:hover { color: var(--tf-violet-press); }
.tf-subtask.is-done .tf-subtask-name { color: var(--tf-ink-3); text-decoration: line-through; }
.tf-subtask-side { display: inline-flex; align-items: center; gap: 8px; margin-left: auto; flex: 0 0 auto; }
.tf-subtask-add { display: flex; align-items: center; gap: 8px; }
.tf-subtask-add input[type="text"] { flex: 1 1 auto; }

/* --------------------------------------------------------- agent marks --- */

.tf-avatar-preset { padding: 0; box-shadow: 0 0 0 1px rgba(15, 17, 21, 0.12); }
.tf-avatar-preset .tf-mark { display: block; line-height: 1; }

/* The mark picker on an agent token. Named for the mark and not for
   "preset", which the notification screen also has a picker of. */
.tf-mark-pick-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.tf-mark-pick {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 5px;
  width: 5.1rem;
  padding: 9px 4px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  font-size: 0.7857rem;
  color: var(--tf-ink-2);
  cursor: pointer;
  transition: border-color var(--tf-ease), background var(--tf-ease);
}
.tf-mark-pick:hover { border-color: var(--tf-violet-light); }
.tf-mark-pick:has(input:checked) {
  border-color: var(--tf-violet);
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  font-weight: 600;
}
.tf-mark-pick:has(input:focus-visible) { outline: 2px solid var(--tf-violet); outline-offset: 2px; }

/* ---------------------------------------------------------- badges/meta --- */

.tf-status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 9px 0 7px;
  border-radius: 999px;
  font-size: 0.7857rem;
  font-weight: 600;
  white-space: nowrap;
  background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 13%, var(--tf-chip-base));
  color: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 72%, var(--tf-chip-ink));
}
.tf-status::before {
  content: "";
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--tf-c, var(--tf-ink-3));
}
/* Several values in one cell: one line, a size down, and clipped at the column
   edge rather than pushing the row taller. */
.tf-cf-vals {
  display: inline-flex;
  flex-wrap: nowrap;
  align-items: center;
  gap: 4px;
  max-width: 100%;
  overflow: hidden;
}
.tf-cf-vals .tf-status { min-width: 0; }
.tf-status.is-mini {
  height: 18px;
  gap: 4px;
  padding: 0 7px 0 6px;
  font-size: 0.7143rem;
}
.tf-status.is-mini::before { width: 5px; height: 5px; }
.tf-status-solid {
  display: inline-flex;
  align-items: center;
  height: 21px;
  padding: 0 8px;
  border-radius: var(--tf-radius-sm);
  background: var(--tf-c, var(--tf-ink-3));
  color: #fff;
  font-size: 0.7857rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
}

.tf-priority {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  white-space: nowrap;
}
.tf-priority .tf-icon { color: var(--tf-p, var(--tf-ink-3)); }
.tf-priority-urgent { --tf-p: var(--tf-red); color: var(--tf-red); font-weight: 600; }
.tf-priority-high { --tf-p: var(--tf-amber); color: #8a5c07; font-weight: 500; }
.tf-priority-normal { --tf-p: var(--tf-blue); }
.tf-priority-low { --tf-p: #9aa2ae; }
.tf-priority-none { --tf-p: #ccd0d8; color: var(--tf-ink-3); }

.tf-due { white-space: nowrap; display: inline-flex; align-items: center; gap: 5px; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-due.is-overdue { color: var(--tf-red); font-weight: 500; }
.tf-due.is-soon { color: #8a5c07; font-weight: 500; }
.tf-due-empty { color: var(--tf-ink-3); font-size: 0.8571rem; }
.tf-ref { font-family: var(--tf-mono); font-size: 0.7857rem; color: var(--tf-ink-3); white-space: nowrap; }

/* A reference and the button that copies it. The button is the quiet kind:
   present enough to find, faint enough that a column of them does not read as
   a column of buttons. */
.tf-ref-cell { display: inline-flex; align-items: center; gap: 2px; min-width: 0; }
.tf-ref-copy {
  flex: 0 0 auto;
  display: inline-grid;
  place-items: center;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--tf-ink-3);
  cursor: pointer;
  transition: opacity var(--tf-ease), background var(--tf-ease), color var(--tf-ease);
}
/* On a board it waits for the row to be hovered — a column of buttons down
   the page would read as a column of buttons. Everywhere else there is one of
   them and nothing to hover, so it simply shows. */
.tf-row .tf-ref-copy { opacity: 0; }
.tf-row:hover .tf-ref-copy,
.tf-row .tf-ref-copy:focus-visible,
.tf-row .tf-ref-copy.is-copied,
.tf-row .tf-ref-copy.is-copy-failed { opacity: 1; }
.tf-ref-copy:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-ref-copy .tf-copy-done { display: none; }
.tf-ref-copy.is-copied { color: var(--tf-green); }
.tf-ref-copy.is-copied .tf-copy-idle { display: none; }
.tf-ref-copy.is-copied .tf-copy-done { display: block; }
.tf-ref-copy.is-copy-failed { color: var(--tf-red); }

/* The same button with a word beside the glyph, for a string that is not six
   characters of monospace: it says what it will copy before the click and what
   it did after, in the place the pointer already is. */
.tf-copy-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 6px;
  margin-left: -6px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: var(--tf-ink-3);
  font: inherit;
  cursor: pointer;
  transition: background var(--tf-ease), color var(--tf-ease);
}
.tf-copy-btn:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-copy-btn .tf-copy-done { display: none; }
.tf-copy-btn.is-copied { color: var(--tf-green); }
.tf-copy-btn.is-copied .tf-copy-idle { display: none; }
.tf-copy-btn.is-copied .tf-copy-done { display: inline-flex; }
.tf-copy-btn.is-copy-failed { color: var(--tf-red); }

.tf-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  object-fit: cover;
  font-size: 0.6429rem;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: #fff;
  background: hsl(var(--tf-hue, 250), 45%, 52%);
  box-shadow: 0 0 0 2px var(--tf-canvas);
  flex: 0 0 auto;
}
.tf-avatar-empty {
  width: 22px;
  height: 22px;
  border: 1px dashed #d5d9e0;
  background: none;
  box-shadow: none;
}
.tf-assignees { display: inline-flex; align-items: center; padding-left: 5px; }
.tf-assignees .tf-avatar { margin-left: -5px; }
.tf-assignees-more {
  display: inline-grid;
  place-items: center;
  min-width: 22px;
  height: 22px;
  margin-left: -5px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--tf-line-soft);
  color: var(--tf-ink-2);
  font-size: 0.6429rem;
  font-weight: 700;
  box-shadow: 0 0 0 2px var(--tf-canvas);
}

/* ------------------------------------------------------------ list view --- */

.tf-board { display: flex; flex-direction: column; flex: 1; min-height: 0; }

.tf-board-head {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 20px 10px;
}
.tf-board-title { font-size: 1.375rem; font-weight: 650; letter-spacing: -0.022em; margin: 0; }
.tf-board-count {
  font-size: 0.8571rem;
  color: var(--tf-ink-3);
  font-variant-numeric: tabular-nums;
  padding-bottom: 2px;
}

.tf-toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 0 20px 12px;
  border-bottom: 1px solid var(--tf-line);
}
.tf-toolbar-right { margin-left: auto; display: flex; align-items: center; gap: 8px; }
.tf-toolbar .tf-search-wrap { position: relative; }
.tf-toolbar .tf-search-wrap .tf-icon {
  position: absolute;
  left: 9px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--tf-ink-3);
  pointer-events: none;
}
.tf-toolbar .tf-search-wrap input {
  height: 30px;
  width: 210px;
  padding-left: 30px;
  font-size: 0.8571rem;
  border-radius: var(--tf-radius-sm);
}
.tf-col-menu { padding: 6px; min-width: 13rem; }
.tf-col-menu label {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 8px;
  border-radius: var(--tf-radius-sm);
  font-size: 0.9286rem;
  cursor: pointer;
}
.tf-col-menu label:hover { background: var(--tf-hover); }

/* The filter panel: one row per column, the whole of what a list can be
   narrowed by. It is a menu rather than a bar because most of these are asked
   after rarely, and a bar of a dozen controls is a bar nobody reads. */
.tf-filter-menu { padding: 8px; width: 20rem; max-height: 70vh; overflow-y: auto; }
.tf-filter-rows { display: flex; flex-direction: column; gap: 4px; }
.tf-filter-row {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  align-items: center;
  gap: 8px;
  margin: 0;
  padding: 2px 4px;
  cursor: pointer;
}
.tf-filter-name {
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-filter-control { min-width: 0; }
.tf-filter-menu .form-control, .tf-filter-menu .form-select {
  height: 30px;
  width: 100%;
  font-size: 0.8571rem;
  padding: 0 0.55rem;
  border-radius: var(--tf-radius-sm);
}
.tf-filter-menu .form-select { padding-right: 1.75rem; }
/* This workspace's own columns, which are nobody else's. */
.tf-filter-sep {
  margin: 8px 4px 2px;
  padding-top: 8px;
  border-top: 1px solid var(--tf-line);
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-filter-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 10px;
  padding-top: 8px;
  border-top: 1px solid var(--tf-line);
}
/* How many columns are narrowing the board, on the button that opens them. */
.tf-filter-count {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 17px;
  height: 17px;
  padding: 0 4px;
  border-radius: 9px;
  background: var(--tf-violet);
  color: #fff;
  font-size: 0.7143rem;
  font-variant-numeric: tabular-nums;
}
/* A filter that is on, said out loud beside the chips — half of them live
   behind the menu, and a filter you cannot see is one you cannot take off. */
.tf-filter-chip { padding-right: 4px; }
.tf-filter-chip-value { max-width: 11rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-filter-chip-x {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 18px;
  height: 18px;
  border-radius: 4px;
  color: inherit;
  opacity: 0.7;
}
.tf-filter-chip-x:hover { background: var(--tf-violet-tint-2); color: inherit; opacity: 1; }

.tf-table-wrap { flex: 1; overflow: auto; min-height: 0; }
.tf-grid {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0;
  background: var(--tf-canvas);
  font-size: 0.9286rem;
}
.tf-grid thead th {
  position: sticky;
  top: 0;
  z-index: 3;
  background: var(--tf-canvas);
  border-bottom: 1px solid var(--tf-line);
  padding: 0 10px;
  height: 32px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--tf-ink-3);
  text-align: left;
  white-space: nowrap;
  user-select: none;
}
/* No `position: relative` here — it would beat the sticky above and drop the
   header out of the top of the board. Sticky is already a positioned value,
   so the resize grip has its containing block either way. */
.tf-grid thead th[data-col] { cursor: grab; }
.tf-grid .tf-col-label { display: block; overflow: hidden; text-overflow: ellipsis; }

/* The edge of a header, widened into something a pointer can find. Sitting
   half outside its own cell puts the grip on the line between two columns,
   which is where a hand reaches for it. */
.tf-col-resize {
  position: absolute;
  top: 0;
  right: -3px;
  z-index: 1;
  width: 7px;
  height: 100%;
  cursor: col-resize;
}
.tf-col-resize::after {
  content: "";
  position: absolute;
  top: 6px;
  bottom: 6px;
  left: 3px;
  width: 1px;
  background: var(--tf-line);
  opacity: 0;
  transition: opacity var(--tf-ease), background var(--tf-ease);
}
.tf-grid thead th:hover .tf-col-resize::after { opacity: 1; }
.tf-col-resize:hover::after { background: var(--tf-violet); opacity: 1; }
/* While a column is being dragged wider the pointer leaves the grip behind,
   so the cursor is held on the whole page rather than on the handle. */
body.is-resizing-column { cursor: col-resize; user-select: none; }
body.is-resizing-column .tf-col-resize::after { background: var(--tf-violet); opacity: 1; }

.tf-grid th[data-col="title"] { min-width: 22rem; }
.tf-grid th[data-col="type"] { width: 7.5rem; }
.tf-grid th[data-col="ref"] { width: 8rem; }
.tf-grid th[data-col="assignees"] { width: 8rem; }
.tf-grid th[data-col="status"] { width: 9rem; }
.tf-grid th[data-col="priority"] { width: 7.5rem; }
.tf-grid th[data-col="due_date"] { width: 8rem; }
.tf-grid th[data-col="list"] { width: 10rem; }
.tf-grid th[data-col^="cf_"] { width: 8rem; }
.tf-col-check, .tf-col-handle { width: 26px; padding: 0 !important; }
.tf-col-check { padding-left: 12px !important; }

/* Group header */
.tf-group-row td {
  padding: 0;
  border-bottom: 1px solid var(--tf-line);
  background: var(--tf-canvas);
}
.tf-group {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 9px 12px 8px;
  background: var(--tf-canvas);
}
.tf-group-toggle {
  border: 0;
  background: none;
  padding: 2px;
  border-radius: 4px;
  color: var(--tf-ink-3);
  line-height: 0;
  transition: transform var(--tf-ease), background var(--tf-ease);
}
.tf-group-toggle:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-group-toggle .tf-icon { transition: transform var(--tf-ease); }
/* Picking a whole heading. Quiet until the heading is hovered, the way the
   add-a-task button beside it is, and lit while the group is wholly picked. */
.tf-group-pick {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  margin-left: -4px;
  padding: 0;
  border: 1px solid var(--tf-line);
  border-radius: 5px;
  background: var(--tf-canvas);
  color: var(--tf-ink-3);
  line-height: 0;
  opacity: 0;
  transition: opacity var(--tf-ease), background var(--tf-ease), color var(--tf-ease), border-color var(--tf-ease);
}
.tf-group:hover .tf-group-pick,
.tf-group-pick:focus-visible,
.tf-group-pick.is-on { opacity: 1; }
.tf-group-pick:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-group-pick.is-on {
  background: var(--tf-violet-tint);
  border-color: var(--tf-violet-tint-2);
  color: var(--tf-violet-press);
}
.tf-group-count {
  font-size: 0.8571rem;
  color: var(--tf-ink-3);
  font-variant-numeric: tabular-nums;
}
.tf-group-add {
  margin-left: 4px;
  opacity: 0;
  transition: opacity var(--tf-ease);
}
.tf-group:hover .tf-group-add, .tf-group-add:focus-visible { opacity: 1; }
tbody.is-collapsed tr:not(.tf-group-row) { display: none; }
tbody.is-collapsed .tf-group-toggle .tf-icon { transform: rotate(-90deg); }
tbody + tbody .tf-group-row td { border-top: 1px solid var(--tf-line-soft); }

/* Rows */
.tf-row td {
  height: var(--tf-row-h);
  padding: 0 10px;
  border-bottom: 1px solid var(--tf-line-soft);
  vertical-align: middle;
  background: var(--tf-canvas);
  transition: background var(--tf-ease);
}
.tf-row:hover td { background: var(--tf-row-hover); }
.tf-row.is-selected td { background: var(--tf-violet-tint); }
.tf-row .form-check-input { opacity: 0; transition: opacity var(--tf-ease); }
.tf-row:hover .form-check-input,
.tf-row .form-check-input:checked,
.tf-row .form-check-input:focus-visible { opacity: 1; }
.tf-drag {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 24px;
  cursor: grab;
  color: #c4c9d2;
  opacity: 0;
  transition: opacity var(--tf-ease);
}
.tf-row:hover .tf-drag { opacity: 1; }
.tf-drag:active { cursor: grabbing; }
.sortable-ghost td { background: var(--tf-violet-tint) !important; }
.sortable-chosen { box-shadow: var(--tf-shadow-2); }

.tf-cell-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
.tf-task-name {
  color: var(--tf-ink);
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-task-name:hover { color: var(--tf-violet-press); text-decoration: none; }
.tf-row.is-done .tf-task-name { color: var(--tf-ink-2); }
/* Work that was called off is quieter still, and struck through: a muted title
   alone reads as finished, which is the one thing this is not. */
.tf-row.is-cancelled .tf-task-name {
  color: var(--tf-ink-3);
  text-decoration: line-through;
  text-decoration-color: var(--tf-ink-3);
}
.tf-task-meta {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
  flex: 0 0 auto;
}
.tf-row-open {
  margin-left: auto;
  opacity: 0;
  flex: 0 0 auto;
  transition: opacity var(--tf-ease);
}
/* The version a row shipped in, against the ID column rather than in the
   title's tag run. `margin-left: 0` because the expand button already ate
   the auto; this just sits after it, next to the next column. */
.tf-row-releases {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  flex: 0 0 auto;
}
.tf-release-chip {
  max-width: 7.5rem;
  font-family: var(--tf-mono);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}
.tf-release-chip:hover { text-decoration: none; filter: brightness(0.96); }
.tf-row:hover .tf-row-open, .tf-row-open:focus-visible { opacity: 1; }
.tf-row-where { font-size: 0.7857rem; color: var(--tf-ink-3); }

.tf-cell-edit { cursor: pointer; }
.tf-cell-edit:hover { box-shadow: inset 0 0 0 1px var(--tf-line); border-radius: 4px; }
.tf-cell-empty { color: var(--tf-ink-3); }

/* A dropdown value fills its column instead of hugging its text. The colour is
   the point of a dropdown field, and a six-character pill floating in a wide
   column was easy to miss entirely. Scoped to the custom-field cells so the
   status pill, tags and every other badge keep the shape they had. Block
   rather than flex so a long value can still end in an ellipsis. */
.tf-grid td[data-col^="cf_"] .tf-status {
  display: block;
  width: 100%;
  height: 24px;
  line-height: 24px;
  padding: 0 9px;
  border-radius: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, var(--tf-chip-base));
}
.tf-inline-input {
  width: 100% !important;
  min-width: 0 !important;
  height: 26px;
  padding: 0 6px;
  font-size: 0.8571rem;
  line-height: 1.2;
  box-sizing: border-box;
}
.tf-assignee-editor {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow-2);
  padding: 6px;
  min-width: 190px;
}
.tf-check-line {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 4px 6px;
  border-radius: var(--tf-radius-sm);
  font-size: 0.9286rem;
  cursor: pointer;
}
.tf-check-line:hover { background: var(--tf-hover); }

/* Inline add row */
.tf-add-row td { padding: 0; border-bottom: 1px solid var(--tf-line-soft); background: var(--tf-canvas); }
.tf-add-form { display: flex; align-items: center; }
.tf-add-form input[type="text"] {
  border: 0;
  border-radius: 0;
  background: none;
  height: 34px;
  padding-left: 46px;
  font-size: 0.9286rem;
  box-shadow: none;
  color: var(--tf-ink);
}
.tf-add-form input[type="text"]::placeholder { color: var(--tf-ink-3); }
.tf-add-form input[type="text"]:focus { background: var(--tf-hover); box-shadow: none; }

.tf-empty-cell { padding: 44px 20px; text-align: center; color: var(--tf-ink-2); }

/* Bulk bar */
.tf-bulk-dock {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 20px;
  z-index: 30;
  display: flex;
  justify-content: center;
  padding: 0 12px;
  pointer-events: none;
}
.tf-bulk {
  pointer-events: auto;
  display: flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 8px 10px 8px 14px;
  border-radius: 12px;
  background: #23262e;
  color: #f4f5f7;
  box-shadow: var(--tf-shadow-3);
  /* Deliberately not scrolled. Assign is a menu drawn inside the bar, and a
     bar that scrolls in one axis clips the other as well — which left the
     menu cut off behind the bar with no way to reach it. A bar too wide for
     the window wraps onto a second line instead. */
  overflow: visible;
  flex-wrap: wrap;
  justify-content: center;
  row-gap: 6px;
  animation: tf-rise 160ms cubic-bezier(0.2, 0, 0.2, 1);
}
.tf-bulk-count { font-size: 0.8571rem; font-weight: 600; margin-right: 4px; white-space: nowrap; }
.tf-bulk .form-select {
  flex: 0 0 auto;
  height: 28px;
  padding: 0 1.6rem 0 0.5rem;
  font-size: 0.8571rem;
  background-color: #32363f;
  border-color: #434852;
  color: #f4f5f7;
  max-width: 8.5rem;
}
.tf-bulk .form-select:focus { border-color: var(--tf-violet); box-shadow: 0 0 0 3px rgba(123, 104, 238, 0.3); }
.tf-bulk .tf-bulk-btn {
  flex: 0 0 auto;
  height: 28px;
  padding: 0 9px;
  border: 0;
  border-radius: var(--tf-radius-sm);
  background: #32363f;
  color: #e9eaee;
  font-size: 0.8571rem;
  font-weight: 500;
  transition: background var(--tf-ease);
}
.tf-bulk .tf-bulk-btn:hover { background: #40454f; color: #fff; }
.tf-bulk .tf-bulk-btn.is-danger:hover { background: var(--tf-red); }
.tf-bulk-sep { width: 1px; height: 20px; background: #434852; margin: 0 2px; }

/* A list that has been put away. Still readable, plainly not in play. */
.tf-list-archived td:first-child a { color: var(--tf-ink-2); }
.tf-list-archived:hover td:first-child a { color: var(--tf-ink); }

/* --------------------------------------------------------------- drawer --- */

.tf-drawer-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(24, 27, 33, 0.32);
  z-index: 40;
  animation: tf-fade 140ms ease;
}
@keyframes tf-fade { from { opacity: 0; } to { opacity: 1; } }
.tf-drawer {
  position: fixed;
  top: 0;
  right: 0;
  width: min(760px, 100%);
  height: 100vh;
  background: var(--tf-canvas);
  z-index: 50;
  display: flex;
  flex-direction: column;
  box-shadow: var(--tf-shadow-3);
  animation: tf-slide 180ms cubic-bezier(0.2, 0, 0.2, 1);
}
@keyframes tf-slide { from { transform: translateX(24px); opacity: 0.4; } to { transform: none; opacity: 1; } }
.tf-drawer turbo-frame { display: flex; flex-direction: column; min-height: 0; flex: 1; }

.tf-panel { display: flex; flex-direction: column; min-height: 0; flex: 1; }
.tf-panel-top {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px 10px 16px;
  border-bottom: 1px solid var(--tf-line);
  flex: 0 0 auto;
}
.tf-panel-scroll { overflow-y: auto; padding: 16px 20px 28px; }
/* Stepping through the list behind the panel. Sits ahead of the crumbs, where
   the way back is, because this is the other way of moving about. */
.tf-panel-step { display: flex; align-items: center; gap: 2px; flex: 0 0 auto; }
.tf-panel-step[hidden] { display: none; }
.tf-panel-step-at { margin-left: 4px; font-size: 0.7857rem; color: var(--tf-ink-3); white-space: nowrap; }
/* A long list name, or a subtask's parent title, ran on under the buttons at
   the right and hid them. Each crumb stops at about thirty characters and
   trails off, and the buttons never give up their room. */
.tf-panel-top .tf-crumbs { flex: 1 1 auto; overflow: hidden; }
.tf-panel-top .tf-crumb { max-width: 26ch; overflow: hidden; text-overflow: ellipsis; }
.tf-panel-top .ms-auto { flex: 0 0 auto; }
.tf-panel-title {
  width: 100%;
  border: 0;
  padding: 2px 0 10px;
  font-size: 1.25rem;
  font-weight: 650;
  letter-spacing: -0.02em;
  color: var(--tf-ink);
  background: none;
  resize: none;
  overflow: hidden;
}
.tf-panel-title:focus { outline: none; box-shadow: none; }

/* The panel's sections, one at a time. The strip stays put while a tab
   scrolls, because the way back to the properties should not be a scroll up. */
.tf-panel-tabs { margin-top: 10px; }
.tf-tabs {
  position: sticky;
  top: -16px;
  z-index: 3;
  display: flex;
  gap: 2px;
  margin: 0 -20px 14px;
  padding: 6px 20px 0;
  background: var(--tf-canvas);
  border-bottom: 1px solid var(--tf-line);
}
.tf-tab {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 10px 8px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--tf-ease), border-color var(--tf-ease);
}
.tf-tab .tf-icon { color: var(--tf-ink-3); }
.tf-tab:hover { color: var(--tf-ink); }
.tf-tab.is-on { color: var(--tf-violet-press); border-bottom-color: var(--tf-violet); }
.tf-tab.is-on .tf-icon { color: var(--tf-violet); }
.tf-tab-count {
  min-width: 18px;
  padding: 0 5px;
  border-radius: 999px;
  background: var(--tf-hover);
  color: var(--tf-ink-2);
  font-size: 0.7143rem;
  font-variant-numeric: tabular-nums;
}
.tf-tab.is-on .tf-tab-count { background: var(--tf-violet-tint-2); color: var(--tf-violet-press); }
.tf-tab-dot {
  padding: 0 5px;
  border-radius: 999px;
  background: rgba(217, 140, 20, 0.16);
  color: var(--tf-amber);
  font-size: 0.7143rem;
  font-weight: 600;
}
.tf-pane[hidden] { display: none; }

/* The same strip on a full page rather than in the panel: nothing to sit
   sticky against and no gutter to bleed into, so it lies flat under the
   title. */
.tf-page-tabs > .tf-tabs {
  position: static;
  margin: 0 0 18px;
  padding: 0;
  background: none;
  overflow-x: auto;
  scrollbar-width: none;
}
.tf-page-tabs > .tf-tabs::-webkit-scrollbar { display: none; }
.tf-meta-grid {
  display: grid;
  grid-template-columns: 5.5rem 1fr;
  gap: 8px 12px;
  align-items: center;
  padding: 4px 0 14px;
  border-bottom: 1px solid var(--tf-line);
  margin-bottom: 16px;
}
.tf-meta-key {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}
.tf-meta-key .tf-icon { color: var(--tf-ink-3); }
.tf-meta-val .form-select, .tf-meta-val .form-control { height: 30px; max-width: 17rem; padding-top: 0; padding-bottom: 0; }
.tf-section { margin-bottom: 20px; }
.tf-section-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
  margin-bottom: 8px;
}
.tf-prose { color: var(--tf-ink); }
.tf-prose :last-child { margin-bottom: 0; }
.tf-prose pre {
  background: var(--tf-shell);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  padding: 10px 12px;
  overflow: auto;
}
.tf-prose code { background: var(--tf-violet-tint); padding: 1px 4px; border-radius: 4px; color: var(--tf-violet-press); }
.tf-prose pre code { background: none; padding: 0; color: inherit; }

.tf-comment { display: flex; gap: 10px; padding: 9px 0; }
.tf-comment-body { min-width: 0; flex: 1; }
.tf-comment-head { display: flex; align-items: baseline; gap: 7px; }
.tf-comment-who { font-weight: 600; font-size: 0.8571rem; }
.tf-comment-when { font-size: 0.7857rem; color: var(--tf-ink-3); }
.tf-comment-text { margin: 2px 0 0; }
.tf-comment-text p:last-child { margin-bottom: 0; }
/* Edit and delete sit together at the far end of the byline. Quiet, but always
   there: hiding them until the pointer arrives would put them out of reach of
   anyone reading on a touch screen. */
.tf-comment-acts { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.tf-comment-acts form { margin: 0; }
/* "Edited" is a footnote to what somebody said, not part of it. */
.tf-comment-edited {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  margin-top: 3px;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
}
.tf-edit-box { margin-top: 4px; }
.tf-comment-box { border: 1px solid var(--tf-line); border-radius: var(--tf-radius); padding: 8px; background: var(--tf-canvas); }
.tf-comment-box textarea { border: 0; box-shadow: none; padding: 2px 4px; resize: vertical; }
.tf-comment-box textarea:focus { box-shadow: none; }
.tf-comment-box-foot { display: flex; align-items: center; justify-content: flex-end; gap: 8px; padding-top: 6px; }
.tf-composer-hint { margin-right: auto; font-size: 0.7857rem; }

/* A question is a comment that somebody is still waiting on, so it is marked
   where the comment's own name is and carries its state under it. Answered, it
   goes quiet again — the thread should show what is outstanding, not shout
   about what has been settled. */
/* Both classes together for the specificity, not for decoration: `.tf-tl-entry`
   sets `padding: 9px 0` further down the sheet, and a bare `.tf-q` lost its
   left padding to that shorthand — which is what put the bar hard against the
   poster's face. The numbers still cancel (14 = 2 border + 12 padding), so a
   question's text sits in the same column as every other comment's.

   Open, it is the loudest thing in the thread, and says so the way a batch's
   open question does: the same solid amber bar, at the same width, over a wash
   that carries down the feed — a half-strength bar standing clear of the
   avatar turned out to be no mark at all. Answered, it goes quiet again, which
   is the base rule here: the thread should show what is outstanding, not shout
   about what has been settled. */
.tf-tl-entry.tf-q { border-left: 2px solid var(--tf-line); padding-left: 12px; margin-left: -14px; }
.tf-tl-entry.tf-q.is-open {
  border-left-color: var(--tf-amber);
  background: rgba(217, 140, 20, 0.07);
  margin-right: -10px;
  padding-right: 10px;
  border-radius: 0 var(--tf-radius-sm) var(--tf-radius-sm) 0;
}
.tf-q-badge, .tf-a-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 18px;
  padding: 0 6px 0 4px;
  border-radius: 999px;
  font-size: 0.7143rem;
  font-weight: 600;
}
.tf-q-badge { background: rgba(217, 140, 20, 0.14); color: var(--tf-amber); }
.tf-a-badge { background: rgba(46, 171, 106, 0.14); color: var(--tf-green); }
.tf-q-ref { font-size: 0.7857rem; margin-top: 1px; }
.tf-q-link { color: var(--tf-ink-3); }
.tf-q-link:hover { color: var(--tf-violet-hover); }
.tf-q-foot { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 6px; }
.tf-q-state { display: inline-flex; align-items: center; gap: 5px; font-size: 0.7857rem; }
.tf-q-state.is-open { color: var(--tf-amber); }
.tf-q-state.is-answered { color: var(--tf-ink-3); }
/* The answer box wants the whole row rather than the sliver its own two
   buttons ask for. What sits in the footer's flex is the reveal wrapper, not
   the form inside it, so the width has to be claimed there — and only while
   the box is open, so that closed, the opener stays on the line beside
   "Waiting for an answer" where it reads as an offer rather than a form. */
.tf-q-answer:has(> [data-reveal-target="panel"]:not([hidden])) { flex: 1 1 100%; }
.tf-answer-box { margin-top: 6px; }
.tf-q-open-list {
  margin-bottom: 12px;
  padding: 8px 10px;
  border: 1px solid rgba(217, 140, 20, 0.35);
  border-radius: var(--tf-radius);
  background: rgba(217, 140, 20, 0.07);
}
.tf-q-open-head {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.7857rem;
  font-weight: 600;
  color: var(--tf-amber);
  margin-bottom: 4px;
}
.tf-q-open-row { display: flex; gap: 6px; padding: 2px 0; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-q-open-row:hover { color: var(--tf-violet-hover); }
.tf-q-open-who { font-weight: 600; white-space: nowrap; }
.tf-q-open-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.tf-image-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); gap: 8px; }
.tf-image-grid img {
  width: 100%;
  height: 84px;
  object-fit: cover;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
}
/* A file that is not a picture cannot be its own thumbnail, so it says what
   it is instead: the extension large, the name under it. Same footprint as an
   image tile, so a grid of the two still lines up. */
.tf-file-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 100%;
  height: 84px;
  padding: 6px;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
  background: var(--tf-hover);
  overflow: hidden;
}
.tf-file-ext {
  font-size: 0.7143rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-file-chip .tf-file-name {
  max-width: 100%;
  font-size: 0.7143rem;
  color: var(--tf-ink-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-drop {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px;
  border: 1px dashed var(--tf-line);
  border-radius: var(--tf-radius);
  background: var(--tf-shell);
}
.tf-drop input[type="file"] { border: 0; background: none; font-size: 0.8571rem; padding: 0; }

/* ----------------------------------------------------------------- auth --- */

.tf-auth {
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding: 24px;
  background:
    radial-gradient(1200px 520px at 12% -8%, #efecfe 0%, rgba(255, 255, 255, 0) 62%),
    radial-gradient(900px 460px at 96% 108%, #eaf1fe 0%, rgba(255, 255, 255, 0) 60%),
    var(--tf-shell);
}
.tf-auth-card {
  width: min(400px, 100%);
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: 16px;
  padding: 28px 26px 24px;
  box-shadow: 0 20px 50px rgba(30, 26, 60, 0.10);
}
.tf-auth-card h1 { font-size: 1.3125rem; margin: 14px 0 4px; }
.tf-auth-card .tf-page-sub { margin-bottom: 20px; }
.tf-auth-mark {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  object-fit: cover;
  object-position: 50% 32%;
  display: block;
}
.tf-auth-foot { margin-top: 18px; padding-top: 16px; border-top: 1px solid var(--tf-line); font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-auth-foot a { font-weight: 500; }
.tf-auth-foot p { margin-bottom: 4px; }

.tf-token {
  font-family: var(--tf-mono);
  font-size: 0.8571rem;
  word-break: break-all;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  padding: 8px 10px;
}

/* The second step of signing in, and the screen that sets it up. */
.tf-auth-or {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 16px 0;
  color: var(--tf-ink-2);
  font-size: 0.7857rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
}
.tf-auth-or::before, .tf-auth-or::after { content: ""; flex: 1; border-top: 1px solid var(--tf-line); }
.tf-auth-details summary { cursor: pointer; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-auth-link { font-size: inherit; font-weight: 500; text-decoration: none; vertical-align: baseline; }
.tf-code-field {
  font-family: var(--tf-mono);
  font-size: 1.1429rem;
  letter-spacing: 0.18em;
}
.tf-card-note { font-size: 0.7143rem; color: var(--tf-ink-2); }
.tf-steps { padding-left: 1.25rem; margin: 0; }
.tf-steps > li + li { margin-top: 20px; }
.tf-qr-wrap {
  display: inline-block;
  background: #fff;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  padding: 8px;
}
.tf-qr { display: block; width: 196px; height: 196px; }
.tf-recovery-codes {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px 16px;
  font-family: var(--tf-mono);
  font-size: 0.9286rem;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  padding: 10px 12px;
  max-width: 22rem;
}
.tf-confirm-row { max-width: 30rem; }
.tf-token-edit-row .tf-confirm-row { padding: 12px 16px; }

/* --------------------------------------------------------------- admin --- */

/* Storage accounting reads as figures first: the stat row carries the number
   a customer gets billed for, the table explains where it came from. */
.tf-stat-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(11rem, 100%), 1fr));
  gap: 12px;
  margin-bottom: 20px;
}
.tf-stat {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  padding: 14px 16px;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.tf-stat-key { font-size: 0.7857rem; text-transform: uppercase; letter-spacing: 0.04em; color: var(--tf-ink-3); }
.tf-stat-val { font-size: 1.375rem; font-weight: 600; font-variant-numeric: tabular-nums; }
.tf-stat-note { font-size: 0.7857rem; color: var(--tf-ink-3); }

.tf-table-admin td { font-variant-numeric: tabular-nums; }

/* The people table and the "create an account" form share a row. A Bootstrap
   column is a flex item, and a flex item is as wide as its content unless told
   otherwise — so a long email or a wide row pushed the table out from under
   its own card and across the form beside it. The column is pinned to the
   width the grid gave it and the table scrolls inside the card instead. */
.tf-admin-people > [class^="col-"] { min-width: 0; }
.tf-admin-people .tf-table-wrap { overflow-x: auto; }
.tf-table-people { min-width: 34rem; }
/* Impersonating is the last cell and the widest control in it; wrapping the
   label under itself is what made it hard to hit. */
.tf-table-people .tf-people-act { width: 1%; white-space: nowrap; text-align: right; }
.tf-table-people .tf-people-act form { margin: 0; }

.tf-meter {
  display: block;
  width: 100%;
  min-width: 4rem;
  height: 6px;
  border-radius: 999px;
  background: var(--tf-line-soft);
  overflow: hidden;
}
.tf-meter-fill { display: block; height: 100%; background: var(--tf-violet); border-radius: 999px; }

.tf-toggle {
  border: 1px solid var(--tf-line);
  background: var(--tf-canvas);
  color: var(--tf-ink-2);
  border-radius: 999px;
  padding: 2px 12px;
  font-size: 0.7857rem;
  font-weight: 500;
}
.tf-toggle:hover { border-color: var(--tf-violet); color: var(--tf-violet); }
.tf-toggle.is-on { background: var(--tf-violet-tint); border-color: var(--tf-violet-tint-2); color: var(--tf-violet-press); }

/* A guest is told plainly, once, why the controls are missing. */
.tf-readonly {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.7857rem;
  color: var(--tf-ink-2);
  background: var(--tf-shell);
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  padding: 3px 10px;
}
.tf-readonly-note { font-size: 0.8571rem; color: var(--tf-ink-3); margin: 0; }

/* ----------------------------------------------------------- responsive --- */

/* The rest of it — everything that changes below the large breakpoint, and
   everything that changes for a finger — is at the foot of this file, where
   source order lets it win. This one is here because it is the default the
   breakpoint turns on. */
.tf-side-close { display: none; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation: none !important; transition: none !important; }
}

/* Drag handles only appear where reordering is persisted. */
.tf-board:not(.is-sortable) .tf-drag { display: none; }

/* Toolbar controls sit at chip height. */
.tf-toolbar .form-select {
  height: 30px;
  width: auto;
  padding: 0 1.75rem 0 0.55rem;
  font-size: 0.8571rem;
  border-radius: var(--tf-radius-sm);
}

/* Workspace switcher popover, matched to the dark navigation. */
.tf-nav-menu {
  --bs-dropdown-bg: var(--tf-nav-menu);
  --bs-dropdown-color: var(--tf-nav-fg);
  --bs-dropdown-border-color: var(--tf-nav-line);
  --bs-dropdown-link-color: var(--tf-nav-fg);
  --bs-dropdown-link-hover-color: var(--tf-nav-fg-strong);
  --bs-dropdown-link-hover-bg: var(--tf-nav-hover);
  --bs-dropdown-link-active-bg: var(--tf-nav-active);
  --bs-dropdown-link-active-color: var(--tf-nav-fg-strong);
  --bs-dropdown-divider-bg: var(--tf-nav-line);
  --bs-dropdown-header-color: var(--tf-nav-fg-dim);
  max-width: 20rem;
}
/* Switching workspace is a button rather than a link, and a button left at the
   width:auto above shrinks to its own text: the rows came out ragged and the
   count had no edge to sit against. Each row is a mark and a name, so it is
   laid out as one, the whole width of the menu. */
.tf-nav-menu .dropdown-item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}
.tf-menu-ws-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Images pasted into descriptions and comments. */
.tf-prose img {
  max-width: 100%;
  height: auto;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
  margin: 6px 0;
}
.tf-comment-text.tf-prose img { max-width: min(100%, 22rem); }
textarea.is-dropping {
  border-color: var(--tf-violet);
  box-shadow: 0 0 0 3px var(--tf-violet-tint);
}

/* What you just pasted, under the field it went into: a description or a
   comment is markdown until it is saved, so without this the only sign an
   image had arrived was the link text standing in for it. */
.tf-paste-shelf { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.tf-paste-shelf:empty { display: none; }
.tf-paste-chip {
  margin: 0;
  width: 104px;
  font-size: 0.7143rem;
  color: var(--tf-ink-3);
}
.tf-paste-chip img {
  width: 100%;
  height: 64px;
  object-fit: cover;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
  opacity: 0.5;
  transition: opacity 140ms ease;
}
.tf-paste-chip figcaption {
  padding-top: 3px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The shelf under a comment box draws the same idea at its own size. */
.tf-paste-chip.is-file .tf-file-ext {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 64px;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
  background: var(--tf-hover);
  opacity: 0.5;
  transition: opacity 140ms ease;
}
.tf-paste-chip.is-ready .tf-file-ext { opacity: 1; }
.tf-paste-chip.is-failed .tf-file-ext { opacity: 0.25; border-color: var(--tf-red); }
.tf-paste-chip.is-ready img { opacity: 1; }
.tf-paste-chip.is-failed img { opacity: 0.25; border-color: var(--tf-red); }
.tf-paste-chip.is-failed figcaption { color: var(--tf-red); white-space: normal; }

/* ------------------------------------------------------------- trix ----- */

/* The description editor. actiontext.css lands first (app stylesheets load in
   name order), so these are the overrides on top of Trix's own defaults.
   Everything is written in the theme's variables, which is all dark mode
   needs. */

trix-editor.tf-trix {
  font-size: 0.9286rem;
  line-height: 1.55;
  min-height: 9rem;
  padding: 0.5rem 0.7rem;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  background: var(--tf-paper, #fff);
  color: var(--tf-ink);
  transition: border-color var(--tf-ease), box-shadow var(--tf-ease);
}
trix-editor.tf-trix:focus {
  border-color: var(--tf-violet);
  box-shadow: 0 0 0 3px var(--tf-violet-tint);
  outline: none;
}
/* Trix leaves the placeholder to CSS, and actiontext.css does not carry the
   rule. */
trix-editor.tf-trix[placeholder]:empty:not(:focus)::before {
  content: attr(placeholder);
  color: var(--tf-ink-3);
  pointer-events: none;
}
[data-theme="dark"] trix-editor.tf-trix { background: #201c2b; }

/* Trix draws its own toolbar. Quieted down to the app's buttons rather than
   the browser's. */
trix-toolbar .trix-button-group {
  border-color: var(--tf-line);
  border-radius: var(--tf-radius-sm);
  margin-bottom: 6px;
  background: var(--tf-shell);
}
trix-toolbar .trix-button {
  border-bottom: 0;
  border-color: var(--tf-line);
  background: transparent;
  color: var(--tf-ink-2);
}
trix-toolbar .trix-button:not(:disabled):hover { background: var(--tf-hover, var(--tf-line-soft)); }
trix-toolbar .trix-button.trix-active {
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press, var(--tf-violet));
}
trix-toolbar .trix-button--icon::before { opacity: 0.75; }
[data-theme="dark"] trix-toolbar .trix-button--icon::before { filter: invert(1); opacity: 0.7; }
trix-toolbar .trix-dialog {
  background: var(--tf-shell);
  border-color: var(--tf-line);
  box-shadow: var(--tf-shadow-2);
}
trix-toolbar .trix-input--dialog {
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
  background: var(--tf-paper, #fff);
  color: var(--tf-ink);
}

/* An image being uploaded, and the same image once it is. Both are in the
   text where they were pasted; only the bar over the top says which. */
trix-editor .attachment--preview img,
.trix-content .attachment--preview img {
  max-width: 100%;
  height: auto;
  border-radius: var(--tf-radius-sm);
  border: 1px solid var(--tf-line);
}
trix-editor .attachment__progress {
  border-radius: 2px;
  background: var(--tf-violet-tint);
}
trix-editor .attachment__progress[value]::-webkit-progress-value { background: var(--tf-violet); }
trix-editor .attachment__progress[value]::-moz-progress-bar { background: var(--tf-violet); }

/* A saved description, wherever it is read. */
.trix-content { color: var(--tf-ink); }
.trix-content :last-child { margin-bottom: 0; }
.trix-content img { max-width: 100%; height: auto; }
.trix-content figure.attachment { margin: 8px 0; }
.trix-content figcaption.attachment__caption {
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
  padding-top: 3px;
}
.trix-content a { color: var(--tf-violet-press, var(--tf-violet)); }
.trix-content blockquote {
  border-left: 3px solid var(--tf-line);
  padding-left: 12px;
  margin-left: 0;
  color: var(--tf-ink-2);
}
.trix-content pre {
  background: var(--tf-shell);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  padding: 10px 12px;
  overflow-x: auto;
  white-space: pre-wrap;
}
/* Descriptions written before this was rich text kept their markdown images
   as plain img tags. */
.tf-prose .trix-content img { margin: 6px 0; border-radius: var(--tf-radius-sm); border: 1px solid var(--tf-line); }

/* -------------------------------------------------------------- www ----- */

/* The site is the one place the product's violet does not appear. It is deep
   navy and signal blue on purpose: the app is a work surface and should read
   as paper, the site is a front door and should read as calm. It is also not
   selling anything — there is no sign-up — so it is one column, wide margins,
   and prose where a sales page would put a grid of cards. Everything below
   lives under .www so none of it can leak into the shell. */

.tf-www {
  background: #061525;
  color: #d7e4f2;
  min-height: 100vh;
}

.www {
  --www-navy: #061525;
  --www-foam: #f3f7fb;
  --www-signal: #3b9bff;
  --www-ink: #b7c9dc;
  --www-ink-dim: #8aa0b8;
  --www-line: rgba(243, 247, 251, 0.12);
  --www-panel: rgba(255, 255, 255, 0.035);
  --www-display: "Sora", var(--tf-ui);
  --www-pad: clamp(1.25rem, 6vw, 5rem);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  overflow-x: hidden;
  font-size: 1.0625rem;
  line-height: 1.7;
}

.www-main { flex: 1; }

.www a { transition: color 120ms ease, background 120ms ease, border-color 120ms ease; }

/* One measure for the whole page. Sections that are pure reading narrow it
   further, because sixty characters is the width a paragraph is read at. */
.www-wrap { max-width: 68rem; margin: 0 auto; }
.www-wrap-narrow { max-width: 44rem; }

/* --- nav --- */

.www-nav {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.1rem var(--www-pad);
  background: rgba(6, 21, 37, 0.82);
  backdrop-filter: blur(10px);
}

.www-brand {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  color: var(--www-foam);
  text-decoration: none;
  font-family: var(--www-display);
  font-weight: 600;
  letter-spacing: 0.02em;
}

.www-brand img {
  width: 40px;
  height: 40px;
  border-radius: 10px;
  object-fit: cover;
  object-position: 50% 32%;
}

.www-nav-links { display: flex; align-items: center; gap: 1.6rem; }

.www-nav-link {
  color: var(--www-ink);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 500;
}
.www-nav-link:hover { color: var(--www-foam); }

/* Sign in is for the people who already have a workspace, so it is a link
   and not a button: nothing on this page is asking a visitor to do anything. */
.www-nav-signin {
  color: var(--www-foam);
  text-decoration: none;
  font-size: 0.92rem;
  font-weight: 600;
  padding: 0.4rem 0.9rem;
  border-radius: 999px;
  border: 1px solid rgba(243, 247, 251, 0.28);
}
.www-nav-signin:hover { color: #fff; background: rgba(255, 255, 255, 0.06); }

/* --- hero --- */

.www-hero {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
  gap: 4vw;
  align-items: center;
  max-width: 76rem;
  margin: 0 auto;
  padding: clamp(3.5rem, 7vw, 6rem) var(--www-pad) clamp(3.5rem, 7vw, 6rem);
}

/* The glow is drawn rather than shipped, so the page still has something on
   it when the network eats the one asset it does load. */
.www-hero::before {
  content: "";
  position: absolute;
  inset: -30% 30% 20% -20%;
  background: radial-gradient(closest-side, rgba(59, 155, 255, 0.16), transparent);
  pointer-events: none;
}

.www-hero-copy, .www-hero-art { position: relative; }

.www-kicker {
  font-family: var(--tf-mono);
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--www-signal);
  margin: 0 0 1rem;
}

.www-hero h1 {
  font-family: var(--www-display);
  font-size: clamp(2.4rem, 5vw, 4.1rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -0.04em;
  color: var(--www-foam);
  margin: 0 0 1.25rem;
}

.www-lede {
  font-size: 1.15rem;
  line-height: 1.6;
  color: var(--www-ink);
  max-width: 36rem;
  margin: 0 0 1.75rem;
}

.www-hero-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; }

.www-btn-primary {
  display: inline-flex;
  align-items: center;
  background: var(--www-signal);
  color: var(--www-navy);
  font-weight: 700;
  text-decoration: none;
  padding: 0.7rem 1.2rem;
  border-radius: 999px;
}
.www-btn-primary:hover { color: var(--www-navy); filter: brightness(1.06); }

.www-hero-proof {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 1.6rem;
  list-style: none;
  margin: 2rem 0 0;
  padding: 0;
  font-size: 0.88rem;
  color: var(--www-ink-dim);
}

.www-hero-proof li { display: flex; align-items: center; gap: 0.5rem; }
.www-hero-proof li::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--www-signal);
  flex: none;
}

.www-hero-art { justify-self: end; width: min(100%, 500px); }

/* --- the ticket feed: one ticket's morning, drawn in the page --- */

.www-feed {
  border: 1px solid var(--www-line);
  border-radius: 18px;
  background: linear-gradient(180deg, #0d2a4e, #08192c);
  box-shadow: 0 30px 60px rgba(0, 0, 0, 0.45);
  overflow: hidden;
}

.www-feed-head {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.9rem 1.1rem;
  border-bottom: 1px solid var(--www-line);
  background: rgba(0, 0, 0, 0.18);
}

.www-feed-ref {
  font-family: var(--tf-mono);
  font-size: 0.74rem;
  color: var(--www-ink-dim);
}

.www-feed-title {
  font-family: var(--www-display);
  font-weight: 600;
  font-size: 0.98rem;
  color: var(--www-foam);
}

.www-feed-status {
  margin-left: auto;
  flex: none;
  font-size: 0.66rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 0.22rem 0.6rem;
  border-radius: 999px;
  color: var(--www-navy);
  background: var(--www-signal);
}

.www-feed-rows { list-style: none; margin: 0; padding: 0.25rem 1.1rem 0.5rem; }

.www-feed-rows li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: start;
  gap: 0.75rem;
  padding: 0.7rem 0;
  border-top: 1px solid rgba(243, 247, 251, 0.07);
  color: var(--www-ink);
  font-size: 0.88rem;
  line-height: 1.45;
}
.www-feed-rows li:first-child { border-top: 0; }

.www-feed-text strong { color: var(--www-foam); font-weight: 600; }
.www-feed-text em {
  font-style: normal;
  font-family: var(--tf-mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--www-ink-dim);
  margin-right: 0.15rem;
}

.www-feed-flag {
  display: inline-block;
  margin-left: 0.4rem;
  font-family: var(--tf-mono);
  font-size: 0.66rem;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: #d98c14;
}

/* Who did it: a person, an agent, or somebody at the customer. Three fills,
   so the eye can tell the feed is mixed before it reads a word. */
.www-who {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  font-family: var(--tf-mono);
  font-size: 0.66rem;
  font-weight: 600;
  color: var(--www-navy);
}
.www-who-person { background: var(--www-foam); }
.www-who-agent { background: var(--www-signal); }
.www-who-customer { background: #2eab6a; }

.www-stage-caption {
  margin: 1rem 0.25rem 0;
  color: var(--www-ink-dim);
  font-size: 0.88rem;
  line-height: 1.5;
}

/* --- bands --- */

.www-band { padding: clamp(4rem, 9vw, 7.5rem) var(--www-pad); }
.www-band-alt { background: rgba(255, 255, 255, 0.025); border-block: 1px solid var(--www-line); }

.www-band h2 {
  font-family: var(--www-display);
  font-size: clamp(1.8rem, 3.4vw, 2.75rem);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.03em;
  color: var(--www-foam);
  max-width: 20em;
  margin: 0 0 1.25rem;
}

.www-section-lede {
  color: var(--www-ink);
  max-width: 34em;
  margin: 0;
}

/* Three things, side by side, with a rule above each in place of a box. */
.www-three {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 3rem;
  margin-top: 3rem;
}

.www-three article { padding-top: 1.25rem; border-top: 1px solid var(--www-line); }

.www-three h3 {
  font-family: var(--www-display);
  font-size: 1.15rem;
  font-weight: 600;
  color: var(--www-foam);
  margin: 0 0 0.75rem;
}

.www-three p { margin: 0; color: var(--www-ink); }

.www-prose { margin-top: 2rem; }
.www-prose p { margin: 0 0 1.4rem; color: var(--www-ink); font-size: 1.1rem; }
.www-prose p:last-child { margin-bottom: 0; }

/* What's inside: two columns of term and line, not eight cards. */
.www-inside {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2rem 4rem;
  margin: 3rem 0 0;
}

.www-inside div { padding-top: 1rem; border-top: 1px solid var(--www-line); }

.www-inside dt {
  font-family: var(--www-display);
  font-weight: 600;
  color: var(--www-foam);
  margin-bottom: 0.35rem;
}

.www-inside dd { margin: 0; color: var(--www-ink); font-size: 0.98rem; }

.www code {
  font-family: var(--tf-mono);
  font-size: 0.86em;
  color: #d7e4f2;
  background: rgba(59, 155, 255, 0.12);
  border-radius: 5px;
  padding: 0.08em 0.35em;
  white-space: nowrap;
}

/* --- split (for the agent) --- */

.www-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 4rem;
  align-items: center;
}

.www-split-art { min-width: 0; }

.www-term {
  border: 1px solid var(--www-line);
  border-radius: 16px;
  background: #04101d;
  box-shadow: 0 24px 50px rgba(0, 0, 0, 0.4);
  overflow: hidden;
}

.www-term-bar {
  padding: 0.6rem 0.9rem;
  border-bottom: 1px solid var(--www-line);
  background: rgba(255, 255, 255, 0.03);
  font-family: var(--tf-mono);
  font-size: 0.72rem;
  color: var(--www-ink-dim);
}

.www-term-body {
  margin: 0;
  padding: 1.1rem 1rem;
  font-family: var(--tf-mono);
  font-size: 0.8rem;
  line-height: 1.75;
  color: var(--www-ink);
  overflow-x: auto;
}

.www-term-body .c { color: var(--www-signal); }
.www-term-body .k { color: var(--www-foam); }
.www-term-body .ok { color: #2eab6a; }
.www-term-body .w { color: #d98c14; }

/* --- footer --- */

.www-foot {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 1.25rem 2rem;
  align-items: center;
  margin-top: auto;
  padding: 2.2rem var(--www-pad) 2.6rem;
  border-top: 1px solid var(--www-line);
  color: var(--www-ink-dim);
  font-size: 0.9rem;
}

.www-foot-brand { display: flex; align-items: center; gap: 0.75rem; }
.www-foot-brand div { display: flex; flex-direction: column; }
.www-foot-brand strong { color: var(--www-foam); font-family: var(--www-display); font-weight: 600; }

.www-foot-mark {
  width: 34px;
  height: 34px;
  border-radius: 9px;
  object-fit: cover;
  object-position: 50% 32%;
}

.www-foot-links { display: flex; flex-wrap: wrap; gap: 0.6rem 1.3rem; justify-content: flex-end; }
.www-foot-links a { color: var(--www-ink-dim); text-decoration: none; }
.www-foot-links a:hover { color: var(--www-foam); }

.www-foot-legal { grid-column: 1 / -1; margin: 0; font-size: 0.84rem; }

/* --- narrower --- */

@media (max-width: 900px) {
  .www-nav-links .www-nav-link { display: none; }
  .www-hero { grid-template-columns: 1fr; }
  .www-hero-art { justify-self: stretch; width: 100%; }
  .www-three { grid-template-columns: 1fr; gap: 2rem; }
  .www-inside { grid-template-columns: 1fr; gap: 1.5rem; }
  .www-split { grid-template-columns: 1fr; gap: 2.5rem; }
  /* Reference and status share the first line; the title gets the second to
     itself rather than breaking under the reference. */
  .www-feed-head { flex-wrap: wrap; gap: 0.5rem 0.9rem; }
  .www-feed-title { order: 3; flex-basis: 100%; }
  .www-foot { grid-template-columns: 1fr; }
  .www-foot-links { justify-content: flex-start; }
}

@media (prefers-reduced-motion: reduce) {
  .www a { transition: none; }
}

/* ------------------------------------------------------- notifications --- */

.tf-bell { position: relative; }
.tf-bell::after { display: none; }
.tf-bell-badge {
  position: absolute;
  top: -1px;
  right: -1px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 999px;
  background: var(--tf-red);
  color: #fff;
  font-size: 0.6429rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
  box-shadow: 0 0 0 2px var(--tf-canvas);
}

.tf-note-menu { width: 22rem; padding: 0; overflow: hidden; }
.tf-note-menu-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 12px;
  border-bottom: 1px solid var(--tf-line);
  font-size: 0.8571rem;
  font-weight: 600;
}
.tf-note-clear {
  margin-left: auto;
  border: 0;
  background: none;
  color: var(--tf-violet-hover);
  font-size: 0.8571rem;
  font-weight: 500;
  padding: 0;
}
.tf-note-clear:hover { color: var(--tf-violet-press); text-decoration: underline; }
.tf-note-permission {
  display: flex;
  align-items: center;
  gap: 7px;
  width: 100%;
  padding: 9px 12px;
  border: 0;
  border-bottom: 1px solid var(--tf-line);
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  font-size: 0.8571rem;
  font-weight: 500;
  text-align: left;
}
.tf-note-permission:hover { background: var(--tf-violet-tint-2); }
.tf-note-menu .tf-note-list { max-height: 22rem; overflow-y: auto; }
.tf-note-form { margin: 0; }
.tf-note {
  display: flex;
  gap: 9px;
  width: 100%;
  padding: 10px 12px;
  border: 0;
  border-bottom: 1px solid var(--tf-line-soft);
  background: none;
  color: var(--tf-ink);
  text-align: left;
  text-decoration: none;
  transition: background var(--tf-ease);
}
.tf-note:hover { background: var(--tf-hover); color: var(--tf-ink); text-decoration: none; }
.tf-note.is-unread { background: var(--tf-violet-tint); }
.tf-note.is-unread:hover { background: var(--tf-violet-tint); }
.tf-note-ico { color: var(--tf-violet-hover); padding-top: 2px; }
.tf-note-body { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.tf-note-title { font-size: 0.8571rem; font-weight: 500; }
.tf-note-text {
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-note-meta { font-size: 0.7857rem; color: var(--tf-ink-3); }
.tf-note-empty { padding: 22px 12px; margin: 0; text-align: center; color: var(--tf-ink-3); font-size: 0.8571rem; }
.tf-note-foot {
  display: block;
  padding: 9px 12px;
  border-top: 1px solid var(--tf-line);
  font-size: 0.8571rem;
  font-weight: 500;
  text-align: center;
}
.tf-note-foot:hover { background: var(--tf-hover); text-decoration: none; }
.tf-toast-live { background: #2f2a3d; }

/* Mentions and agents */
.tf-mention {
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  border-radius: 4px;
  padding: 0 3px;
  font-weight: 500;
}
.tf-mention-menu {
  position: absolute;
  z-index: 20;
  left: 8px;
  bottom: calc(100% + 4px);
  min-width: 12rem;
  padding: 4px;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow-2);
}
.tf-mention-option {
  display: block;
  width: 100%;
  padding: 5px 8px;
  border: 0;
  border-radius: var(--tf-radius-sm);
  background: none;
  color: var(--tf-ink);
  font-size: 0.9286rem;
  text-align: left;
}
.tf-mention-option:hover, .tf-mention-option.is-active { background: var(--tf-violet-tint); color: var(--tf-violet-press); }
.tf-agent-badge {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  height: 18px;
  padding: 0 6px 0 4px;
  border-radius: 999px;
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  font-size: 0.7143rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.tf-avatar-agent {
  background: linear-gradient(150deg, #8a76ff 10%, #5b45d6 90%);
  border-radius: var(--tf-radius-sm);
}

/* =========================================================================
   Themes. Mixed (default) pairs dark navigation with a paper work surface.
   Light keeps the whole shell pale. Dark takes the plum all the way through.
   ========================================================================= */

[data-theme="light"] {
  --tf-nav: #fafafb;
  --tf-nav-hover: #f0f1f4;
  --tf-nav-line: #e8e9ee;
  --tf-nav-fg: #40474f;
  --tf-nav-fg-dim: #8f97a3;
  --tf-nav-fg-strong: #292d34;
  --tf-nav-active: var(--tf-violet-tint);
  --tf-nav-field: #ffffff;
  --tf-nav-field-focus: #ffffff;
  --tf-nav-menu: #ffffff;
}
[data-theme="light"] .tf-side-link.is-active { color: var(--tf-violet-press); }
[data-theme="light"] .tf-side-link.is-active .tf-icon { color: var(--tf-violet-hover); }
[data-theme="light"] .tf-stage { border-left: 1px solid var(--tf-line); }

[data-theme="dark"] {
  color-scheme: dark;

  --tf-canvas: #17151f;
  --tf-shell: #1c1927;
  --tf-hover: #241f30;
  --tf-row-hover: #1e1b28;
  --tf-line: #2c2739;
  --tf-line-soft: #241f31;
  --tf-ink: #ece9f6;
  --tf-ink-2: #a9a2be;
  --tf-ink-3: #7f7896;

  --tf-nav: #201c2b;
  --tf-nav-hover: rgba(255, 255, 255, 0.06);
  --tf-nav-line: rgba(255, 255, 255, 0.09);
  --tf-nav-menu: #2a2537;

  --tf-chip-base: #221e2d;
  --tf-chip-ink: #ffffff;

  --tf-violet-tint: rgba(123, 104, 238, 0.18);
  --tf-violet-tint-2: rgba(123, 104, 238, 0.30);
  --tf-violet-hover: #a695ff;
  --tf-violet-press: #bcaeff;
  --tf-red-tint: rgba(229, 72, 77, 0.14);

  --tf-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.32);
  --tf-shadow-2: 0 6px 18px rgba(0, 0, 0, 0.42);
  --tf-shadow-3: 0 18px 44px rgba(0, 0, 0, 0.55);

  --bs-body-bg: var(--tf-canvas);
  --bs-body-color: var(--tf-ink);
  --bs-secondary-bg: var(--tf-shell);
  --bs-tertiary-bg: var(--tf-hover);
  --bs-emphasis-color: var(--tf-ink);
  --bs-border-color: var(--tf-line);
  --bs-link-color-rgb: 166, 149, 255;
  --bs-link-hover-color-rgb: 188, 174, 255;
}
[data-theme="dark"] body { background: var(--tf-canvas); }
[data-theme="dark"] .form-control,
[data-theme="dark"] .form-select {
  background-color: #201c2b;
  border-color: var(--tf-line);
  color: var(--tf-ink);
}
[data-theme="dark"] .form-select {
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23a9a2be' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}
[data-theme="dark"] .form-control::placeholder { color: var(--tf-ink-3); }
[data-theme="dark"] .form-check-input { background-color: #201c2b; border-color: #3a3450; }
[data-theme="dark"] .dropdown-menu {
  --bs-dropdown-bg: #241f31;
  --bs-dropdown-color: var(--tf-ink);
  --bs-dropdown-border-color: var(--tf-line);
  --bs-dropdown-link-color: var(--tf-ink);
  --bs-dropdown-link-hover-bg: var(--tf-hover);
  --bs-dropdown-link-hover-color: var(--tf-ink);
}
[data-theme="dark"] .btn-outline-secondary,
[data-theme="dark"] .tf-btn-quiet,
[data-theme="dark"] .tf-chip {
  --bs-btn-bg: #201c2b;
  --bs-btn-color: var(--tf-ink);
  --bs-btn-hover-bg: var(--tf-hover);
  --bs-btn-hover-color: var(--tf-ink);
  background: #201c2b;
  color: var(--tf-ink);
  border-color: var(--tf-line);
}
[data-theme="dark"] .tf-chip:hover { background: var(--tf-hover); color: var(--tf-ink); }
[data-theme="dark"] .alert-success { background: rgba(46, 171, 106, 0.14); color: #7ddda8; border-color: rgba(46, 171, 106, 0.3); }
[data-theme="dark"] .alert-danger { background: var(--tf-red-tint); color: #ff9ea1; border-color: rgba(229, 72, 77, 0.32); }
[data-theme="dark"] .alert-warning { background: rgba(217, 140, 20, 0.14); color: #eec27a; border-color: rgba(217, 140, 20, 0.3); }
[data-theme="dark"] .tf-priority-high { color: #eec27a; }
[data-theme="dark"] .tf-due.is-soon { color: #eec27a; }
[data-theme="dark"] .tf-auth {
  background:
    radial-gradient(1200px 520px at 12% -8%, #241f36 0%, rgba(0, 0, 0, 0) 62%),
    radial-gradient(900px 460px at 96% 108%, #1d2340 0%, rgba(0, 0, 0, 0) 60%),
    #14121b;
}
[data-theme="dark"] .tf-prose code { background: var(--tf-violet-tint); color: var(--tf-violet-press); }
[data-theme="dark"] .tf-assignees-more { background: var(--tf-hover); color: var(--tf-ink-2); }
[data-theme="dark"] .tf-drag { color: #4c4560; }

/* -------------------------------------------------- assignee popover ---- */

.tf-picker {
  position: fixed;
  /* Above the drawer (50): the task panel opens the status picker too, and a
     picker drawn under the panel hands its clicks to whatever field of the
     panel happens to be behind it. Still under the topbar's own layer. */
  z-index: 55;
  width: 15rem;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow-2);
  overflow: hidden;
  animation: tf-rise 120ms cubic-bezier(0.2, 0, 0.2, 1);
}
.tf-picker-head {
  padding: 8px 10px;
  border-bottom: 1px solid var(--tf-line);
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-picker-search { padding: 8px 8px 4px; }
.tf-picker-search input { width: 100%; height: 28px; font-size: 0.8571rem; border-radius: var(--tf-radius-sm); border: 1px solid var(--tf-line); padding: 0 8px; background: var(--tf-canvas); color: var(--tf-ink); }
.tf-picker-list { max-height: 13rem; overflow-y: auto; padding: 4px; }

/* ------------------------------------------------------------- batches --- */

/* A batch is watched more than it is read: what state it is in, whether it is
   waiting on somebody, and who has it, all before a word of the title. */
.tf-batch-filters { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; }
.tf-filter-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  background: var(--tf-canvas);
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
  text-decoration: none;
}
.tf-filter-pill:hover { border-color: var(--tf-violet); color: var(--tf-violet-hover); }
.tf-filter-pill.is-on { background: var(--tf-violet-tint); border-color: var(--tf-violet); color: var(--tf-violet-press); }

/* Batch cards want the room a settings table wants: a card holds a title, a
   state, a reference, a holder line and a note, and at 66rem two of them sat
   in a column with the rest of the page empty beside them. */
/* `minmax` will not shrink a track below its own minimum, so on a screen
   narrower than one card the minimum has to give way to the screen. */
.tf-release-list { display: grid; grid-template-columns: minmax(0, 1fr); gap: 10px; }
.tf-release-state-form select { width: auto; }
.tf-batch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(24rem, 100%), 1fr)); gap: 14px; }
.tf-batch-card .tf-card-head h2 { font-size: 0.9286rem; }
.tf-batch-card.is-blocked { border-color: rgba(217, 140, 20, 0.5); }
.tf-batch-facts { display: flex; flex-wrap: wrap; gap: 12px; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-batch-facts span { display: inline-flex; align-items: center; gap: 5px; }
.tf-batch-asking { color: var(--tf-amber); font-weight: 600; }
.tf-batch-holder { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-batch-note {
  margin: 10px 0 0;
  padding: 7px 9px;
  border-radius: var(--tf-radius-sm);
  background: var(--tf-shell);
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}
.tf-batch-send { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--tf-line); }

.tf-release-counts { display: flex; flex-wrap: wrap; gap: 14px; }
.tf-release-count { display: inline-flex; align-items: baseline; gap: 6px; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-release-count strong { font-size: 1.1429rem; color: var(--tf-c, var(--tf-ink)); }
.tf-release-summary-head { font-size: 0.8571rem; font-weight: 600; margin: 18px 0 6px; color: var(--tf-ink-2); }
.tf-release-plan {
  padding: 14px 0;
  border-top: 1px solid var(--tf-line);
}
.tf-release-plan:first-of-type { border-top: 0; padding-top: 0; }
.tf-release-plan-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin-bottom: 10px;
}
.tf-card.is-blocked { border-color: rgba(217, 140, 20, 0.5); }

/* A table runs to the edges of the card it sits in: its own header rule is the
   line the padding was drawing. */
.tf-card-body-flush { padding: 0; }
.tf-card-body-flush .tf-table-wrap { border-radius: 0 0 var(--tf-radius) var(--tf-radius); }

/* The batch's tasks, read as a board reads them. Every column is given a width
   so that six rows line up down the page rather than each one wrapping to
   whatever its own badges happened to measure. */
.tf-batch-table th.tf-bt-title { min-width: 20rem; }
.tf-batch-table th.tf-bt-ref { width: 8rem; }
.tf-batch-table th.tf-bt-status { width: 9rem; }
.tf-batch-table th.tf-bt-assignees { width: 7.5rem; }
.tf-batch-table th.tf-bt-priority { width: 7.5rem; }
.tf-batch-table th.tf-bt-due_date { width: 7.5rem; }
.tf-batch-table th.tf-bt-list { width: 10rem; }
/* A column the workspace added. Narrower than the built-in ones because most
   of them hold a word or a pill, and any of them can be dragged wider and will
   be remembered that way. */
.tf-batch-table th.tf-bt-cf { width: 8rem; }
.tf-batch-table th.tf-bt-x { width: 34px; }
.tf-batch-table td.tf-bt-x { padding: 0 6px 0 0; }
.tf-batch-table .tf-bt-where {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}
/* The X that takes a task out of the batch keeps to itself until the row is
   under the pointer, like the open-in-full mark at the other end of it. */
.tf-batch-table td.tf-bt-x .tf-icon-btn { opacity: 0; transition: opacity var(--tf-ease); }
.tf-batch-table .tf-row:hover td.tf-bt-x .tf-icon-btn,
.tf-batch-table td.tf-bt-x .tf-icon-btn:focus-visible { opacity: 1; }
/* The bar docks to the window, so the table under it keeps a clear run at the
   bottom rather than hiding its last row behind it. */
.tf-batch-tasks .tf-table-wrap { max-height: min(62vh, 40rem); }
.tf-batch-tasks { padding-bottom: 4px; }
/* The strip the Columns menu sits on, above the batch's tasks. It has one
   control on it, so it is a right-aligned line rather than a toolbar. */
.tf-batch-tasks-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 6px;
  padding: 0 8px 8px;
}

/* A question an agent is waiting on. Open, it is the loudest thing on the
   page, because the whole batch has stopped behind it. */
.tf-bq { padding: 12px 0; border-bottom: 1px solid var(--tf-line-soft); }
.tf-bq:last-child { border-bottom: 0; padding-bottom: 0; }
.tf-bq.is-open { border-left: 2px solid var(--tf-amber); padding-left: 12px; margin-left: -14px; }

/* Answered, it is a settled fact rather than news: one line of who asked what
   and who settled it, and the whole exchange a press away for anybody who
   wants the working out. */
details.tf-bq { padding: 4px 0; }
details.tf-bq[open] { padding-bottom: 12px; }
.tf-bq-settled-head {
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid var(--tf-line);
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-bq-brief {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 0;
  cursor: pointer;
  list-style: none;
  font-size: 0.8571rem;
}
.tf-bq-brief::-webkit-details-marker { display: none; }
.tf-bq-brief:hover .tf-bq-brief-ask { color: var(--tf-violet-press); }
.tf-bq-brief .tf-icon { flex: none; color: var(--tf-green); align-self: center; }
.tf-bq-brief-ask {
  min-width: 0;
  flex: 1;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--tf-ink);
}
.tf-bq-brief-by { flex: none; color: var(--tf-ink-3); white-space: nowrap; }
.tf-bq-full { margin-top: 6px; }
.tf-bq-head { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.tf-bq-body { margin: 4px 0 8px; color: var(--tf-ink); }
.tf-bq-body p:last-child { margin-bottom: 0; }
.tf-bq-answer {
  padding: 9px 11px;
  border-radius: var(--tf-radius);
  background: var(--tf-shell);
  border: 1px solid var(--tf-line);
  font-size: 0.9286rem;
}
.tf-bq-answer p:last-child { margin-bottom: 0; }
.tf-bq-answer-head {
  display: flex;
  align-items: center;
  gap: 5px;
  font-size: 0.7857rem;
  font-weight: 600;
  color: var(--tf-green);
  margin-bottom: 4px;
}
.tf-bq-form { margin-top: 4px; }

/* The answers an agent said it would take. Laid out as rows rather than chips:
   an option here is a sentence — a filename, a course of action — and a row
   can be read down at a glance where a wrapped chip cannot. */
.tf-bq-choices {
  margin: 0 0 8px;
  padding: 0;
  border: 0;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.tf-bq-choices-legend {
  float: none;
  width: auto;
  margin-bottom: 4px;
  padding: 0;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-bq-choice {
  display: flex;
  align-items: baseline;
  gap: 8px;
  padding: 6px 8px;
  border-radius: var(--tf-radius-sm);
  font-size: 0.9286rem;
  color: var(--tf-ink);
  cursor: pointer;
}
.tf-bq-choice:hover { background: var(--tf-shell); }
.tf-bq-choice input { flex: none; margin: 0; align-self: center; accent-color: var(--tf-violet); }
.tf-bq-choice span { min-width: 0; }
/* The way out. Set apart from the agent's own answers, because it is not one
   of them. */
.tf-bq-choice-else { margin-top: 3px; padding-top: 9px; border-top: 1px solid var(--tf-line-soft); }
.tf-bq-choice-else span { color: var(--tf-ink-3); }

/* What was offered, to somebody who cannot answer it. */
.tf-bq-offered {
  margin: 0 0 8px;
  padding-left: 18px;
  color: var(--tf-ink-2);
  font-size: 0.9286rem;
}

/* What was picked, on a question that has been settled. */
.tf-bq-picked { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 6px; }
.tf-bq-pick {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 8px 2px 6px;
  border-radius: 999px;
  background: var(--tf-shell);
  border: 1px solid var(--tf-line);
  font-size: 0.8571rem;
  font-weight: 500;
  color: var(--tf-ink);
}
.tf-bq-pick .tf-icon { flex: none; color: var(--tf-green); }
.tf-picker-option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 7px;
  border-radius: var(--tf-radius-sm);
  font-size: 0.9286rem;
  cursor: pointer;
}
.tf-picker-option:hover { background: var(--tf-hover); }
.tf-picker-empty { padding: 10px 8px; margin: 0; color: var(--tf-ink-3); font-size: 0.8571rem; }
/* What ticking a box here actually does, said once above the list rather than
   discovered afterwards: everybody on it reads the ticket from outside. */
.tf-picker-note {
  padding: 0 9px 6px;
  margin: 0;
  color: var(--tf-amber);
  font-size: 0.7857rem;
}
/* Somebody at a customer, under the customer they are at. Indented, because
   ticking the company and ticking one person there are different answers and
   the shape should say which is which before the words do. */
.tf-picker-sub { padding-left: 22px; }
.tf-picker-foot {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  padding: 8px;
  border-top: 1px solid var(--tf-line);
}
.tf-cell-edit.is-picking { box-shadow: inset 0 0 0 2px var(--tf-violet); border-radius: 4px; }

/* The panel's assignee menu wears the picker's foot, so clearing a task of
   everybody is in the same place whichever of the two you opened. */
.tf-assign-menu-foot {
  display: flex;
  justify-content: flex-end;
  gap: 6px;
  margin-top: 4px;
  padding-top: 6px;
  border-top: 1px solid var(--tf-line);
}
/* Many people, three buttons: only the names scroll. */
.tf-assign-menu-list { max-height: 15rem; overflow-y: auto; }
.tf-assign-menu { min-width: 13rem; }

/* Theme picker on the profile screen */
.tf-theme-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.tf-theme-option { position: relative; }
.tf-theme-option input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.tf-theme-card {
  display: block;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  overflow: hidden;
  cursor: pointer;
  transition: border-color var(--tf-ease), box-shadow var(--tf-ease);
}
.tf-theme-option input:checked + .tf-theme-card {
  border-color: var(--tf-violet);
  box-shadow: 0 0 0 3px var(--tf-violet-tint);
}
.tf-theme-option input:focus-visible + .tf-theme-card { outline: 2px solid var(--tf-violet); outline-offset: 2px; }
.tf-theme-preview { display: flex; height: 54px; }
.tf-theme-rail { width: 12px; }
.tf-theme-side { width: 34%; }
.tf-theme-body { flex: 1; }
.tf-theme-name { padding: 7px 9px; font-size: 0.8571rem; font-weight: 500; border-top: 1px solid var(--tf-line); }
.tf-theme-mixed .tf-theme-rail { background: #17121f; }
.tf-theme-mixed .tf-theme-side { background: #221b2e; }
.tf-theme-mixed .tf-theme-body { background: #ffffff; }
.tf-theme-light .tf-theme-rail { background: #17121f; }
.tf-theme-light .tf-theme-side { background: #fafafb; }
.tf-theme-light .tf-theme-body { background: #ffffff; }
.tf-theme-dark .tf-theme-rail { background: #17121f; }
.tf-theme-dark .tf-theme-side { background: #201c2b; }
.tf-theme-dark .tf-theme-body { background: #17151f; }

/* ----------------------------------------------------------------- tags --- */

.tf-tag-row { display: inline-flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.tf-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  max-width: 9rem;
  height: 20px;
  padding: 0 7px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 14%, var(--tf-chip-base));
  color: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 74%, var(--tf-chip-ink));
  font-size: 0.7857rem;
  font-weight: 500;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tf-tag-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.tf-tag-more { background: var(--tf-line-soft); color: var(--tf-ink-2); }
.tf-tag-x {
  display: inline-grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border: 0;
  background: none;
  color: inherit;
  opacity: 0.55;
  padding: 0;
  line-height: 0;
}
.tf-tag-x:hover { opacity: 1; }
.tf-tag-form { display: inline; margin: 0; }
.tf-tag-add { display: flex; gap: 6px; align-items: center; margin-top: 6px; }
.tf-tag-add input { height: 28px; font-size: 0.8571rem; max-width: 11rem; }
.tf-picker-create {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-top: 1px solid var(--tf-line);
  background: none;
  color: var(--tf-violet-hover);
  font-size: 0.8571rem;
  font-weight: 500;
  text-align: left;
}
.tf-picker-create:hover { background: var(--tf-violet-tint); }

/* Notification rows: what it is about, then who did what. */
.tf-note { align-items: flex-start; gap: 10px; }
.tf-note-face { padding-top: 1px; }
.tf-note-body { flex: 1; min-width: 0; }
.tf-note-title,
.tf-note-meta,
.tf-note-text {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-note-title { font-weight: 600; }
.tf-note-dot {
  flex: 0 0 auto;
  width: 7px;
  height: 7px;
  margin-top: 7px;
  border-radius: 999px;
  background: var(--tf-violet);
}

/* Inline token editing */
.tf-token-edit-row > td { padding: 0 !important; background: var(--tf-shell); }
.tf-token-edit { padding: 14px 16px; border-top: 1px solid var(--tf-line); }
.tf-token-picture { display: flex; align-items: flex-start; gap: 12px; }
.tf-token-picture-now { flex: 0 0 auto; }
.tf-token-picture .tf-avatar { box-shadow: 0 0 0 2px var(--tf-canvas); }
.tf-picker-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* =========================================================================
   List rows, second pass: the task cell carries status and type, selection
   is visible before you hover, and the bulk bar reads as plain actions.
   ========================================================================= */

/* Rail: icon and label both mark the active area, and stay legible whether
   the active pill is dark (mixed, dark) or near-white (light). */
.tf-rail-item.is-active span { color: #fff; font-weight: 600; }
.tf-rail-item:hover span { color: #fff; }
[data-theme="light"] .tf-rail-item.is-active span,
[data-theme="light"] .tf-rail-item.is-active .tf-icon { color: var(--tf-violet-press); }
[data-theme="light"] .tf-rail-item.is-active:hover span,
[data-theme="light"] .tf-rail-item.is-active:hover .tf-icon { color: var(--tf-violet-press); }

/* Status as a dot in front of the task. The ring is always the status colour
   and always solid; what changes is the pie inside it. Work that has not
   started is a washed-out disc, each in-progress status fills an even step
   further round, and a completed status floods the whole circle edge to edge
   — so "how far along is this" reads down the column without reading a word.
   Both numbers arrive as custom properties: --tf-c the colour, --tf-fill the
   turn of the pie. */
.tf-status-dot {
  flex: 0 0 auto;
  width: 13px;
  height: 13px;
  padding: 1.5px;
  border: 2px solid var(--tf-c, var(--tf-ink-3));
  border-radius: 999px;
  background-color: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, transparent);
  background-image: conic-gradient(var(--tf-c, var(--tf-ink-3)) calc(var(--tf-fill, 0) * 1turn), transparent 0);
  /* Clipped to the padding box, which leaves a hairline of canvas between the
     ring and the pie. Without it a part-filled circle reads as a smudge. */
  background-clip: content-box;
  background-origin: content-box;
  background-repeat: no-repeat;
  transition: box-shadow var(--tf-ease), background-color var(--tf-ease), border-color var(--tf-ease);
}
/* Nothing has happened yet: outline only, and the little colour there is
   drains away so a backlog stays quiet next to work in flight. */
.tf-status-dot.is-not-started {
  background-color: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 14%, transparent);
  background-image: none;
}
.tf-status-dot.is-complete {
  background-color: var(--tf-c, var(--tf-ink-3));
  background-image: none;
  background-clip: border-box;
  background-origin: border-box;
}
/* Called off: empty, like a status nothing has started — so the ring is
   dashed, because those two empties mean opposite things and a board full of
   circles has no other way to tell them apart. */
.tf-status-dot.is-cancelled {
  border-style: dashed;
  background-color: transparent;
  background-image: none;
}
button.tf-status-dot { cursor: pointer; }
/* Hover lifts a ring around the dot rather than filling it: the fill is a
   fact about the task, and pointing at it does not move the work on. */
button.tf-status-dot:hover {
  background-color: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 38%, transparent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, transparent);
}
button.tf-status-dot.is-complete:hover { background-color: var(--tf-c, var(--tf-ink-3)); }
.tf-status-dot.is-picking { box-shadow: 0 0 0 3px color-mix(in srgb, var(--tf-c) 30%, transparent); }
/* The type icon once it can be changed: a button drawn as the badge it wraps,
   with the same quiet lift under the pointer the status dot gets. */
.tf-type-pick {
  display: inline-flex;
  align-items: center;
  flex: 0 0 auto;
  padding: 2px;
  margin: -2px;
  border: 0;
  border-radius: 4px;
  background: none;
  color: inherit;
  line-height: 1;
  transition: background-color var(--tf-ease), box-shadow var(--tf-ease);
}
.tf-row button.tf-type-pick { cursor: pointer; }
.tf-type-pick:hover, .tf-type-pick.is-picking { background: var(--tf-hover); }
.tf-type-pick.is-picking { box-shadow: 0 0 0 2px var(--tf-violet-tint); }
.tf-cell-title .tf-type { gap: 0; }
.tf-cell-title .tf-type i { font-size: 0.9286rem; }

/* Who opened the task: one face, one name, and the name gives way first when
   the column is dragged narrow. */
.tf-created-by { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 100%; }
.tf-created-by .tf-avatar { flex: 0 0 auto; }
.tf-created-by-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Assignees: people together, agents apart. */
.tf-assign-cell { display: flex; align-items: center; gap: 10px; width: 100%; }
/* Who is on it from outside, drawn as the assignee stack beside it is: the
   marks overlap into one run so the column reads as one thing at a glance,
   however many customers a ticket has been shown to. */
.tf-sh-stack { display: inline-flex; align-items: center; padding-left: 5px; }
.tf-sh-stack .tf-ws-mark, .tf-sh-stack .tf-avatar { margin-left: -5px; box-shadow: 0 0 0 2px var(--tf-canvas); }
.tf-sh-stack .tf-assignees-more { box-shadow: 0 0 0 2px var(--tf-canvas); }
.tf-assign-agents { display: inline-flex; align-items: center; gap: 2px; margin-left: auto; }
.tf-assign-agents .tf-avatar { box-shadow: 0 0 0 2px var(--tf-canvas); }

/* Selection is visible before you reach for it. */
.tf-row .form-check-input {
  opacity: 1;
  transition: border-color var(--tf-ease), background-color var(--tf-ease);
}
/* Only the empty box is outlined — a ticked one keeps its violet fill. */
.tf-row .form-check-input:not(:checked) { border-color: #c3c7d0; background-color: transparent; }
.tf-row:hover .form-check-input:not(:checked) { border-color: var(--tf-violet); }
[data-theme="dark"] .tf-row .form-check-input:not(:checked) { border-color: #4a4460; }
.tf-row { cursor: pointer; }
.tf-row .tf-cell-edit, .tf-row a, .tf-row button, .tf-row input, .tf-row select { cursor: auto; }
.tf-row a, .tf-row button.tf-status-dot, .tf-row .tf-drag { cursor: pointer; }

/* Status picker */
.tf-picker-slim { width: 12.5rem; }
/* The status on the task panel: the circle and the pill side by side, sized
   to the same line the select it replaced stood on, and a pointer once it is
   something you can change. */
.tf-panel-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 31px;
  padding: 2px 6px;
  margin-left: -6px;
  border-radius: 4px;
}
/* The heading over each kind of status in the picker: what the statuses under
   it mean, set the way the picker's own head is but lighter, so the four
   groups read as one list with a fold in it rather than four lists. */
.tf-picker-group {
  padding: 8px 8px 3px;
  font-size: 0.7143rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
}
.tf-picker-group + .tf-picker-option { margin-top: 1px; }
.tf-picker-list > .tf-picker-group:first-child { padding-top: 4px; }
/* The picker shows the same value the cell will show, so the pills match the
   cell: full width of the row they sit in rather than hugging their text. A
   list of colours you are choosing between reads as a list of colours. */
.tf-picker-slim .tf-picker-pick .tf-status {
  display: block;
  width: 100%;
  height: 24px;
  line-height: 24px;
  padding: 0 9px;
  border-radius: 5px;
  overflow: hidden;
  text-overflow: ellipsis;
  background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, var(--tf-chip-base));
}
.tf-picker-pick {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  border: 0;
  background: none;
  color: var(--tf-ink);
  text-align: left;
}
.tf-picker-pick.is-current { background: var(--tf-violet-tint); color: var(--tf-violet-press); font-weight: 600; }

/* Bulk bar */
.tf-bulk-dock { bottom: 46px; }
.tf-bulk {
  gap: 0;
  row-gap: 4px;
  padding: 6px 8px;
  border-radius: 12px;
  background: #1c1e24;
  border: 1px solid rgba(255, 255, 255, 0.08);
}
.tf-bulk-count {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 26px;
  margin-right: 6px;
  padding: 0 5px 0 9px;
  border-radius: 8px;
  background: #33363f;
  font-size: 0.8571rem;
  font-weight: 600;
  white-space: nowrap;
}
.tf-bulk-x {
  display: inline-grid;
  place-items: center;
  width: 18px;
  height: 18px;
  border: 0;
  border-radius: 5px;
  background: none;
  color: #b8bcc6;
}
.tf-bulk-x:hover { background: rgba(255, 255, 255, 0.14); color: #fff; }
.tf-bulk-act {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: 0 0 auto;
  height: 28px;
  padding: 0 7px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: #d7dae1;
  font-size: 0.8571rem;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--tf-ease), color var(--tf-ease);
}
.tf-bulk-act:hover { background: rgba(255, 255, 255, 0.10); color: #fff; }
.tf-bulk-act .tf-icon { color: #a2a7b3; }
.tf-bulk-act:hover .tf-icon { color: #fff; }
.tf-bulk-act.is-danger .tf-icon { color: #ff8f92; }
.tf-bulk-act.is-danger:hover { background: rgba(229, 72, 77, 0.18); }
.tf-bulk-act.is-danger:hover .tf-icon { color: #ff8f92; }
/* The native select sits invisibly over its label, so one click opens it.
   The date input is laid over its label the same way, and the bar opens its
   calendar on the click since the input alone would only focus. */
.tf-bulk-act select, .tf-bulk-act input[type="date"] {
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 0;
  border: 0;
  cursor: pointer;
}
/* The one control in the bar that is a menu rather than a select. Assigning is
   several answers — who, and whether they join the people already there — and a
   native select can hold neither. */
.tf-bulk-assign { display: inline-flex; flex: 0 0 auto; }
.tf-bulk-assign .dropdown-toggle::after { margin-left: 1px; opacity: 0.6; }
.tf-bulk-sep { width: 1px; height: 18px; margin: 0 5px; background: rgba(255, 255, 255, 0.14); }


/* =========================================================================
   Detail pane properties, the task page's editable properties, and tags
   that stay on one line.
   ========================================================================= */

/* Two properties to a row: a task's shape is one glance, not a long ladder. */
.tf-meta-fields {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px 16px;
  padding: 6px 0 16px;
  border-bottom: 1px solid var(--tf-line);
  margin-bottom: 16px;
}
.tf-field { min-width: 0; }
.tf-field.is-wide { grid-column: 1 / -1; }
.tf-field-key {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--tf-ink-3);
  margin-bottom: 5px;
}
.tf-field-key .tf-icon { color: var(--tf-ink-3); }
.tf-field-val { min-width: 0; }
.tf-field-val .form-select,
.tf-field-val .form-control { height: 32px; width: 100%; max-width: none; padding-top: 0; padding-bottom: 0; }
.tf-field-val .tf-chip { max-width: 100%; }
.tf-field-val .tf-tag-add input { max-width: none; flex: 1; }

/* The task page's Properties card carries the same click-to-edit cells the
   board does, so they need room to be clicked. */
.tf-meta-grid-flat { border-bottom: 0; margin-bottom: 0; padding: 0; }
.tf-meta-grid .tf-meta-val.tf-cell-edit {
  padding: 3px 6px;
  margin: -3px -6px;
  border-radius: 5px;
  min-height: 28px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px;
}
.tf-meta-grid .tf-meta-val.tf-cell-edit:hover { box-shadow: inset 0 0 0 1px var(--tf-line); background: var(--tf-hover); }

/* Tags ride at the end of the task's title rather than in a column of their
   own. They keep to the row's one line, and they never take space from the
   title itself — the title truncates first, because a task you cannot read
   the name of is worse than a tag you cannot read the name of. */
.tf-tag-row.is-one-line { flex-wrap: nowrap; }
.tf-title-tags {
  display: inline-flex;
  align-items: center;
  flex: 0 1 auto;
  flex-wrap: nowrap;
  gap: 4px;
  min-width: 0;
  max-width: 14rem;
  overflow: hidden;
  padding: 2px 4px;
  margin: -2px -4px;
  border-radius: 5px;
}
.tf-title-tags .tf-tag { max-width: 6.5rem; }
.tf-title-tags.tf-cell-edit:hover { background: var(--tf-hover); box-shadow: inset 0 0 0 1px var(--tf-line); }
/* An untagged row has nothing to show and would have nothing to click, so it
   keeps a mark that only surfaces under the pointer. */
.tf-tag-hint { display: inline-grid; place-items: center; color: var(--tf-ink-3); opacity: 0; transition: opacity var(--tf-ease); }
.tf-row:hover .tf-tag-hint, .tf-title-tags:hover .tf-tag-hint { opacity: 1; }
.tf-title-tags:has(.tf-tag-hint) { padding-inline: 2px; margin-inline: -2px; }
.tf-tag-x {
  flex: 0 0 auto;
  margin-right: -2px;
  border-radius: 999px;
}
.tf-tag-x:hover { background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, transparent); }
.tf-row .tf-tag-x { cursor: pointer; }

/* Work handed over as one piece wears its batch in front of its status: it is
   the first thing to know about the row, and the mark is the way back to the
   batch it went out in. The slot is one fixed width whether or not there is a
   batch to show, so every status circle in the list stands on the same line —
   a column that reads down the page is worth more than a ragged edge, and a
   package mark in the batch's own colour stands out of the white space either
   way. Nothing in here may change that width: the X is taken out of the flow
   below, and the marks are centred rather than sized to fit. */
.tf-row-batch {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  width: 18px;
  margin-left: -2px;
  padding: 1px;
  border-radius: 5px;
}
.tf-row-batch.tf-cell-edit:hover { background: var(--tf-hover); box-shadow: inset 0 0 0 1px var(--tf-line); }
/* Background only while the picker is open: the violet ring every picking cell
   wears is the one that should show. */
.tf-row-batch.is-picking { background: var(--tf-hover); }
.tf-batch-mark {
  display: inline-grid;
  place-items: center;
  border-radius: 4px;
  line-height: 0;
  color: var(--tf-c, var(--tf-ink-3));
}
.tf-batch-mark:hover { background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 20%, transparent); }
/* The X rides on the corner of the mark rather than beside it: a batched row
   has to take up exactly as much room as an unbatched one, and it only ever
   shows while the row is under the pointer, so it wears that row's own
   background and reads as cut out of the package. */
.tf-batch-x {
  position: absolute;
  top: -4px;
  right: -6px;
  display: inline-grid;
  place-items: center;
  width: 14px;
  height: 14px;
  border: 0;
  padding: 0;
  background: var(--tf-row-hover);
  color: var(--tf-ink-3);
  line-height: 0;
  border-radius: 999px;
  opacity: 0;
  transition: opacity var(--tf-ease);
}
.tf-row:hover .tf-batch-x, .tf-batch-x:focus-visible { opacity: 1; }
.tf-batch-x:hover { background: color-mix(in srgb, var(--tf-c, var(--tf-ink-3)) 22%, var(--tf-row-hover)); color: var(--tf-ink); }
.tf-row .tf-batch-x { cursor: pointer; }
/* A row in no batch has nothing to show and would have nothing to click, so —
   as with an untagged row — it keeps a mark that only surfaces under the
   pointer. Without it the only way into a batch from here is to select the
   row and use the bulk bar. */
.tf-batch-hint { display: inline-grid; place-items: center; color: var(--tf-ink-3); line-height: 0; opacity: 0; transition: opacity var(--tf-ease); }
/* Both marks are asked for small and would otherwise come back at the icon
   default, which is wider than the slot they sit in. */
.tf-batch-hint .tf-icon, .tf-batch-x .tf-icon { width: 12px; height: 12px; }
/* The pointer is over the picker, not the row, for as long as it is open —
   the mark it is anchored to has to stay lit or it reads as nothing. */
.tf-row:hover .tf-batch-hint, .tf-row-batch:hover .tf-batch-hint, .tf-row-batch.is-picking .tf-batch-hint { opacity: 1; }
/* What state a batch is in, on the right of its line in the picker. */
.tf-picker-side { flex: 0 0 auto; color: var(--tf-ink-3); font-size: 0.7857rem; }

/* Leaving full screen: back to the row this task came from. */
.tf-minimise { margin-left: 4px; align-self: center; }
.tf-minimise:hover { background: var(--tf-hover); color: var(--tf-ink); }

/* ------------------------------------------------- dropdown field values --- */

.tf-cf-options { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.tf-cf-option { display: flex; align-items: center; gap: 6px; }
.tf-cf-option input[type="color"] { flex: 0 0 auto; width: 34px; padding: 3px; }
.tf-cf-option input[type="text"] { flex: 1 1 6rem; min-width: 0; }
.tf-cf-preview { flex: 0 0 auto; max-width: 8rem; overflow: hidden; text-overflow: ellipsis; }
.tf-cf-add { padding-left: 2px; }
.tf-cf-add svg { margin-right: 2px; vertical-align: -2px; }

/* ------------------------------------------------------- storage badges --- */

/* Whose bucket a workspace's files land in. BYOS is the exception, so it is
   the one that carries colour — a platform workspace should read as quiet. */
.tf-store-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  white-space: nowrap;
  background: var(--tf-hover);
  color: var(--tf-ink-2);
  box-shadow: inset 0 0 0 1px var(--tf-line);
}
.tf-store-badge.is-byos {
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  box-shadow: inset 0 0 0 1px var(--tf-violet-tint-2);
}
.tf-store-badge .tf-icon { color: currentColor; }
.tf-store-note { font-size: 0.7857rem; color: var(--tf-ink-3); white-space: nowrap; }

/* =========================================================================
   Live work: the timeline a task keeps, who is typing into it, and the two
   pages that read across a workspace — one person's own work wherever it
   lives, and everything that has happened here.
   ========================================================================= */

/* ------------------------------------------------------------ timeline --- */

/* Comments and what happened share one column. A comment is the loud thing on
   it and keeps the weight it always had; an activity line is deliberately
   quieter — it is context for the conversation, not part of it. */
.tf-timeline { display: flex; flex-direction: column; }
.tf-timeline-empty { margin: 6px 0 0; }
.tf-tl-entry { display: flex; gap: 10px; padding: 9px 0; }
.tf-tl-comment + .tf-tl-comment,
.tf-tl-act + .tf-tl-comment,
.tf-tl-comment + .tf-tl-act { border-top: 1px solid var(--tf-line-soft); }

.tf-tl-act { align-items: flex-start; padding: 5px 0; }
.tf-tl-mark {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 999px;
  background: var(--tf-hover);
  color: var(--tf-ink-3);
}
.tf-tl-mark.is-agent { background: var(--tf-violet-tint); color: var(--tf-violet-hover); }
[data-theme="dark"] .tf-tl-mark.is-agent { background: rgba(123, 104, 238, 0.22); color: var(--tf-violet-light); }

.tf-tl-line {
  min-width: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 5px;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  line-height: 22px;
}
.tf-tl-who { font-weight: 600; color: var(--tf-ink); }
.tf-tl-val { font-weight: 600; color: var(--tf-ink); font-style: normal; }
a.tf-tl-val, .tf-tl-line a { color: var(--tf-violet-hover); font-weight: 600; }
.tf-tl-when, .tf-tl-dim { font-size: 0.7857rem; color: var(--tf-ink-3); }
.tf-tl-when { margin-left: 2px; }

/* Which door a change came in through. A person clicking in the web UI is the
   unremarkable case and says nothing; an agent or a webhook is worth a word. */
.tf-tl-src {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0 5px;
  height: 17px;
  border-radius: 999px;
  border: 1px solid var(--tf-line);
  background: var(--tf-shell);
  font-size: 0.7143rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--tf-ink-2);
}
.tf-tl-src.is-agent {
  border-color: var(--tf-violet-tint-2);
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
}
[data-theme="dark"] .tf-tl-src.is-agent {
  border-color: rgba(123, 104, 238, 0.4);
  background: rgba(123, 104, 238, 0.18);
  color: var(--tf-violet-light);
}
.tf-tl-via { font-size: 0.7143rem; color: var(--tf-ink-3); }

/* -------------------------------------------------------------- typing --- */

/* Only true while it is on screen. Nothing about it is stored. */
.tf-typing {
  display: flex;
  align-items: center;
  gap: 7px;
  min-height: 22px;
  padding: 2px 2px 6px;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
}
.tf-typing[hidden] { display: none; }
.tf-typing-dots { display: inline-flex; gap: 3px; }
.tf-typing-dots i {
  width: 4px;
  height: 4px;
  border-radius: 999px;
  background: var(--tf-violet);
  animation: tf-typing 1.1s infinite ease-in-out;
}
.tf-typing-dots i:nth-child(2) { animation-delay: 0.15s; }
.tf-typing-dots i:nth-child(3) { animation-delay: 0.3s; }
@keyframes tf-typing {
  0%, 60%, 100% { opacity: 0.25; transform: translateY(0); }
  30% { opacity: 1; transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .tf-typing-dots i { animation: none; opacity: 0.6; }
}

/* ---------------------------------------------------------- board news --- */

/* A task somebody else added. Where it belongs on your board depends on how
   you have grouped and filtered it, so it is named rather than slotted in. */
.tf-board-news:empty { display: none; }
.tf-board-news { padding: 6px 16px 0; display: flex; flex-direction: column; gap: 4px; }
.tf-board-news-item {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 10px;
  border: 1px dashed var(--tf-line);
  border-radius: var(--tf-radius);
  background: var(--tf-shell);
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}
.tf-board-news-go {
  margin-left: auto;
  border: 0;
  background: none;
  padding: 0;
  font-size: 0.8571rem;
  font-weight: 600;
  color: var(--tf-violet-hover);
  cursor: pointer;
}
.tf-board-news-go:hover { text-decoration: underline; }

/* A row somebody else added, dropped straight into the group it belongs to.
   Nobody asked for it, so it lands lit and fades back into the board rather
   than appearing as though it had been there all along. */
.tf-row.is-arriving { animation: tf-arrive 1.4s ease-out; }
@keyframes tf-arrive {
  from { background-color: color-mix(in srgb, var(--tf-violet) 16%, transparent); }
  to { background-color: transparent; }
}
@media (prefers-reduced-motion: reduce) {
  .tf-row.is-arriving { animation-duration: 1ms; }
}

/* ------------------------------------------------------------- my work --- */

.tf-mywork { padding-bottom: 40px; }
.tf-mywork .tf-toolbar { border-bottom: 1px solid var(--tf-line); }

/* One card per workspace, so "how much is waiting over there" is answered
   before you have to go and look. */
.tf-ws-strip {
  display: flex;
  gap: 8px;
  flex-wrap: wrap;
  padding: 12px 16px 14px;
  border-bottom: 1px solid var(--tf-line-soft);
}
.tf-ws-card {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 168px;
  padding: 8px 12px 8px 9px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  background: var(--tf-canvas);
  color: var(--tf-ink);
  text-decoration: none;
  transition: border-color var(--tf-ease), box-shadow var(--tf-ease);
}
.tf-ws-card:hover { text-decoration: none; border-color: var(--tf-violet-tint-2); box-shadow: var(--tf-shadow-1); }
.tf-ws-card.is-on { border-color: var(--tf-violet); box-shadow: 0 0 0 1px var(--tf-violet); }
.tf-ws-card.is-quiet { opacity: 0.62; }
.tf-ws-card-mark { flex: 0 0 auto; }
.tf-ws-card-all { display: grid; place-items: center; width: 26px; height: 26px; border-radius: var(--tf-radius-sm); background: var(--tf-ink); color: #fff; }
.tf-ws-card-body { display: flex; flex-direction: column; min-width: 0; line-height: 1.3; }
.tf-ws-card-name { font-weight: 600; font-size: 0.8571rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tf-ws-card-count { font-size: 0.7857rem; color: var(--tf-ink-3); font-variant-numeric: tabular-nums; }

.tf-mywork-groups { padding: 4px 0 0; }
.tf-mywork-group + .tf-mywork-group { margin-top: 6px; }
.tf-mywork-group-head {
  position: sticky;
  top: 0;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 10px 16px 7px;
  background: var(--tf-canvas);
}
.tf-mywork-group-head h2 { font-size: 0.8571rem; margin: 0; letter-spacing: 0.01em; }
.tf-mywork-list { list-style: none; margin: 0; padding: 0; }

.tf-mywork-row {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: var(--tf-row-h);
  padding: 4px 16px;
  border-top: 1px solid var(--tf-line-soft);
}
.tf-mywork-row:hover { background: var(--tf-row-hover); }
.tf-mywork-row.is-done .tf-mywork-name { color: var(--tf-ink-3); text-decoration: line-through; }
.tf-mywork-name {
  min-width: 0;
  font-size: 0.9286rem;
  color: var(--tf-ink);
  text-decoration: none;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-mywork-name:hover { color: var(--tf-violet-press); text-decoration: none; }
.tf-mywork-side { margin-left: auto; display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* Questions waiting on the reader. Not the my-work row: what matters here is
   what was asked, so the question itself gets the line and the task it was
   asked on sits under it as context. */
.tf-qlist { list-style: none; margin: 0; padding: 0; }
.tf-qlist-row {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 11px 16px;
  border-top: 1px solid var(--tf-line-soft);
}
.tf-qlist-row:hover { background: var(--tf-row-hover); }
.tf-qlist-mark { display: inline-flex; flex: 0 0 auto; margin-top: 1px; color: var(--tf-amber); }
/* A question you asked that somebody has settled. It stays on the page —
   finding out is the reason to come here — but it stops asking for anything,
   so the mark goes green and the row sits back. */
.tf-qlist-mark.is-answered { color: var(--tf-green); }
.tf-qlist-row.is-settled .tf-qlist-text { color: var(--tf-ink-2); }
.tf-qlist-settled { color: var(--tf-green); }
.tf-qlist-waiting { display: inline-flex; align-items: center; gap: 4px; color: var(--tf-amber); }
.tf-qlist-body { min-width: 0; flex: 1 1 auto; }
.tf-qlist-said { display: flex; align-items: baseline; gap: 7px; flex-wrap: wrap; }
.tf-qlist-who { font-weight: 600; font-size: 0.8571rem; }
.tf-qlist-text {
  display: block;
  margin: 2px 0 4px;
  color: var(--tf-ink);
  text-decoration: none;
}
.tf-qlist-text:hover { color: var(--tf-violet-press); text-decoration: none; }
.tf-qlist-where {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  font-size: 0.7857rem;
  color: var(--tf-ink-2);
}
.tf-qlist-task { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 28rem; }
.tf-mywork-git { display: inline-flex; align-items: center; gap: 3px; font-size: 0.7857rem; color: var(--tf-ink-3); }

/* Which workspace a line came from, kept to the width of a mark plus a word:
   on this page it is the one column that is never the same twice. */
.tf-mywork-ws { display: inline-flex; align-items: center; gap: 6px; min-width: 0; }
.tf-mywork-ws-name {
  font-size: 0.7857rem;
  color: var(--tf-ink-2);
  max-width: 10rem;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-mywork-empty { padding: 34px 16px; }

@media (max-width: 900px) {
  .tf-mywork-side { gap: 6px; }
  .tf-mywork-ws-name, .tf-mywork-row .tf-tag, .tf-mywork-git { display: none; }
}

/* --------------------------------------------------------------- feed --- */

.tf-feed-day + .tf-feed-day { margin-top: 18px; }
.tf-feed-date {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--tf-ink-2);
  padding-bottom: 6px;
}
.tf-feed {
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  background: var(--tf-canvas);
  overflow: hidden;
}
.tf-feed-row { display: flex; align-items: flex-start; gap: 10px; padding: 8px 12px; }
.tf-feed-row + .tf-feed-row { border-top: 1px solid var(--tf-line-soft); }
.tf-feed-row:hover { background: var(--tf-row-hover); }
.tf-feed-body { min-width: 0; flex: 1; }
.tf-feed-where { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.tf-feed-task {
  font-size: 0.8571rem;
  font-weight: 500;
  color: var(--tf-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tf-feed-task:hover { color: var(--tf-violet-press); }
.tf-feed-when {
  flex: 0 0 auto;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
  font-variant-numeric: tabular-nums;
  padding-top: 2px;
}

/* ---------------------------------------------------------- statuses page --

   The statuses list is a settings table you can drag, so its handle behaves
   the way the board's does: out of the way until the row is under the mouse. */
.tf-status-row:hover .tf-drag { opacity: 1; }
.tf-status-cell { display: flex; align-items: center; gap: 9px; }

/* What a status means for the work in it, said in a word next to the circle
   that says it in a shape. */
.tf-status-kind {
  display: inline-flex;
  align-items: center;
  padding: 2px 9px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  font-size: 0.7857rem;
  font-weight: 500;
  color: var(--tf-ink-2);
  white-space: nowrap;
}
.tf-status-kind.is-not-started { color: var(--tf-ink-3); }
.tf-status-kind.is-in-progress { color: var(--tf-blue); border-color: color-mix(in srgb, var(--tf-blue) 30%, var(--tf-line)); }
.tf-status-kind.is-completed { color: var(--tf-green); border-color: color-mix(in srgb, var(--tf-green) 30%, var(--tf-line)); }
.tf-status-kind.is-cancelled {
  color: var(--tf-ink-3);
  border-style: dashed;
  text-decoration: line-through;
}

.tf-copy-sources { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.tf-copy-sources li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 10px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
}

/* ------------------------------------------------------------- a modal ----

   One dialog per page, filled by a turbo-frame. Padding sits on the parts
   inside rather than the dialog itself, so a click anywhere the content is
   not is a click on the backdrop, and closes it. */
.tf-dialog {
  width: min(38rem, calc(100vw - 2rem));
  max-height: min(44rem, calc(100vh - 3rem));
  padding: 0;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
  background: var(--tf-canvas);
  color: var(--tf-ink);
  box-shadow: var(--tf-shadow-2);
  overflow: hidden;
}
.tf-dialog::backdrop { background: rgba(20, 18, 28, 0.42); }
/* The node a dialog is swapped in and out of. It has to be an element so a
   stream can replace it — replacing the dialog is how the dialog closes — but
   it must not be a box: these mount inside a page head, which is a flex row of
   exactly two things, and a third empty child would push them apart. */
.tf-dialog-mount { display: contents; }
/* The one place a chevron points the other way: "back to where you were" on a
   run, where every other arrow in the set already has a job. */
.tf-back-arrow { transform: rotate(180deg); }

/* Only when open: a bare `display: flex` would beat the UA rule that keeps a
   closed dialog off the screen. */
.tf-dialog[open] { display: flex; flex-direction: column; }

/* The head and the foot are fixed; the body between them takes what is left
   and scrolls. The chain has to run unbroken from the dialog down to the
   body, because a link in it that sizes to its content — the turbo-frame, or
   the form wrapping the body and the foot — hands the body an unbounded
   height, and the confirm button ends up below the bottom edge. `min-height:
   0` at each step is what lets a flex child be shorter than its content. */
.tf-dialog[open] > turbo-frame,
.tf-dialog[open] > turbo-frame > form {
  display: flex;
  flex-direction: column;
  flex: 1 1 auto;
  min-height: 0;
}
.tf-dialog-head, .tf-dialog-foot { flex: 0 0 auto; }
.tf-dialog-head {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  padding: 16px 18px;
  border-bottom: 1px solid var(--tf-line);
}
.tf-dialog-head h2 { margin: 0; font-size: 1.0714rem; font-weight: 600; }
.tf-dialog-head .tf-icon-btn { margin-left: auto; }
.tf-dialog-sub { margin: 3px 0 0; font-size: 0.8571rem; color: var(--tf-ink-3); }
.tf-dialog-body { flex: 1 1 auto; min-height: 0; padding: 18px; overflow-y: auto; overscroll-behavior: contain; }
.tf-dialog-foot {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  padding: 13px 18px;
  border-top: 1px solid var(--tf-line);
  background: var(--tf-shell);
}

.tf-copy-block + .tf-copy-block { margin-top: 20px; padding-top: 20px; border-top: 1px solid var(--tf-line-soft); }
.tf-copy-title { margin: 0 0 8px; font-size: 0.7857rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: var(--tf-ink-3); }
.tf-copy-list, .tf-copy-map { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.tf-copy-list li { display: flex; align-items: center; gap: 9px; }
.tf-copy-note { margin-left: auto; font-size: 0.8571rem; }
.tf-copy-tag { padding: 2px 8px; border-radius: 999px; font-size: 0.7857rem; font-weight: 500; }
.tf-copy-tag.is-new { color: var(--tf-green); background: color-mix(in srgb, var(--tf-green) 12%, transparent); }
.tf-copy-tag.is-changed { color: var(--tf-amber); background: color-mix(in srgb, var(--tf-amber) 14%, transparent); }

/* Where the tasks on a status about to disappear are being sent. */
.tf-copy-map li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 13rem);
  align-items: center;
  gap: 10px;
}
.tf-copy-from { display: flex; align-items: center; gap: 9px; min-width: 0; }
.tf-copy-arrow { color: var(--tf-ink-3); display: inline-grid; place-items: center; }
@media (max-width: 34rem) {
  .tf-copy-map li { grid-template-columns: minmax(0, 1fr); }
  .tf-copy-arrow { display: none; }
}

/* ------------------------------------------------ inherited sets --- */

/* The buttons that sit under a settings screen rather than beside its title:
   a confirm, a cancel, the one dangerous thing at the bottom of the page. */
.tf-page-actions { display: flex; align-items: center; gap: 8px; }

/* A level reading somebody else's statuses. The screen still shows them —
   what applies here is the question, and something always does — but nothing
   on it can be changed from here, so the rows read as quotations. */
.tf-inherit-note {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 10px 13px;
  border: 1px solid var(--tf-line);
  border-left: 3px solid var(--tf-violet-hover);
  border-radius: 8px;
  background: var(--tf-violet-tint);
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
}
tr.is-inherited td { color: var(--tf-ink-3); }
tr.is-inherited .tf-status, tr.is-inherited .tf-tag { opacity: 0.75; }

/* ---------------------------------------------------------- impersonation --
   Somebody else's account, worn over your own. It takes a strip off the top of
   the window rather than floating over it, because a bar you can scroll past
   is a bar you will forget about. Nothing else in the app is this colour. */
.tf-imp-bar {
  height: var(--tf-imp-h);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0 14px;
  background: #8b1d18;
  color: #fff;
  font-size: 0.8214rem;
  line-height: 1;
}
.tf-imp-bar .tf-icon { color: #ffd9d6; flex: 0 0 auto; }
.tf-imp-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-imp-quiet { opacity: 0.72; font-family: var(--tf-mono); font-size: 0.9em; }
.tf-imp-bar form { margin: 0; }
.tf-imp-stop {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  border: 1px solid rgba(255, 255, 255, 0.45);
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  border-radius: var(--tf-radius-sm);
  padding: 3px 9px;
  font-size: 0.7857rem;
  font-weight: 600;
  white-space: nowrap;
}
.tf-imp-stop:hover { background: rgba(255, 255, 255, 0.24); color: #fff; }
body.is-impersonating .tf-app-frame,
body.is-impersonating .tf-rail,
body.is-impersonating .tf-side { height: calc(100vh - var(--tf-imp-h)); }

/* -------------------------------------------------------------- everything --
   Reading every workspace at once. What is borrowed from another workspace
   says so — in the sidebar as a heading over its projects, on a card as a mark
   beside the name — because a list called "Bugs" means nothing on its own. */
.tf-ws-mark-all { background: var(--tf-violet); display: grid; place-items: center; }
.tf-menu-note { margin-left: auto; opacity: 0.7; font-size: 0.7857rem; }
/* A batch is named by its title, which can be a sentence; the reference stays
   readable at the end of the row however long the title runs. */
.tf-batch-menu { max-width: 22rem; }
.tf-batch-menu-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-side-ws {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 10px 12px 4px;
  color: var(--tf-nav-fg-dim);
  font-size: 0.7857rem;
  font-weight: 600;
  letter-spacing: 0.02em;
}
.tf-side-ws-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-side-ws .tf-side-add { margin-left: auto; }
.tf-card-ws {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 0.7857rem;
  color: var(--tf-ink-3);
}
.tf-group-head {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 22px 0 10px;
}
.tf-group-head h2 { font-size: 1rem; font-weight: 600; margin: 0; }
.tf-group-head:first-child { margin-top: 0; }

@media (max-width: 991.98px) {
  /* Last word on the height, so it beats the plain-vh rule above rather than
     losing to it on source order. */
  body.is-impersonating .tf-app-frame,
  body.is-impersonating .tf-side { height: calc(100dvh - var(--tf-imp-h)); }
}

/* Merging several tasks into one. The plan is read before it is run, so the
   choices and their consequences sit on the page together rather than behind
   one another. */
.tf-merge-modes { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }

.tf-merge-opt {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  padding: 12px 14px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  cursor: pointer;
}
.tf-merge-opt:hover { background: var(--tf-hover); }
.tf-merge-opt.is-on { border-color: var(--tf-violet); background: var(--tf-violet-tint); }
.tf-merge-opt input[type="radio"] { margin-top: 3px; flex: none; }

.tf-merge-opt-text { display: grid; gap: 4px; min-width: 0; }
.tf-merge-opt-text > .tf-dim { font-size: 0.8571rem; line-height: 1.45; }
.tf-merge-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.tf-merge-ref { max-width: 16rem; margin-top: 4px; }

.tf-merge-blurb { font-size: 0.8571rem; margin: -2px 0 8px; }

.tf-merge-plan { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.tf-merge-plan li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 2px 9px;
  align-items: start;
  padding: 9px 0;
  border-top: 1px solid var(--tf-line-soft);
}
.tf-merge-plan li:first-child { border-top: 0; padding-top: 0; }
.tf-merge-mark { color: var(--tf-ink-3); display: flex; align-items: center; height: 20px; }
.tf-merge-plan .tf-merge-note { grid-column: 2; color: var(--tf-ink-2); font-size: 0.8571rem; }
/* A row that cannot move, and a row that has nowhere to move to, are both
   worth reading past — neither is an error somebody has to fix. */
.tf-merge-plan li.is-refused .tf-merge-mark { color: var(--tf-amber); }
.tf-merge-plan li.is-refused .tf-merge-note { color: var(--tf-amber); }
.tf-merge-plan li.is-quiet { opacity: 0.6; }

.tf-merge-new { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: 12px; margin-bottom: 16px; }
.tf-merge-actions { display: flex; justify-content: flex-end; gap: 8px; }

@media (max-width: 720px) {
  .tf-merge-new { grid-template-columns: minmax(0, 1fr); }
}

/* The menu behind a right-click on a project or a list. Moved to the body and
   placed at the pointer, so it is never clipped by the panel it came from. */
.tf-ctx {
  position: fixed;
  z-index: 1080;
  min-width: 12rem;
  max-width: 18rem;
  padding: 5px;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow-2);
  color: var(--tf-ink);
}
.tf-ctx[hidden] { display: none; }

.tf-ctx-head {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 9px 7px;
  color: var(--tf-ink-3);
  font-size: 0.7857rem;
  border-bottom: 1px solid var(--tf-line-soft);
  margin-bottom: 4px;
}
.tf-ctx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; color: var(--tf-ink-2); }

.tf-ctx-item {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 9px;
  border-radius: calc(var(--tf-radius) - 2px);
  color: var(--tf-ink);
  text-decoration: none;
  font-size: 0.8571rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.tf-ctx-item:hover, .tf-ctx-item:focus-visible { background: var(--tf-violet-tint); color: var(--tf-ink); }
.tf-ctx-item svg { flex: none; color: var(--tf-ink-3); }
.tf-ctx-item:hover svg { color: var(--tf-violet); }

.tf-ctx-sep { height: 1px; background: var(--tf-line-soft); margin: 4px 2px; }

/* The row menu's items are buttons — each one submits the selection — and
   some are shown or hidden for the selection it opens on. display: flex above
   would otherwise beat the hidden attribute. */
button.tf-ctx-item {
  width: 100%;
  border: 0;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.tf-ctx-item[hidden], .tf-ctx-sep[hidden] { display: none; }

/* An item holding a menu of its own opens it beside itself, or to the left
   when the menu is too near the right edge for it. Opened by context-menu
   rather than :hover, so it can be kept on screen and does not snap shut
   when the pointer cuts across the item below on its way in. */
.tf-ctx-parent { position: relative; }
.tf-ctx-parent.is-open > .tf-ctx-item { background: var(--tf-violet-tint); }
.tf-ctx-more { display: inline-flex; margin-left: auto; padding-left: 12px; }
.tf-ctx-sub {
  display: none;
  position: absolute;
  top: -6px;
  left: calc(100% + 6px);
  min-width: 13rem;
  max-width: 20rem;
  max-height: min(60vh, 24rem);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 5px;
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  box-shadow: var(--tf-shadow-2);
}
.tf-ctx-parent.is-open > .tf-ctx-sub { display: block; }
.tf-ctx.is-flip .tf-ctx-sub { left: auto; right: calc(100% + 6px); }

/* A tick where the whole selection already is, a dash where only some of it
   is, and the room kept either way so the names line up. */
.tf-ctx-tick { display: inline-flex; width: 14px; flex: none; justify-content: center; }
[aria-checked] > .tf-ctx-tick svg { visibility: hidden; }
[aria-checked="true"] > .tf-ctx-tick svg { visibility: visible; color: var(--tf-violet); }
[aria-checked="mixed"] > .tf-ctx-tick { position: relative; }
[aria-checked="mixed"] > .tf-ctx-tick::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 3px;
  right: 3px;
  height: 2px;
  margin-top: -1px;
  border-radius: 1px;
  background: var(--tf-ink-3);
}
.tf-ctx-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tf-ctx-aside { margin-left: auto; padding-left: 12px; color: var(--tf-ink-3); font-size: 0.7857rem; }
.tf-ctx-group {
  padding: 7px 9px 3px;
  color: var(--tf-ink-3);
  font-size: 0.7143rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------------------------------------------------------------- portal ---

   What somebody at a customer sees. Deliberately not the app: no rail, no
   sidebar, one column, and enough air around it that it reads as a page
   somebody was sent rather than a tool they are expected to learn. It borrows
   the app's tokens so a workspace's theme still carries, and nothing else. */

.tf-portal {
  min-height: 100vh;
  background: var(--tf-shell);
  color: var(--tf-ink);
}

.tf-portal-top {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 24px;
  background: var(--tf-canvas);
  border-bottom: 1px solid var(--tf-line);
}
.tf-portal-brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.tf-portal-ws { font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tf-portal-who { display: flex; align-items: center; gap: 10px; margin-left: auto; }
.tf-portal-name { font-size: 0.9286rem; color: var(--tf-ink-2); }
.tf-portal-back { font-size: 0.8571rem; color: var(--tf-ink-3); margin-right: 4px; }
.tf-portal-out {
  border: 0;
  background: none;
  padding: 4px;
  color: var(--tf-ink-3);
  display: inline-flex;
}
.tf-portal-out:hover { color: var(--tf-ink); }

.tf-portal-main { padding: 32px 24px 64px; }
.tf-portal-page { width: min(760px, 100%); margin: 0 auto; }
.tf-portal-head { margin-bottom: 24px; }
.tf-portal-head h1 { font-size: 1.4375rem; margin: 0 0 8px; }

.tf-portal-groups { display: flex; flex-wrap: wrap; gap: 8px; }
.tf-portal-group {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 4px 10px 4px 4px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  background: var(--tf-canvas);
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}

.tf-portal-empty {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
  padding: 40px 28px;
  text-align: center;
}
.tf-portal-empty-lead { font-size: 1.0625rem; font-weight: 500; margin: 0 0 6px; }
.tf-portal-empty .tf-dim { margin: 0; }

/* ------------------------------------------------------- stakeholders --- */

.tf-sh-chips { display: inline-flex; flex-wrap: wrap; gap: 4px; align-items: center; }
.tf-sh-chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 7px 2px 2px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  background: var(--tf-canvas);
  font-size: 0.7857rem;
}
.tf-sh-menu { min-width: 280px; max-height: 340px; overflow-y: auto; }
.tf-sh-warn {
  margin: 2px 6px 8px;
  font-size: 0.7857rem;
  line-height: 1.45;
  color: var(--tf-ink-3);
}
.tf-sh-count { margin-left: auto; font-size: 0.7857rem; color: var(--tf-ink-3); }
.tf-sh-contact { padding-left: 22px; }

/* The portal's ticket list and one ticket. */

.tf-portal-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.tf-portal-item {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
}
.tf-portal-item.is-done .tf-portal-item-title { color: var(--tf-ink-3); }
.tf-portal-item-link {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 18px;
  color: inherit;
  text-decoration: none;
}
.tf-portal-item-link:hover { color: inherit; background: var(--tf-violet-tint); border-radius: var(--tf-radius-lg); }
.tf-portal-item-main { display: grid; gap: 5px; min-width: 0; flex: 1; }
.tf-portal-item-title { font-weight: 500; }
.tf-portal-item-meta {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  font-size: 0.8571rem;
  color: var(--tf-ink-3);
}
.tf-portal-sep { color: var(--tf-line); }
.tf-portal-waiting {
  flex: none;
  padding: 3px 10px;
  border-radius: 999px;
  background: rgba(217, 140, 20, 0.12);
  color: var(--tf-amber);
  font-size: 0.7857rem;
  font-weight: 500;
}

.tf-portal-back-link {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.8571rem;
  color: var(--tf-ink-3);
  text-decoration: none;
  margin-bottom: 14px;
}
.tf-portal-back-link:hover { color: var(--tf-ink); }

.tf-portal-ticket {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
  padding: 26px 28px;
}
.tf-portal-ticket h1 { font-size: 1.3125rem; margin: 0 0 20px; line-height: 1.35; }

.tf-portal-props {
  display: grid;
  grid-template-columns: minmax(120px, max-content) 1fr;
  gap: 8px 20px;
  margin: 0 0 22px;
  font-size: 0.9286rem;
}
.tf-portal-props dt { color: var(--tf-ink-3); font-weight: 400; }
.tf-portal-props dd { margin: 0; display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }

.tf-portal-desc { border-top: 1px solid var(--tf-line-soft); padding-top: 18px; line-height: 1.6; }
.tf-portal-desc img { max-width: 100%; height: auto; }

.tf-portal-qs { margin-top: 28px; }
.tf-portal-qs h2 { font-size: 1.0625rem; margin: 0 0 12px; }
.tf-portal-q {
  background: var(--tf-canvas);
  border: 1px solid var(--tf-line);
  border-left: 3px solid var(--tf-line);
  border-radius: var(--tf-radius-lg);
  padding: 16px 18px;
  margin-bottom: 10px;
}
.tf-portal-q.is-open { border-left-color: var(--tf-amber); }
.tf-portal-q.is-answered { border-left-color: var(--tf-green); }
.tf-portal-q-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  font-size: 0.8571rem;
  margin-bottom: 6px;
}
.tf-portal-q-who { font-weight: 500; }
.tf-portal-q-when { color: var(--tf-ink-3); }
.tf-portal-q-body { line-height: 1.55; }
.tf-portal-q-body p:last-child { margin-bottom: 0; }
.tf-portal-a {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--tf-line-soft);
}
.tf-mention-outside {
  margin-left: auto;
  padding: 1px 6px;
  border-radius: 999px;
  background: rgba(14, 165, 183, 0.14);
  color: #0b7f8c;
  font-size: 0.7143rem;
}
.tf-mention-option { display: flex; align-items: center; gap: 8px; }
.tf-portal-answer { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--tf-line-soft); }
.tf-portal-answer-foot { display: flex; justify-content: flex-end; margin-top: 8px; }
.tf-row-shared { color: #0ea5b7; display: inline-flex; vertical-align: middle; margin-left: 4px; }

/* ---------------------------------------------------- Notification settings --
   The preference table is the screen. Everything else on the page is there to
   keep it from being the first thing somebody meets: a preset row above it, so
   most people never have to read twenty-two lines, and the delivery settings
   below, which are answered once and left alone. */
.tf-preset-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: 10px;
}
.tf-preset {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px 12px 12px 34px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  cursor: pointer;
  transition: border-color var(--tf-ease), box-shadow var(--tf-ease);
}
.tf-preset input { position: absolute; left: 12px; top: 14px; }
.tf-preset.is-on { border-color: var(--tf-violet); box-shadow: 0 0 0 3px var(--tf-violet-tint); }
.tf-preset:focus-within { outline: 2px solid var(--tf-violet); outline-offset: 2px; }
.tf-preset-name { font-weight: 600; font-size: 0.9286rem; }
.tf-preset-hint { color: var(--tf-ink-3); font-size: 0.8571rem; line-height: 1.4; }

.tf-prefs-table { margin-bottom: 0; }
.tf-prefs-table > thead th {
  font-size: 0.7143rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--tf-ink-3);
  font-weight: 600;
  border-bottom: 1px solid var(--tf-line);
}
.tf-prefs-table > tbody > tr > td { border-top: 1px solid var(--tf-line-soft); vertical-align: middle; }
.tf-prefs-group th {
  background: var(--tf-shell);
  font-size: 0.7143rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--tf-ink-3);
  font-weight: 600;
  padding-top: 10px;
  padding-bottom: 6px;
}
.tf-prefs-label { font-weight: 500; }
/* The stored key, shown on purpose: it is what appears in a support question
   and what a person searching these docs will type. */
.tf-prefs-key { font-family: var(--tf-mono); font-size: 0.7143rem; color: var(--tf-ink-3); }

.tf-project-picks {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
  gap: 8px;
}
.tf-project-pick {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 10px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  cursor: pointer;
}
.tf-project-pick:hover { background: var(--tf-hover); }

/* ---------------------------------------------------------------- AI runners

   A run is a log somebody watches arrive, so it is laid out as one: a fixed
   timestamp gutter, and a body that changes shape with what the agent was
   doing. Tool calls and terminal output are monospace because they are
   literal; what the agent said is prose because it is. Colours come from the
   same semantic tokens as everything else, so a run reads correctly in both
   themes without a second palette. */

.tf-badge {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 2px 8px;
  border-radius: 999px;
  font-size: 0.7857rem;
  font-weight: 500;
  white-space: nowrap;
  border: 1px solid transparent;
}
.tf-badge-wait { background: var(--tf-hover); color: var(--tf-ink-2); border-color: var(--tf-line); }
.tf-badge-live { background: var(--tf-violet-tint); color: var(--tf-violet-press); border-color: var(--tf-violet-tint-2); }
.tf-badge-ok   { background: color-mix(in srgb, var(--tf-green) 12%, transparent); color: var(--tf-green); }
.tf-badge-bad  { background: var(--tf-red-tint); color: var(--tf-red); }
.tf-badge-off  { background: var(--tf-hover); color: var(--tf-ink-3); border-color: var(--tf-line); }

/* A runner that is answering, and one that is not. The live one pulses, which
   is the only animation here: it is the single fact on the page that is true
   right now rather than when it loaded. */
.tf-dot-live { background: var(--tf-green); animation: tf-pulse 2s ease-in-out infinite; }
.tf-dot-off  { background: var(--tf-ink-3); }
@keyframes tf-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
@media (prefers-reduced-motion: reduce) { .tf-dot-live { animation: none; } }

.tf-card-foot {
  padding: 13px 16px;
  border-top: 1px solid var(--tf-line);
  background: var(--tf-shell);
  border-radius: 0 0 var(--tf-radius-lg) var(--tf-radius-lg);
}

.tf-filters { display: flex; flex-wrap: wrap; gap: 6px; }
.tf-filter {
  padding: 4px 11px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  background: var(--tf-canvas);
  text-decoration: none;
}
.tf-filter:hover { border-color: var(--tf-violet); color: var(--tf-violet); text-decoration: none; }
.tf-filter-on {
  background: var(--tf-violet-tint);
  border-color: var(--tf-violet-tint-2);
  color: var(--tf-violet-press);
  font-weight: 500;
}

/* The facts about a run that do not change while you watch it. */
.tf-facts { display: flex; flex-wrap: wrap; gap: 8px 26px; margin: 0; }
.tf-facts > div { display: flex; flex-direction: column; gap: 1px; }
.tf-facts dt { font-size: 0.7857rem; color: var(--tf-ink-3); font-weight: 400; }
.tf-facts dd { margin: 0; font-size: 0.8571rem; color: var(--tf-ink); }

.tf-run-summary {
  padding: 9px 12px;
  border-left: 2px solid var(--tf-violet-tint-2);
  background: var(--tf-shell);
  border-radius: 0 var(--tf-radius) var(--tf-radius) 0;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}

/* The log itself. Capped and scrolled rather than left to grow the page: a
   run of any length has to stay usable, and the interesting end is the bottom. */
.tf-run { max-height: 70vh; overflow-y: auto; }
.tf-run-event { display: flex; gap: 12px; padding: 7px 16px; border-bottom: 1px solid var(--tf-line-soft); }
.tf-run-event:last-child { border-bottom: 0; }
.tf-run-gutter { flex: 0 0 4.5rem; padding-top: 1px; }
.tf-run-gutter .tf-mono { font-size: 0.75rem; }
.tf-run-body { flex: 1 1 auto; min-width: 0; }

.tf-run-tool { display: flex; align-items: center; gap: 6px; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-run-pre {
  margin: 3px 0 0;
  padding: 7px 10px;
  background: var(--tf-shell);
  border: 1px solid var(--tf-line-soft);
  border-radius: var(--tf-radius);
  font-family: var(--tf-mono);
  font-size: 0.7857rem;
  line-height: 1.5;
  color: var(--tf-ink-2);
  white-space: pre-wrap;
  word-break: break-word;
  max-height: 22rem;
  overflow-y: auto;
}
.tf-run-console { color: var(--tf-ink); }
.tf-run-error { border-color: var(--tf-red); color: var(--tf-red); }
.tf-run-status { font-size: 0.8571rem; color: var(--tf-ink-3); display: flex; align-items: center; gap: 6px; }
.tf-run-said { font-size: 0.9286rem; color: var(--tf-ink); }
.tf-run-said p:last-child { margin-bottom: 0; }
.tf-run-user { color: var(--tf-ink-2); }
.tf-run-user::before { content: "› "; color: var(--tf-ink-3); }


/* ---------------------------------------------------------------------------
   Test plans

   A checklist somebody works down against a ticket. The four answers are the
   loudest thing in the pane on purpose: whoever is reading this may never have
   tested anything before, and the only thing they have to find is the button
   that says what happened.

   Verdict colour is the app's own semantic set — the same green, red and amber
   a priority and a status already use — so a chip here needs no learning.
   --------------------------------------------------------------------------- */
.tf-tests { display: flex; flex-direction: column; gap: 14px; }

.tf-tests-empty { display: flex; flex-direction: column; gap: 10px; }
.tf-tests-start { display: flex; gap: 8px; align-items: center; }
.tf-tests-start .form-control { max-width: 260px; }

.tf-tests-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.tf-tests-title { font-weight: 600; }
.tf-tests-count { font-size: 0.8571rem; }
.tf-tests-head-acts { margin-left: auto; }
.tf-round-new { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.tf-round-new .form-control { width: 220px; }

/* The rounds, oldest first. Nothing until there are two of them: one round is
   not a switcher, it is just this plan. */
.tf-rounds { display: flex; flex-wrap: wrap; gap: 6px; }
.tf-round {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 3px 10px;
  border: 1px solid var(--tf-line);
  border-radius: 999px;
  background: var(--tf-canvas);
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
  text-decoration: none;
}
.tf-round:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-round.is-on {
  border-color: var(--tf-violet);
  background: var(--tf-violet-tint);
  color: var(--tf-violet-press);
  font-weight: 500;
}
.tf-round-done { display: inline-flex; color: var(--tf-green); }

.tf-steps { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }

.tf-step {
  display: grid;
  grid-template-columns: 26px 1fr auto;
  gap: 10px;
  padding: 12px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius);
  background: var(--tf-canvas);
}
/* A step that did not work is marked down its edge rather than tinted all
   over: the row still has to be read, and a wash of red over a paragraph of
   instructions makes it harder, not easier. */
.tf-step.is-fail { box-shadow: inset 3px 0 0 var(--tf-red); }
.tf-step.is-blocked { box-shadow: inset 3px 0 0 var(--tf-amber); }
.tf-step.is-pass { box-shadow: inset 3px 0 0 var(--tf-green); }
.tf-step.is-skipped { box-shadow: inset 3px 0 0 #ccd0d8; }

.tf-step-no {
  font-variant-numeric: tabular-nums;
  color: var(--tf-ink-3);
  font-size: 0.8571rem;
  padding-top: 1px;
}
.tf-step-body { display: flex; flex-direction: column; gap: 6px; min-width: 0; }

/* Answered, a step is one line: what it was, what happened, who said so. The
   instructions fold away behind the same press that changes the answer —
   somebody re-reading them is somebody thinking of changing it. */
.tf-step-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-width: 0; }
.tf-step-title { font-weight: 500; }
.tf-step-who { font-size: 0.7857rem; white-space: nowrap; }

/* Every secondary control in the pane is this one shape: a word, no chrome
   until it is pointed at. Four loud buttons and a dozen quiet ones would be a
   busier page than four loud buttons and nothing else. */
.tf-step-quiet {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 2px 7px;
  border: 1px solid transparent;
  border-radius: var(--tf-radius-sm);
  background: none;
  color: var(--tf-ink-3);
  font-size: 0.7857rem;
  transition: background var(--tf-ease), color var(--tf-ease), border-color var(--tf-ease);
}
.tf-step-quiet:hover { background: var(--tf-hover); border-color: var(--tf-line); color: var(--tf-ink); }

.tf-step-detail { display: flex; flex-direction: column; gap: 6px; }
.tf-step-line { margin: 0; font-size: 0.8571rem; color: var(--tf-ink-2); }
.tf-step-key {
  display: inline-block;
  min-width: 96px;
  color: var(--tf-ink-3);
  font-size: 0.7857rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.tf-step-after { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; }
.tf-step-note {
  margin: 0;
  padding: 6px 10px;
  border-radius: var(--tf-radius-sm);
  background: var(--tf-shell);
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
  align-self: stretch;
}
/* What it was last time, sitting at the end of the title line rather than
   under it: it is a hint, not a finding. */
.tf-step-last { font-size: 0.7857rem; color: var(--tf-ink-3); white-space: nowrap; }
.tf-step-bug {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  font-size: 0.8571rem;
  color: var(--tf-ink-2);
}
.tf-step-bug form { display: inline; }

.tf-note-form { display: flex; flex-direction: column; gap: 6px; align-self: stretch; }
.tf-note-form-foot { display: flex; justify-content: flex-end; gap: 6px; }

/* The one loud thing in the pane. Whoever is reading this may never have
   tested anything before, and the only thing they have to find is the button
   that says what happened. */
.tf-verdict-form { display: inline; }
.tf-verdict-btns { display: flex; flex-wrap: wrap; gap: 6px; }
.tf-verdict-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 5px 11px;
  border: 1px solid var(--tf-line);
  border-radius: var(--tf-radius-sm);
  background: var(--tf-canvas);
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
  transition: background var(--tf-ease), border-color var(--tf-ease), color var(--tf-ease);
}
.tf-verdict-btn:hover { background: var(--tf-hover); color: var(--tf-ink); }
.tf-verdict-btn.tf-verdict-pass:hover, .tf-verdict-btn.tf-verdict-pass.is-on {
  border-color: var(--tf-green); color: #17794a; background: #eef8f2;
}
.tf-verdict-btn.tf-verdict-fail:hover, .tf-verdict-btn.tf-verdict-fail.is-on {
  border-color: var(--tf-red); color: #b3383c; background: var(--tf-red-tint);
}
.tf-verdict-btn.tf-verdict-blocked:hover, .tf-verdict-btn.tf-verdict-blocked.is-on {
  border-color: var(--tf-amber); color: #8a5c07; background: #fdf5e7;
}
.tf-verdict-btn.tf-verdict-skipped:hover, .tf-verdict-btn.tf-verdict-skipped.is-on {
  border-color: #ccd0d8; color: var(--tf-ink-2); background: var(--tf-hover);
}
.tf-verdict-btn.is-on { font-weight: 500; }

.tf-verdict {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 1px 8px;
  border-radius: 999px;
  font-size: 0.7857rem;
  font-weight: 500;
}
.tf-verdict-pass { background: #eef8f2; color: #17794a; }
.tf-verdict-fail { background: var(--tf-red-tint); color: #b3383c; }
.tf-verdict-blocked { background: #fdf5e7; color: #8a5c07; }
.tf-verdict-skipped { background: var(--tf-hover); color: var(--tf-ink-2); }

/* The same words on a line of history, where they are read in a column of
   sentences rather than as a chip. */
.tf-tl-val.tf-verdict-pass, .tf-tl-val.tf-verdict-fail,
.tf-tl-val.tf-verdict-blocked, .tf-tl-val.tf-verdict-skipped { padding: 0 5px; border-radius: 4px; }

.tf-step-edit { display: flex; align-items: flex-start; gap: 2px; }

.tf-step-add { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.tf-step-add .form-control { flex: 1 1 180px; min-width: 140px; }

/* =========================================================================
   Small screens and touch.

   Last in the file on purpose. Almost nothing below raises its specificity
   to win — a phone's `.tf-chip` is the same selector as a desk's — so what
   settles it is source order, and a rule written earlier would quietly lose
   to the one it was meant to replace.
   ========================================================================= */

/* The areas the rail leads to, drawn into the menu that opens in the rail's
   place. Two to a row, at a size a thumb can land on without aiming. */
.tf-side-places {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  padding: 6px 8px 10px;
  margin-bottom: 2px;
  border-bottom: 1px solid var(--tf-nav-line);
}
.tf-place {
  display: flex;
  align-items: center;
  gap: 9px;
  min-height: 44px;
  padding: 8px 10px;
  border-radius: var(--tf-radius);
  background: var(--tf-nav-field);
  color: var(--tf-nav-fg);
  font-size: 0.9286rem;
  font-weight: 500;
}
.tf-place:hover, .tf-place:active { background: var(--tf-nav-hover); color: var(--tf-nav-fg-strong); text-decoration: none; }
.tf-place .tf-icon { color: var(--tf-nav-fg-dim); }
.tf-place-mark { display: inline-grid; place-items: center; flex: 0 0 auto; line-height: 0; }
.tf-place-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.tf-place.is-active { background: var(--tf-nav-active); color: var(--tf-nav-fg-strong); font-weight: 600; }
.tf-place.is-active .tf-icon { color: var(--tf-violet-light); }

/* Everything that narrows a board, as one group. Not a box at all on a wide
   screen — the controls stay direct children of the toolbar's flex row, laid
   out exactly as they were before the group existed. */
.tf-toolbar-tools { display: contents; }

@media (max-width: 991.98px) {
  /* Nothing here is a phone-only stylesheet: the whole system is measured in
     rem off this one number, so raising it takes body copy from 13px to 15px
     and every label, chip and gap with it. What follows is the handful of
     things measured in pixels, which had to be raised by hand. */
  html { font-size: 16px; }

  :root {
    --tf-topbar-h: 54px;
    --tf-row-h: 46px;
  }

  .tf-rail { display: none; }
  .tf-side.offcanvas-lg { border-right: 1px solid var(--tf-line); }
  .tf-side { --bs-offcanvas-width: min(21rem, 88vw); }
  .tf-app-frame { height: 100dvh; }
  .tf-side-close { display: inline-grid; }
  .tf-stage { border-left: 0; }
  .tf-page-pad { padding: 16px 14px calc(48px + env(safe-area-inset-bottom)); }
  .tf-board-head, .tf-toolbar { padding-left: 12px; padding-right: 12px; }
  .tf-meta-grid { grid-template-columns: 1fr; gap: 4px; }
  .tf-meta-key { margin-top: 8px; }
  .tf-meta-fields { grid-template-columns: 1fr; }

  /* Anything a finger has to find. 28px was a mouse's target. */
  .tf-icon-btn { width: 40px; height: 40px; }
  .tf-chip { height: 38px; padding: 0 11px; }
  .tf-toolbar .form-select { height: 38px; }
  .tf-toolbar .tf-search-wrap input { height: 38px; }
  .tf-tab { padding: 11px 11px 12px; }
  .tf-side-link, .tf-tree-summary { padding: 10px 9px; }
  .tf-filter-pill { min-height: 38px; }
  .btn { --bs-btn-padding-y: 0.5rem; --bs-btn-padding-x: 0.85rem; }
  .btn-sm { --bs-btn-padding-y: 0.4rem; --bs-btn-padding-x: 0.7rem; }

  /* Under 16px a mobile browser zooms the page to meet the field, and never
     zooms back out. Every input on a phone is at least that. */
  .form-control, .form-select, .tf-panel-title { font-size: 1rem; }
  .form-control-sm, .form-select-sm { font-size: 1rem; min-height: 40px; }

  /* A trail of five crumbs has nowhere to go in a 54px bar. The last of them
     is the one that says where you are; the menu is how you get anywhere
     else, and a task keeps its own way back beside the title. */
  .tf-crumbs > *:not(:last-child) { display: none; }

  /* One strip that scrolls, rather than four rows of chrome above the first
     task. The search comes first in it, because it is what anybody reaching
     for the toolbar on a phone is reaching for. */
  .tf-toolbar { flex-wrap: wrap; gap: 8px 6px; }
  .tf-toolbar-tools {
    display: flex;
    flex: 1 1 100%;
    flex-wrap: nowrap;
    align-items: center;
    gap: 6px;
    overflow-x: auto;
    scrollbar-width: none;
    /* The strip bleeds into the toolbar's padding so a chip scrolled to the
       edge sits against the edge, rather than in a gutter. */
    margin-inline: -12px;
    padding-inline: 12px;
    scroll-padding-inline: 12px;
  }
  .tf-toolbar-tools::-webkit-scrollbar { display: none; }
  .tf-toolbar-tools > * { flex: 0 0 auto; }
  .tf-toolbar-tools > form { order: -1; flex-wrap: nowrap; }
  .tf-toolbar .tf-search-wrap input { width: 11rem; }
  .tf-toolbar-right { flex: 1 1 100%; margin-left: 0; justify-content: flex-end; }

  /* The panel's strip has as many sections as the page's and half the width to
     draw them in. */
  .tf-tabs { overflow-x: auto; scrollbar-width: none; }
  .tf-tabs::-webkit-scrollbar { display: none; }

  /* A title and its row of verbs, one above the other. Side by side they left
     the verbs a two-button-wide column against the edge of the screen. */
  .tf-page-head { flex-direction: column; align-items: flex-start; gap: 12px; }
  .tf-page-head > .d-flex { flex-wrap: wrap; }

  /* A menu is never wider than what it opens over. */
  .dropdown-menu { max-width: calc(100vw - 16px); }
  .tf-note-menu { width: min(22rem, calc(100vw - 16px)); }
  .tf-filter-menu { width: min(20rem, calc(100vw - 16px)); max-height: 70dvh; }

  /* A bare table in a card took the whole page sideways with it. The card is
     the thing that scrolls; the page stays put. */
  .tf-card:has(> .tf-table) { overflow-x: auto; }

  /* The panel is the whole screen here, so the properties read one to a line
     rather than two, and the whole of it is reachable under a browser bar
     that comes and goes. */
  .tf-drawer { width: 100%; height: 100dvh; }
  .tf-panel-scroll { padding: 14px 16px calc(28px + env(safe-area-inset-bottom)); }
  .tf-panel-tabs > .tf-tabs { margin-inline: -16px; padding-inline: 16px; }

  /* The board is a table however narrow the screen, and it scrolls sideways
     inside its own wrapper. What it must not do is spend the whole width on
     the first column: 22rem left no sign there was anything to the right of
     it, and no reason to try. */
  .tf-grid th[data-col="title"] { min-width: 15rem; }
  .tf-side-search input { height: 40px; }

  /* A page whose whole height is the page. `vh` on a phone is the height the
     window would be with the browser's bars away, which is not the height it
     has. */
  .tf-auth, .tf-www, .tf-portal { min-height: 100dvh; }
  .tf-portal-top { padding: 12px 14px; }
  .tf-portal-main { padding: 20px 14px calc(48px + env(safe-area-inset-bottom)); }

  /* Above the gesture bar, not under it. */
  .tf-bulk-dock { bottom: calc(16px + env(safe-area-inset-bottom)); }
  .tf-side-foot { padding-bottom: calc(8px + env(safe-area-inset-bottom)); }
  /* A sheet up from the bottom edge rather than a card in the middle: it is
     the whole width anyway at this size, and the bottom is where the thumb
     is. */
  .tf-dialog {
    width: 100vw;
    max-width: 100vw;
    max-height: 92dvh;
    margin: auto auto 0;
    border-width: 1px 0 0;
    border-radius: var(--tf-radius-lg) var(--tf-radius-lg) 0 0;
  }
  .tf-dialog-foot { padding-bottom: calc(13px + env(safe-area-inset-bottom)); }
}

/* Touch. Not the same question as "is the screen narrow" — a tablet is both,
   a laptop with a touchscreen is only the second — so it is asked separately.
   Everything below was drawn to appear under the pointer, which on a finger
   means it never appeared at all. */
@media (hover: none) {
  .tf-row .form-check-input,
  .tf-row .tf-ref-copy,
  .tf-row-open,
  .tf-tag-hint,
  .tf-batch-hint,
  .tf-batch-x,
  .tf-group-pick,
  .tf-group-add,
  .tf-side-add,
  .tf-gh-unlink,
  .tf-batch-table td.tf-bt-x .tf-icon-btn { opacity: 1; }

  /* The grip is for a drag that a finger cannot start — the board's rows are
     reordered with a mouse — so it stays out of the way rather than standing
     as a control that does nothing. */
  .tf-drag { display: none; }

  /* A row's checkbox is now always drawn, so it needs room to be pressed
     without catching the title beside it. */
  .tf-col-check { width: 34px; padding-left: 8px !important; }
  .tf-row .form-check-input { width: 1.15em; height: 1.15em; }

  /* Column widths are dragged with a pointer. There is nothing here for a
     finger but a 7px strip of column header that swallows the press. */
  .tf-col-resize { display: none; }
}


/* ------------------------------------------------------------------ reports --
   Everything here is built from the tokens above rather than from hex
   literals, because the whole page is drawn again in the dark theme and a
   colour written down once only works in one of them. */

.tf-report-note {
  margin: 0 0 1rem;
  padding: 0.55rem 0.8rem;
  border: 1px solid var(--tf-line-soft);
  border-left: 3px solid var(--tf-amber);
  border-radius: var(--tf-radius-sm);
  background: var(--tf-shell);
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
}
/* A workspace whose history is entirely measured still says so, quietly: the
   sentence is worth having and does not need a warning colour. */
.tf-report-note.is-quiet {
  border-left-color: var(--tf-green);
  color: var(--tf-ink-3);
}

.tf-stat-delta {
  font-size: 0.7857rem;
  font-variant-numeric: tabular-nums;
}
.tf-stat-delta.is-good { color: var(--tf-green); }
.tf-stat-delta.is-bad { color: var(--tf-red); }
.tf-stat-delta.is-flat { color: var(--tf-ink-3); }

/* A list of proportion bars, reusing the meter the storage page draws. */
.tf-bars { display: flex; flex-direction: column; gap: 0.5rem; }
.tf-bar-row {
  display: grid;
  grid-template-columns: minmax(7rem, 12rem) 1fr auto;
  align-items: center;
  gap: 0.75rem;
}
.tf-bar-label { color: var(--tf-ink-2); font-size: 0.8571rem; }
.tf-bar-value {
  color: var(--tf-ink-3);
  font-size: 0.8571rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
@media (max-width: 40rem) {
  .tf-bar-row { grid-template-columns: 1fr auto; }
  .tf-bar-row .tf-meter { grid-column: 1 / -1; order: 3; }
}

.tf-chart {
  width: 100%;
  height: 11rem;
  overflow: visible;
}
.tf-chart-grid { stroke: var(--tf-line-soft); stroke-width: 0.4; vector-effect: non-scaling-stroke; }
.tf-chart-line {
  fill: none;
  stroke-width: 2;
  /* Without this the viewBox stretch would thin the line horizontally and
     fatten it vertically, which reads as a wobble rather than a line. */
  vector-effect: non-scaling-stroke;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.tf-chart-line.is-created { stroke: var(--tf-blue); }
.tf-chart-line.is-finished { stroke: var(--tf-green); }
.tf-chart-key {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  margin-top: 0.75rem;
  color: var(--tf-ink-2);
  font-size: 0.8571rem;
}
.tf-chart-key .tf-dim { margin-left: auto; }
.tf-chart-swatch {
  width: 0.75rem;
  height: 0.2rem;
  border-radius: 999px;
  display: inline-block;
}
.tf-chart-swatch.is-created { background: var(--tf-blue); }
.tf-chart-swatch.is-finished { background: var(--tf-green); }
.tf-chart-swatch:not(:first-child) { margin-left: 0.6rem; }

.tf-filter-chips { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-bottom: 1rem; align-items: center; }

/* A list that is a list of things rather than a list of bullets. */
.tf-plain-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.4rem; }
.tf-plain-list li { display: flex; gap: 0.5rem; align-items: baseline; }
