@charset "UTF-8";
/*
 * Support Inbox — design system foundation.
 * Compiled by `yarn build:css` (Dart Sass) into app/assets/builds/application.css,
 * then fingerprinted and served by Propshaft. Tokens first, then layout regions,
 * then components. Light theme only for this slice.
 */
:root {
  --color-surface: #ffffff;
  --color-surface-sunken: #f6f7f9;
  --color-surface-raised: #ffffff;
  --color-border: #e3e6ea;
  --color-border-strong: #cdd2d9;
  --color-text: #1b1f24;
  --color-muted: #6b7280;
  --color-accent: #2f6feb;
  --color-accent-soft: #eaf1fe;
  --color-danger: #b42318;
  --color-danger-soft: #fef3f2;
  --color-success: #067647;
  --color-success-soft: #ecfdf3;
  --color-warning: #b54708;
  --color-warning-soft: #fffaeb;
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  --text-sm: 13px;
  --text-base: 14px;
  --text-lg: 16px;
  --text-xl: 20px;
  --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
  --shadow-md: 0 4px 16px rgba(16, 24, 40, 0.10);
}

* {
  box-sizing: border-box;
}

html, body {
  height: 100%;
}

body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-base);
  color: var(--color-text);
  background: var(--color-surface-sunken);
  -webkit-font-smoothing: antialiased;
}

a {
  color: inherit;
  text-decoration: none;
}

:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* ---------- Flash ---------- */
/* Top-right toast so it clears the centered sticky pane headers; auto-dismissed
   by the flash Stimulus controller (Turbo-Frame nav never reloads <body>). */
.flash {
  position: fixed;
  top: var(--space-4);
  right: var(--space-4);
  z-index: 50;
  /* The toast is fixed to the same top-right corner as the thread header's action row, so for
     its 4s lifetime it sat on top of the spam toggle and swallowed the click. It carries no
     controls of its own — nothing here needs to receive a pointer — so let clicks through
     rather than moving an established piece of chrome. */
  pointer-events: none;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  box-shadow: var(--shadow-md);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.flash--dismissing {
  opacity: 0;
  transform: translateY(-8px);
}

.flash--alert {
  background: var(--color-danger-soft);
  color: var(--color-danger);
  border: 1px solid #f6c9c4;
}

.flash--notice {
  background: var(--color-success-soft);
  color: var(--color-success);
  border: 1px solid #b9e0c9;
}

/* ---------- Buttons & fields ---------- */
.btn {
  appearance: none;
  border: 1px solid transparent;
  border-radius: var(--radius-md);
  padding: var(--space-2) var(--space-4);
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  background: var(--color-surface-sunken);
  color: var(--color-text);
}

.btn--primary {
  background: var(--color-accent);
  color: #fff;
  width: 100%;
}

.btn--primary:hover {
  background: #2a61cf;
}

.btn--ghost {
  background: transparent;
  border-color: var(--color-border-strong);
  color: var(--color-muted);
  padding: var(--space-1) var(--space-3);
  font-size: var(--text-sm);
}

.btn--ghost:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.field {
  display: block;
  margin-bottom: var(--space-4);
}

.field__label {
  display: block;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
  margin-bottom: var(--space-1);
}

.field__input {
  width: 100%;
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  font: inherit;
  background: var(--color-surface);
  color: var(--color-text);
}

.field__input:focus {
  border-color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* The app's one native control. Everywhere else a "pick one" is the .dropdown popover, but that
   is position:absolute inside .modal{overflow:hidden} and a scrolling .settings__detail, so a
   list long enough to matter gets clipped. A native select's list is painted by the browser
   outside the document, which no ancestor can clip — and it brings keyboard nav and type-ahead
   with it. The cost is the UA chrome, stripped here so it reads as a .field__input with a caret. */
.field__select {
  appearance: none;
  padding: var(--space-2) var(--space-6) var(--space-2) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  font: inherit;
  background-color: var(--color-surface);
  color: var(--color-text);
  cursor: pointer;
  /* The caret the appearance:none reset removed, redrawn in the muted token rather than left to
     the platform's own arrow. */
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-2) center;
  background-size: 14px;
}

.field__select:hover {
  border-color: var(--color-accent);
}

.field__select:focus {
  border-color: var(--color-accent);
  outline: none;
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

/* ---------- Login ---------- */
.login {
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-5);
}

.login__card {
  width: 100%;
  max-width: 360px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}

.login__title {
  margin: 0;
  font-size: var(--text-xl);
}

.login__subtitle {
  margin: var(--space-1) 0 var(--space-5);
  color: var(--color-muted);
}

.login__hint {
  margin: var(--space-4) 0 0;
  font-size: 12px;
  color: var(--color-muted);
  text-align: center;
}

/* ---------- App shell (three regions) ---------- */
/* The rail's width is a custom property so collapsing overrides one value rather than restating
   the whole track list — which is also why the narrow-viewport rule below, setting
   grid-template-columns outright, wins over the collapsed state without needing to know it. */
.app {
  --sidebar-width: 240px;
  display: grid;
  grid-template-columns: var(--sidebar-width) 340px 1fr;
  height: 100vh;
  background: var(--color-surface);
}

@media (prefers-reduced-motion: no-preference) {
  .app {
    transition: grid-template-columns 0.15s ease;
  }
}
/* ---------- Icons ---------- */
/* Phosphor glyphs are text, so the size has to drive both the box and the font — one custom
   property keeps them in step, and every override below only has to restate `--icon-size`.
   The explicit box is what stops a slow font load from shifting the layout: the space is
   reserved whether or not the glyph has arrived.
   Keep the display non-inline — `transform` (used by .sidebar__chevron) does not apply to
   inline elements. */
.icon {
  --icon-size: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--icon-size);
  height: var(--icon-size);
  font-size: var(--icon-size);
  line-height: 1;
  flex: 0 0 auto;
  vertical-align: middle;
}

/* ---------- Sidebar ---------- */
.sidebar {
  display: flex;
  flex-direction: column;
  height: 100%;
  border-right: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
  overflow: hidden; /* the nav region scrolls, not the whole rail */
}

/* Header: avatar + name + settings gear */
.sidebar__header {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-4);
  border-bottom: 1px solid var(--color-border);
}

.sidebar__avatar {
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  font-size: 12px;
  font-weight: 700;
}

.sidebar__name {
  flex: 1;
  font-weight: 700;
  font-size: var(--text-lg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__gear, .sidebar__collapse-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  padding: var(--space-1);
  border-radius: var(--radius-md);
  color: var(--color-muted);
}

.sidebar__gear:hover, .sidebar__collapse-toggle:hover {
  color: var(--color-text);
  background: var(--color-surface);
}

.sidebar__gear[aria-current=true] {
  color: var(--color-accent);
  background: var(--color-accent-soft);
}

/* The gear is an <a>, so its rules never had to undo anything. Sharing them with a <button>
   does: without this the toggle keeps the browser's buttonface background and 3D border. */
.sidebar__collapse-toggle {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  cursor: pointer;
}

/* One glyph for both directions, turned around when the rail is shut — the same rotate-on-state
   the project chevron uses. */
.sidebar__collapse-icon {
  transition: transform 0.15s ease;
}

/* Scrolling nav region */
.sidebar__nav {
  flex: 1;
  overflow-y: auto;
  padding: var(--space-3) var(--space-3) var(--space-4);
}

/* Nav rows: Inbox, Spam, and project children */
.sidebar__nav-item {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sidebar__nav-item:hover {
  background: var(--color-accent-soft);
}

.sidebar__nav-item[aria-current=true] {
  background: var(--color-accent-soft);
  color: var(--color-accent);
  font-weight: 600;
}

.sidebar__nav-icon {
  color: var(--color-muted);
}

.sidebar__nav-item[aria-current=true] .sidebar__nav-icon {
  color: var(--color-accent);
}

.sidebar__section-label {
  font-size: 12px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--color-muted);
  margin: var(--space-4) var(--space-3) var(--space-2);
}

.sidebar__projects {
  list-style: none;
  margin: 0;
  padding: 0;
}

.sidebar__project {
  margin: 0;
}

/* Project disclosure toggle — a <button> reset to read like a nav row */
.sidebar__project-toggle {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.sidebar__project-toggle:hover {
  background: var(--color-accent-soft);
}

/* Wraps the initials and the name so a rename can stream both back as one node (and carry the
   tooltip with them). flex:1 keeps it filling the row the way those two spans did on their own —
   which is also what keeps the title's hover target the whole label, not just the text. */
.sidebar__project-label {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

.sidebar__project-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Stands in for the project's name in the collapsed rail, where there is no room for text.
   Always in the DOM — the row is also injected by create.turbo_stream.erb, which knows nothing
   about the rail's state — so it is the CSS that decides which of the two shows.
   Borrows the avatar's treatment so the two read as one family. */
.sidebar__project-initials {
  display: none;
  flex: 0 0 auto;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  place-items: center;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  font-size: 11px;
  font-weight: 700;
}

/* Rotates on open — which needs .icon to keep a non-inline display. */
.sidebar__chevron {
  color: var(--color-muted);
  transition: transform 0.15s ease;
}

.sidebar__project.is-open .sidebar__chevron {
  transform: rotate(180deg);
}

.sidebar__project-children {
  list-style: none;
  margin: var(--space-1) 0 var(--space-2);
  padding-left: var(--space-4);
}

/* An <li> in the projects list now, so it takes the same vertical rhythm as the rows it sits
   between instead of a paragraph's default margins. */
.sidebar__empty {
  color: var(--color-muted);
  font-size: var(--text-sm);
  padding: var(--space-2) var(--space-3);
}

/* "+ Project" — the last row of the projects list above, which is why it lives here rather than
   with the footer it used to sit in. */
.sidebar__new-project {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  margin: 0;
}

/* Reads as a list row, not a button chip: it sits among the project toggles now, so it borrows
   their geometry wholesale. It carries no .btn, which means the native <button> chrome has to be
   reset here — background, border and font all come from the UA otherwise. Accent-coloured
   because it is the one row in the list that acts rather than navigates; the "Project" label
   carries that meaning too, so the colour is never the only signal. */
.sidebar__new-project-toggle {
  appearance: none;
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border: 0;
  border-radius: var(--radius-md);
  background: transparent;
  color: var(--color-accent);
  font: inherit;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}

.sidebar__new-project-toggle:hover {
  background: var(--color-accent-soft);
}

.sidebar__new-project-toggle .sidebar__nav-icon {
  color: var(--color-accent);
}

/* Only the form is a flex column; the fields wrapper stays display-neutral so the
   `hidden` attribute (display:none) still collapses it when the disclosure is closed. */
.sidebar__new-project-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.sidebar__new-project-fields[hidden] {
  display: none;
}

.sidebar__form-error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

/* Footer: account only */
.sidebar__footer {
  border-top: 1px solid var(--color-border);
  padding: var(--space-3);
}

.sidebar__account {
  display: flex;
  align-items: center;
}

/* .btn is a bare <button>, so it stays inline-block and `gap` alone would be inert — the glyph
   would sit flush against its label. The display change is what makes the gap real, which is why
   .sidebar__new-project-toggle sets one too for the same icon+label pairing. */
.sidebar__account .btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

/* ---------- Sidebar, collapsed to an icon rail ----------
   Everything here is keyed off ONE class on .app, which the server renders from the cookie and
   the sidebar-collapse controller toggles. The markup is identical in both states; only these
   rules differ. That is what lets the toggle flip a class instead of re-rendering, and what lets
   a turbo-stream prepend a project row without knowing which state the rail is in.

   Scoped above the stacking breakpoint on purpose: below it the rail is a full-width block on top
   of the page, and centred lone icons across 100% of the viewport would be nonsense. */
@media (min-width: 901px) {
  .app.is-sidebar-collapsed {
    --sidebar-width: 56px;
  }
  /* The way back out is the first thing in the rail, so a collapsed sidebar can never trap. */
  .app.is-sidebar-collapsed .sidebar__header {
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3) var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__collapse-toggle {
    order: -1;
  }
  .app.is-sidebar-collapsed .sidebar__collapse-icon {
    transform: rotate(180deg);
  }
  .app.is-sidebar-collapsed .sidebar__nav {
    padding: var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__nav-item,
  .app.is-sidebar-collapsed .sidebar__project-toggle,
  .app.is-sidebar-collapsed .sidebar__new-project-toggle {
    justify-content: center;
    padding: var(--space-2);
  }
  /* These spans ARE the accessible name of the control they sit in, so they are hidden from the
     eye but left in the accessibility tree — display:none would leave a screen reader announcing
     an unlabelled icon. */
  .app.is-sidebar-collapsed .sidebar__nav-label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
  /* Text nothing takes its name from, and controls that cannot work at this width. The project
     name goes with them because its button falls back to the title attribute. */
  .app.is-sidebar-collapsed :is(.sidebar__name,
  .sidebar__section-label,
  .sidebar__project-name,
  .sidebar__chevron,
  .sidebar__empty,
  .sidebar__new-project-fields) {
    display: none;
  }
  .app.is-sidebar-collapsed .sidebar__project-initials {
    display: grid;
  }
  /* The label is down to just the badge here, so it must stop claiming the row's width or the
     toggle's justify-content:center would have nothing left to centre. */
  .app.is-sidebar-collapsed .sidebar__project-label {
    flex: 0 0 auto;
  }
  /* Carries the grouping the hidden PROJECTS caption used to. */
  .app.is-sidebar-collapsed .sidebar__projects {
    border-top: 1px solid var(--color-border);
    margin-top: var(--space-2);
    padding-top: var(--space-2);
  }
  .app.is-sidebar-collapsed .sidebar__project-children {
    padding-left: 0;
  }
  .app.is-sidebar-collapsed .sidebar__footer {
    padding: var(--space-2);
  }
  /* Sign out is the footer's only row now, so it centres like every other glyph in the rail.
     It used to get that for free from `flex-direction: column` + the base `align-items: center`,
     which only made sense while the email sat above it. */
  .app.is-sidebar-collapsed .sidebar__account {
    justify-content: center;
  }
}
/* ---------- Conversation list ---------- */
.app__list {
  border-right: 1px solid var(--color-border);
  overflow-y: auto;
  background: var(--color-surface);
}

.list__header {
  position: sticky;
  top: 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-4);
  z-index: 1;
}

.list__title {
  margin: var(--space-3) 0 0;
  font-size: var(--text-lg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ---------- List toolbar (search / filter / sort) ---------- */
.list__toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.list__search {
  position: relative;
  flex: 1;
  min-width: 0;
}

.list__search-icon {
  position: absolute;
  left: var(--space-2);
  top: 50%;
  transform: translateY(-50%);
  color: var(--color-muted);
  pointer-events: none;
}

.list__search-input {
  padding-left: calc(var(--space-5) + var(--space-1));
}

.list__tool-button {
  position: relative;
  appearance: none;
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--color-border-strong);
  background: var(--color-surface);
  color: var(--color-muted);
  border-radius: var(--radius-md);
  padding: var(--space-2);
  cursor: pointer;
}

.list__tool-button:hover {
  color: var(--color-text);
}

.list__tool-button[aria-expanded=true] {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

/* Active-filter dot on the funnel — hidden unless a non-default filter is set. */
.list__tool-button__dot {
  display: none;
  position: absolute;
  top: 3px;
  right: 3px;
  width: 7px;
  height: 7px;
  border-radius: 999px;
  background: var(--color-accent);
  border: 1px solid var(--color-surface);
}

.list__tool-button__dot.is-active {
  display: block;
}

/* ---------- Dropdown menu (filter / sort) ---------- */
.dropdown {
  position: relative;
}

.dropdown__menu {
  position: absolute;
  right: 0;
  top: calc(100% + var(--space-1));
  z-index: 20;
  min-width: 168px;
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-1);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* Re-assert so the `hidden` attribute wins over `display:flex` (the slice-1 trap). */
.dropdown__menu[hidden] {
  display: none;
}

.dropdown__option {
  appearance: none;
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  font: inherit;
  color: var(--color-text);
  cursor: pointer;
  white-space: nowrap;
}

.dropdown__option:hover {
  background: var(--color-accent-soft);
}

.dropdown__option[aria-checked=true] {
  color: var(--color-accent);
  font-weight: 600;
  background: var(--color-accent-soft);
}

/* ---------- Timestamp popover (UTC / local) ---------- */
/* The same panel recipe as .dropdown__menu — surface, hairline, --radius-md, --shadow-md — but
   fixed rather than absolute, and parented to the body. Every timestamp it serves sits inside a
   container that sets overflow (.app__list, .thread__scroll, .thread-info), so an absolutely
   positioned panel would be sheared at the very edge it opens against. The controller writes
   top/left from the trigger's viewport rect. */
.time-popover {
  position: fixed;
  /* Over .dropdown__menu (20) and the .flash toast (50), under .modal-overlay (100) — no
     timestamp is drawn inside the settings modal, so this must never cover one. */
  z-index: 60;
  /* Purely informational, and two of the three triggers sit inside a row's navigation link. If
     the panel could take a pointer it would swallow the click that opens the thread — the same
     reason .flash gives up its own pointer events. */
  pointer-events: none;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-1) var(--space-3);
  /* A long IANA zone (America/Argentina/ComodRivadavia) wraps inside the panel instead of
     widening it past a narrow viewport. */
  max-width: min(260px, 100vw - 16px);
  padding: var(--space-2) var(--space-3);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* Re-assert so the `hidden` attribute wins over `display:grid` — the same slice-1 trap
   .dropdown__menu[hidden] guards against. */
.time-popover[hidden] {
  display: none;
}

/* Mirrors .filter-menu__label, so these captions read as the same system as every other
   grouping label in the app. */
.time-popover__label {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-muted);
}

.time-popover__label[hidden] {
  display: none;
}

.time-popover__value {
  margin: 0;
  font-size: var(--text-sm);
  white-space: nowrap;
}

.time-popover__value[hidden] {
  display: none;
}

/* Qualifies the local line rather than standing as a third fact, so it sits under it in the
   value column. */
.time-popover__zone {
  grid-column: 2;
  margin: 0;
  font-size: 11px;
  color: var(--color-muted);
}

/* Sectioned filter popover (Status / Type / Priority) — taller than a plain menu, so cap + scroll. */
.filter-menu {
  min-width: 184px;
  max-height: 60vh;
  overflow-y: auto;
}

.filter-menu__section {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.filter-menu__label {
  margin: var(--space-1) var(--space-2) 2px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-muted);
}

.conversation-rows {
  list-style: none;
  margin: 0;
  padding: 0;
}

.conversation-row {
  border-bottom: 1px solid var(--color-border);
}

.conversation-row__link {
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-areas: "badges badges" "contact time" "subject count" "snippet snippet";
  gap: var(--space-1) var(--space-2);
  padding: var(--space-3) var(--space-4);
}

.conversation-row__link:hover {
  background: var(--color-surface-sunken);
}

.conversation-row__link[aria-current=true] {
  background: var(--color-accent-soft);
}

.conversation-row__contact {
  grid-area: contact;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__subject {
  grid-area: subject;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__snippet {
  grid-area: snippet;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.conversation-row__time {
  grid-area: time;
  color: var(--color-muted);
  font-size: 12px;
  white-space: nowrap;
}

/* The shared badge stripped to coloured text here — a row already carries three lines, and a run of
   pills on top of them competes with the subject for attention. Only the chrome is dropped; the
   .badge--* variant still owns the colour, so the strip and the pane's chips stay one language.
   Wrapping rather than scrolling: several types must push the sender down, never widen the list or
   clip a type off the right edge, and the wider gap is what separates them once the pills are gone. */
/* align-items: uniform 11px text needed no cross-axis rule; a fixed-height glyph beside it does. */
.conversation-row__badges {
  grid-area: badges;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0 var(--space-3);
  min-width: 0;
}

.conversation-row__badges .badge {
  display: block;
  padding: 0;
  border: 0;
  background: none;
  font-size: 11px;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The glyph stands for the value on its own here, so this wrapper carries the accessible name.
   It also pulls back from the strip's shared gap: --space-3 is what two type NAMES need between
   them to read as separate words, and a 14px mark set against the first of them needs less than
   that or it drifts away from the row it labels. Expressed as the difference between the two
   tokens rather than a bare -4px, so it still tracks the scale if the gap ever changes. */
.conversation-row__priority {
  display: inline-flex;
  margin-right: calc(var(--space-2) - var(--space-3));
}

.conversation-row__count {
  grid-area: count;
  align-self: center;
  color: var(--color-accent);
  font-weight: 700;
  font-size: var(--text-sm);
  text-align: right;
  white-space: nowrap;
}

/* ---------- Conversation thread ---------- */
.app__thread {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--color-surface-sunken);
}

/* The turbo-frame fills the pane and lays out its two columns: the thread and the info rail. The
   grid lives here rather than on .app because a list row navigates THIS frame, and a frame
   navigation swaps only the frame it targets — a rail outside it would keep describing the thread
   the reader just left.
   `auto` on the second track so the "Select a conversation" empty state (a lone child) reserves no
   dead column; minmax(0,1fr) on the row so the thread's inner scroll region sizes to the frame
   instead of to its own content. */
#conversation {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: 1fr auto;
  grid-template-rows: minmax(0, 1fr);
}

/* turbo_stream_from renders a real element inside this frame, and a grid lays out every child —
   so left alone it takes the 1fr column, pushes the thread into the `auto` track and wraps the
   rail onto a second row. It draws nothing, so it gets no box. */
#conversation > turbo-cable-stream-source {
  display: none;
}

/* The thread column: fixed header + scrolling messages + pinned composer. */
.thread {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
}

.thread__header {
  flex-shrink: 0;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: var(--space-4) var(--space-5);
}

.thread__subject {
  margin: 0;
  font-size: var(--text-xl);
}

/* ---------- Conversation info rail (the thread's 4th pane) ---------- */
/* Surface rather than the pane's sunken background, so it reads as its own region beside the
   thread — the same treatment the conversation list gets. */
.thread-info {
  width: 300px;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: var(--space-4);
  overflow-y: auto;
  background: var(--color-surface);
  border-left: 1px solid var(--color-border);
}

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

/* Mirrors .filter-menu__label, so a section caption here reads as the same system as the one
   above the toolbar's filter groups. */
.thread-info__heading {
  margin: 0;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--color-muted);
}

/* Triage. The two pickers are buttons, not inputs, so the captions above them are spans — laid
   out as their own row so a wrapped set of chips never collides with the label beside it. */
.thread-info__form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.thread-info__field {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  align-items: flex-start;
}

.thread-info__label {
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* Details. A grid rather than the <dl>'s default block flow, so the terms share one column and
   the values line up against it. */
.thread-info__rows {
  margin: 0;
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-3);
  align-items: baseline;
}

.thread-info__term {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The padding is not decoration: `overflow: hidden` (for the ellipsis) also shears anything drawn
   OUTSIDE a child's box, and the project link's focus ring is drawn there. The negative margin
   gives the ring its 2px back without moving the text off the term column's baseline grid. */
.thread-info__value {
  margin: 0 calc(var(--space-1) * -1);
  padding: var(--space-1);
  min-width: 0;
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__muted {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The workflow state's colour, as Linear holds it. A dot rather than a badge: the rows around it
   are quiet text, and a pill here would out-shout the ticket key above it. Decorative — the state's
   name sits beside it, so the colour is never the only thing carrying the meaning. */
.thread-info__state-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-right: var(--space-2);
  border-radius: 999px;
  background: var(--state-color);
  /* Keeps a pale state colour from vanishing into the surface. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
}

.thread-info__link {
  color: var(--color-accent);
  font-weight: 600;
}

.thread-info__link:hover {
  text-decoration: underline;
}

/* Sender. Tighter than the section's own gap: the name and the address are one block of two lines,
   not two separate facts. Both truncate — an address is the longest plausible string in the rail. */
.thread-info__sender {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.thread-info__sender-name {
  margin: 0;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__sender-email {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__sublabel {
  margin: 0;
  font-size: var(--text-sm);
  color: var(--color-muted);
}

/* Their other threads. The negative margin lets a row's hover bleed to the rail's padding edge
   without the rows themselves sitting flush against it. */
.thread-info__threads {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.thread-info__thread-link {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--space-2);
  align-items: baseline;
  padding: var(--space-2);
  margin: 0 calc(var(--space-2) * -1);
  border-radius: var(--radius-sm);
}

.thread-info__thread-link:hover {
  background: var(--color-surface-sunken);
}

.thread-info__thread-subject {
  min-width: 0;
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.thread-info__thread-time {
  color: var(--color-muted);
  font-size: 12px;
  white-space: nowrap;
}

.thread-info__more {
  font-size: var(--text-sm);
  color: var(--color-accent);
  font-weight: 600;
}

.thread-info__more:hover {
  text-decoration: underline;
}

/* .empty-state's --space-6 padding and centering are built for a whole pane; in a 300px rail they
   read as a hole rather than an answer. */
.thread-info__empty {
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Actions. */
.thread-info__actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

/* button_to wraps its button in a form, which would otherwise become the flex child and leave the
   button unstretched. display:contents hands the button straight to the column. */
.thread-info__action-form {
  margin: 0;
  display: contents;
}

/* .btn sets no display, so the icon+label gap needs one declared here. */
.thread-info__action-button {
  display: inline-flex;
  align-items: center;
  justify-content: flex-start;
  gap: var(--space-2);
  width: 100%;
  white-space: nowrap;
}

.thread-info__action-icon {
  --icon-size: 14px;
}

.badge {
  display: inline-flex;
  align-items: center;
  padding: 2px var(--space-2);
  border: 1px solid;
  border-radius: 999px;
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.4;
}

.badge--danger {
  color: var(--color-danger);
  border-color: var(--color-danger);
  background: var(--color-danger-soft);
}

.badge--success {
  color: var(--color-success);
  border-color: var(--color-success);
  background: var(--color-success-soft);
}

.badge--accent {
  color: var(--color-accent);
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}

.badge--warning {
  color: var(--color-warning);
  border-color: var(--color-warning);
  background: var(--color-warning-soft);
}

.badge--muted {
  color: var(--color-muted);
  border-color: var(--color-border-strong);
  background: var(--color-surface-sunken);
}

/* A badge whose colour is not one of ours: a request type, which carries a hex an admin picked or
   a Linear label brought with it. The three properties are written inline by BadgeColor — this rule
   only says which of them goes where, so the derivation stays in one testable place. */
.badge--custom {
  color: var(--badge-fg);
  border-color: var(--badge-border);
  background: var(--badge-bg);
}

/* Priority as a hand-drawn glyph (conversations/_priority_icon): the same four tokens the badges
   carry, now with a shape as well, which is what lets the list row drop the word.

   `fill: currentColor` so one colour rule paints the whole drawing, and the colour is set on the
   svg itself rather than inherited — .dropdown__option[aria-checked="true"] recolours its own text
   to accent, and an element's own rule beats an inherited value whatever the specificity, so
   Urgent stays red while it is the checked option. */
.priority-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 auto;
  display: block;
  fill: currentColor;
}

.priority-icon--muted {
  color: var(--color-muted);
}

.priority-icon--accent {
  color: var(--color-accent);
}

.priority-icon--warning {
  color: var(--color-warning);
}

.priority-icon--danger {
  color: var(--color-danger);
}

/* Every level draws all three bars and dims the ones above it, so the glyph keeps one footprint
   and the level reads from how many are lit rather than from how much ink is on the row. Dimmed
   rather than dropped: an unlit bar is what makes "one of three" legible instead of just "small". */
.priority-icon--level-1 .priority-icon__bar:nth-child(n+2),
.priority-icon--level-2 .priority-icon__bar:nth-child(n+3) {
  opacity: 0.26;
}

/* Editable metadata trigger: the .badge look on a <button>. Reset native chrome only —
   never the badge's background/color (the .badge--* variant owns those). */
.metadata-field__trigger {
  appearance: none;
  font-family: inherit;
  gap: var(--space-1);
  cursor: pointer;
}

.metadata-field__trigger:hover {
  filter: brightness(0.97);
}

.metadata-field__trigger:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

/* Open/active: tighten the border to accent without an outset ring (avoids overflow clipping). */
.metadata-field__trigger[aria-expanded=true] {
  border-color: var(--color-accent);
}

/* Unset placeholder ("Set type") — dashed to read as "click to classify". */
.metadata-field__trigger--empty {
  border-style: dashed;
  font-weight: 500;
}

.metadata-field__label {
  white-space: nowrap;
}

.metadata-field__caret {
  --icon-size: 12px;
  opacity: 0.7;
}

.metadata-field__plus {
  --icon-size: 12px;
  opacity: 0.7;
}

/* Request types: chips sit beside the "+ Add type" trigger inside one field. Scoped to the --multi
   modifier so the single-select priority field keeps exactly the layout it has today. */
.metadata-field--multi {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

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

.metadata-field__chip {
  gap: var(--space-1);
}

/* The chip's remove button. Reusing a badge's insides on a <button> keeps the browser's own
   background and border, so those are stripped here — and the box is padded out to a real target
   rather than the ~10px the glyph alone would give. */
.badge__remove {
  appearance: none;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 18px;
  min-height: 18px;
  margin-right: -4px;
  padding: 0 2px;
  border-radius: 999px;
  cursor: pointer;
  opacity: 0.7;
}

.badge__remove:hover {
  opacity: 1;
  background: rgba(0, 0, 0, 0.08);
}

.badge__remove:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 1px;
}

.badge__remove-icon {
  --icon-size: 10px;
}

/* Checkable menu option: the tick is always laid out so labels stay aligned whether or not the
   row is on, and selection reads three ways (tick, weight, background) rather than by color alone. */
.dropdown__option--checkable {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* --iconed reserves a glyph column with padding rather than a flex gap, and hangs the glyph in it.
   That is what keeps "None" — the one option with no glyph, because unset is not a level — on the
   same text column as the four above it, the way "Clear all" already lines up under its ticks. */
.dropdown__option--iconed {
  position: relative;
  padding-left: calc(var(--space-3) + 14px + var(--space-2));
}

.dropdown__option--iconed .priority-icon {
  position: absolute;
  left: var(--space-3);
  top: 50%;
  transform: translateY(-50%);
}

.dropdown__option-check {
  --icon-size: 12px;
  flex-shrink: 0;
}

.dropdown__option[aria-checked=false] .dropdown__option-check {
  visibility: hidden;
}

/* "Clear all" carries no tick, so it is indented by the tick's width + gap to sit on the same
   text column as the options above it. */
.metadata-field__clear .dropdown__option {
  padding-left: calc(var(--space-3) + 12px + var(--space-2));
}

.dropdown__divider {
  height: 1px;
  margin: var(--space-1) 0;
  background: var(--color-border);
}

/* Metadata triggers are left-anchored, so their menu opens left-aligned — the toolbar's
   right:0 (its triggers sit on the right) would run this menu off the left edge.

   Capped and scrolled for the same reason .filter-menu is: the request-type list is admin-managed
   now, so its length is not knowable from here. Uncapped, a taxonomy of a dozen types opens a panel
   taller than the thread pane and .app__thread's overflow shears the bottom of it off — and no
   amount of scrolling recovers a clipped panel. */
.metadata-field .dropdown__menu {
  left: 0;
  right: auto;
  max-height: 60vh;
  overflow-y: auto;
}

.dropdown__empty {
  margin: 0;
  padding: var(--space-2) var(--space-3);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.thread__linear {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-3);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-size: var(--text-sm);
  font-weight: 600;
  white-space: nowrap;
}

.thread__linear:hover {
  border-color: var(--color-accent);
  color: var(--color-accent);
}

.thread__linear--plain {
  cursor: default;
}

.thread__linear--plain:hover {
  border-color: var(--color-border-strong);
  color: var(--color-text);
}

.thread__linear-icon {
  --icon-size: 14px;
}

.thread__scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* Cap the reading column so body lines stay readable on ultra-wide viewports. */
.thread__messages {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  max-width: 860px;
}

.message-card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  padding: var(--space-4) var(--space-5);
  box-shadow: var(--shadow-sm);
}

/* Agent replies read as right-aligned bubbles. */
.message-card--outbound {
  background: var(--color-accent-soft);
  border-color: #d4e2fb;
  margin-left: auto;
  max-width: 80%;
}

.message-card__meta {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}

.message-card__from {
  font-weight: 600;
}

.message-card__email {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.message-card__time {
  margin-left: auto;
  color: var(--color-muted);
  font-size: 12px;
}

.message-card__body {
  line-height: 1.55;
  word-break: break-word;
}

.message-card__body img {
  max-width: 100%;
  height: auto;
}

.message-card__body a {
  color: var(--color-accent);
  text-decoration: underline;
}

/* Agents can now send numbered and bulleted replies, and those come back through ingest into
   this same body. Left to the browser they indent 40px and space themselves at 1em, which
   reads as unstyled beside the card's own rhythm. Client mail carrying lists gets the same
   treatment, which it never had before. */
.message-card__body ul,
.message-card__body ol {
  margin: var(--space-2) 0;
  padding-left: var(--space-5);
}

.message-card__body li {
  margin: 0;
}

.message-card__body p {
  margin: 0 0 var(--space-2);
}

.message-card__body p:last-child {
  margin-bottom: 0;
}

/* On the sanitizer's allowlist and the tag every mail client wraps a quote in, but styled nowhere
   until now — so it shipped the browser's 40px default indent, which stair-steps a reply-to-a-reply
   off the side of the card. Same left rule as .quoted-body__content, so a nested quote reads as one
   more level of the same thing rather than a different treatment. */
.message-card__body blockquote {
  margin: var(--space-2) 0;
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border);
  color: var(--color-muted);
}

/* ---------- Quoted reply history ---------- */
/* Inline in the reading flow, not a bordered meta row like .blocked-images below: this is body
   content the agent may want, and on a two-word reply a full-width banner would be taller than the
   message it is decluttering. */
.quoted-body {
  margin-top: var(--space-3);
}

.quoted-body__caret {
  --icon-size: 14px;
  flex: none;
}

.quoted-body__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
}

.quoted-body.is-open .quoted-body__caret {
  transform: rotate(180deg);
}

/* The label swaps in CSS off the class `disclosure` already sets, so the shared controller three
   settings screens depend on needs no per-caller knowledge of what its button says. */
.quoted-body__label--hide {
  display: none;
}

.quoted-body.is-open .quoted-body__label--show {
  display: none;
}

.quoted-body.is-open .quoted-body__label--hide {
  display: inline;
}

.quoted-body__content {
  margin-top: var(--space-3);
  padding-left: var(--space-3);
  border-left: 2px solid var(--color-border);
  color: var(--color-muted);
}

/* ---------- Blocked remote images ---------- */
/* Deliberately mirrors the .attachments strip below — same top border, same spacing, same
   muted 14px icon + label — so a card carrying both reads as one stack of meta rows rather
   than two competing banners. */
.blocked-images {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.blocked-images__icon {
  --icon-size: 14px;
  flex: none;
}

/* Trailing edge, the way .message-card__time sits. Survives wrapping: on a narrow pane the
   button drops to its own line instead of crushing the label. */
.blocked-images__button {
  margin-left: auto;
}

/* Targeted by attribute rather than class, because the sanitizer allowlist has no `class` —
   a class here could never survive on sender markup. The image keeps its alt text, which is
   the only description of it we have, and the dashed box gives that text somewhere to sit.
   data-image-failed is set by the Stimulus controller when a revealed image fails to load. */
.message-card__body img[data-blocked-src],
.message-card__body img[data-image-failed] {
  min-width: 88px;
  min-height: 64px;
  padding: var(--space-2);
  border: 1px dashed var(--color-border-strong);
  border-radius: var(--radius-sm);
  background: var(--color-surface-sunken);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* ---------- Attachments ---------- */
.attachments {
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: 1px solid var(--color-border);
}

.attachments__label {
  display: flex;
  align-items: center;
  gap: var(--space-1);
  margin: 0 0 var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.attachments__icon {
  --icon-size: 14px;
}

.attachments__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  margin: 0;
  padding: 0;
}

/* Fixed width, not content-sized, so a row of chips with wildly different filename
   lengths still lines up. */
.attachment {
  width: 140px;
}

.attachment__link {
  display: block;
  overflow: hidden;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  color: inherit;
  text-decoration: none;
}

a.attachment__link:hover {
  border-color: var(--color-accent);
}

a.attachment__link:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* Nothing to click on a file that is still downloading or could not be stored. */
.attachment__link--inert {
  color: var(--color-muted);
  cursor: default;
}

.attachment__preview {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  background: var(--color-surface-sunken);
  border-bottom: 1px solid var(--color-border);
}

/* cover, so a very wide or very tall image fills the square without letterboxing. */
.attachment__thumb {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.attachment__icon {
  --icon-size: 28px;
  color: var(--color-muted);
}

.attachment__caption {
  display: block;
  padding: var(--space-2);
}

.attachment__name {
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--text-sm);
}

.attachment__meta {
  display: block;
  color: var(--color-muted);
  font-size: 12px;
}

/* ---------- Reply composer ---------- */
.composer {
  flex-shrink: 0;
  border-top: 1px solid var(--color-border);
  background: var(--color-surface);
  padding: var(--space-3) var(--space-4);
}

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

.composer__error {
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  /* A parked mailbox's address can be long; wrap it rather than pushing the pane wide. */
  word-break: break-word;
}

/* Trailing edge, the way .blocked-images__button sits — and it drops to its own line on a
   narrow pane instead of crushing the sentence. */
.composer__error-action {
  margin-left: auto;
}

/* Deliberately mirrors .blocked-images: same flex row, same muted 14px icon + --text-sm
   label, so the view-only strip reads as the same family of quiet explanatory rows rather
   than a new kind of banner. */
.composer__notice {
  margin: 0;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  font-size: var(--text-sm);
  color: var(--color-muted);
}

.composer__notice-icon {
  --icon-size: 14px;
  flex: none;
}

/* Only announced while a send is in flight; empty the rest of the time, so it takes up no
   room in the resting composer. */
/* The spinner in the Send button is the visual "sending" signal — this line carries the same
   state for screen readers only. The spinner is decorative (aria-hidden), so deleting this
   text rather than hiding it would leave assistive tech with nothing to announce. */
.composer__status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Sits under the box on the leading edge, aligned with the text the agent is typing.
   Quiet by design: a hint the eye can skip once it has been learned. */
.composer__hint {
  margin: 0;
  font-size: 12px;
  color: var(--color-muted);
}

/* ---------- The composer box ---------- */
/* One bordered container holds the toolbar, the editor, the picked files and Send. The error,
   hint and status lines stay outside it — they are messages *about* the send rather than part
   of the surface the agent composes on. */
.composer__box {
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-md);
  background: var(--color-surface);
  overflow: hidden;
}

/* The focus ring moves from the field to the container, because the editor is no longer the
   only thing inside it. Same treatment as .field__input:focus, so it reads as the same family. */
.composer__box:focus-within {
  border-color: var(--color-accent);
  box-shadow: 0 0 0 3px var(--color-accent-soft);
}

.composer__toolbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-1);
  padding: var(--space-1) var(--space-2);
  background: var(--color-surface-sunken);
  border-bottom: 1px solid var(--color-border);
}

/* These are <button>s, so the UA background, border and font have to be reset explicitly —
   borrowing an icon-button look without stripping the native chrome leaves grey buttonface
   showing through. */
.composer__tool {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text);
  font: inherit;
  cursor: pointer;
}

.composer__tool:hover {
  background: var(--color-accent-soft);
}

/* The pressed look mirrors .btn--ghost.is-on and .dropdown__option[aria-checked] — the app
   already says "on" this way. Both colour and background carry it, never colour alone. */
.composer__tool[aria-pressed=true],
.composer__tool[aria-expanded=true] {
  color: var(--color-accent);
  background: var(--color-accent-soft);
}

.composer__tool:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* The global :focus-visible ring sits 2px outside the element, which on a 28px button in a
   28px-tall band collides with the container border. Drawn inside instead. */
.composer__tool:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: -2px;
}

.composer__tool-icon {
  --icon-size: 18px;
}

/* .dropdown__divider turned on its side — but a step darker than that one, because it sits on
   the sunken toolbar band rather than on white, and --color-border against --color-surface-sunken
   is too close in value to read as a rule at 1px. */
.composer__tool-divider {
  width: 1px;
  height: 20px;
  margin: 0 var(--space-1);
  background: var(--color-border-strong);
  flex: none;
}

/* In the flow, not floating: .app__thread clips its overflow and this composer sits on its
   bottom edge, so an absolutely-positioned popover would be cut off exactly where it opens. */
.composer__link-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding: var(--space-2);
  background: var(--color-surface-sunken);
  border-bottom: 1px solid var(--color-border);
}

.composer__link-row[hidden] {
  display: none;
}

.composer__link-icon {
  --icon-size: 14px;
  flex: none;
  color: var(--color-muted);
}

.composer__link-input {
  flex: 1;
  min-width: 140px;
  width: auto;
  padding: var(--space-1) var(--space-2);
  font-size: var(--text-sm);
}

.composer__link-input--invalid {
  border-color: var(--color-danger);
}

.composer__link-error {
  flex-basis: 100%;
  margin: 0;
  font-size: 12px;
  color: var(--color-danger);
}

.composer__link-error[hidden] {
  display: none;
}

/* Visually hidden but present: the placeholder is not an accessible name. */
.composer__link-label {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.composer__editor {
  min-height: 44px;
  max-height: 40vh;
  overflow-y: auto;
  padding: var(--space-3);
  line-height: 1.5;
  word-break: break-word;
  /* The box owns the focus ring; a second one on the editor would draw a rectangle inside a
     rectangle. */
  outline: none;
}

.composer__editor:empty::before {
  content: attr(data-placeholder);
  color: var(--color-muted);
  pointer-events: none;
}

/* The agent's own formatting, shown the way the recipient will see it. Lists especially: the
   browser default indents them 40px, which reads as broken next to 12px of padding. */
.composer__editor p {
  margin: 0 0 var(--space-2);
}

.composer__editor p:last-child {
  margin-bottom: 0;
}

.composer__editor ul,
.composer__editor ol {
  margin: var(--space-2) 0;
  padding-left: var(--space-5);
}

.composer__editor a {
  color: var(--color-accent);
  text-decoration: underline;
}

/* ---------- Picked files ---------- */
.composer__files {
  padding: var(--space-2) var(--space-3);
  border-top: 1px solid var(--color-border);
}

.composer__files[hidden] {
  display: none;
}

/* Capped so a reply with eight files still leaves the editor and Send reachable. */
.composer__files-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 25vh;
  overflow-y: auto;
}

.composer__file {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) 0;
  font-size: var(--text-sm);
}

.composer__file-icon {
  --icon-size: 16px;
  flex: none;
  color: var(--color-muted);
}

/* Both glyphs are in the markup and CSS picks one, so the Stimulus controller only toggles a
   class on the row and never reaches into the icon. The shown glyph restates .icon's
   inline-flex — `block` here would drop the centring and sit the glyph on the text baseline. */
.composer__file-icon--image {
  display: none;
}

.composer__file--image .composer__file-icon--image {
  display: inline-flex;
}

.composer__file--image .composer__file-icon--file {
  display: none;
}

/* The flexible column: the name ellipsises, the size and the remove control never shrink. */
.composer__file-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.composer__file-size {
  color: var(--color-muted);
  font-size: 12px;
  flex: none;
}

.composer__file-status {
  color: var(--color-muted);
  font-size: 12px;
  flex: none;
}

.composer__file.is-failed {
  color: var(--color-danger);
}

.composer__file.is-failed .composer__file-status {
  color: var(--color-danger);
}

.composer__file.is-uploading .composer__file-size {
  display: none;
}

.composer__file-remove {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-muted);
  font: inherit;
  cursor: pointer;
  flex: none;
}

.composer__file-remove:hover {
  color: var(--color-danger);
  background: var(--color-danger-soft);
}

.composer__file-remove-icon {
  --icon-size: 14px;
}

.composer__files-total {
  margin: var(--space-1) 0 0;
  font-size: 12px;
  color: var(--color-muted);
}

.composer__files-total--over {
  color: var(--color-danger);
}

/* Sits below the running total and survives it: a rejected file never joins the list, so this
   is the only record that it was dropped. */
.composer__files-rejected {
  margin: var(--space-1) 0 0;
  font-size: 12px;
  color: var(--color-danger);
  word-break: break-word;
}

.composer__files-rejected[hidden] {
  display: none;
}

/* ---------- Action row ---------- */
/* No rule above it, unlike the toolbar: the bottom row reads as part of the editor's white
   field rather than as a second band. */
.composer__actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2);
}

/* The one glyph carrying its own background, as the reference design has it. */
.composer__attach {
  border-radius: 999px;
  background: var(--color-surface-sunken);
  color: var(--color-muted);
}

.composer__attach:hover {
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

/* Never rendered, but still focusable and announced if left in the tab order. */
.composer__file-input {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

.composer__send {
  flex: 0 0 auto;
  margin-left: auto;
  width: 32px;
  height: 32px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* Muted and flat until there is something to send, so an armed composer is visibly different
   from an empty one rather than differing only in the cursor. */
.composer__send:disabled {
  opacity: 1;
  background: transparent;
  color: var(--color-muted);
  cursor: not-allowed;
}

/* ---------- Resting vs. expanded ---------- */
/* At rest the box is nothing but the editor line — the same treatment as .field__input, one row
   high. The toolbar and the action row collapse to zero rather than display:none, because a
   height of zero is something that can be animated to and from. visibility is what takes their
   eight controls out of the tab order and the accessibility tree while they cannot be seen;
   overflow:hidden on the child is what lets a grid item shrink below its content. */
.composer__collapse {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows 0.15s ease, visibility 0.15s;
}

/* Its own element rather than the toolbar or the action row directly: overflow:hidden lets a
   grid item's content shrink to nothing, but the item's own padding and border survive it — and
   both of those rows are padded, which would leave 25px of empty box behind at rest. */
.composer__collapse-inner {
  overflow: hidden;
}

.composer__form.is-expanded .composer__collapse {
  grid-template-rows: 1fr;
  visibility: visible;
}

/* grid-template-rows is chosen over max-height because the toolbar wraps to a second row on a
   narrow pane: any fixed ceiling would either clip it or make the collapse snap late. */
/* The hint sits outside the box, in a flex column with a gap — collapsing it to zero height
   would leave that 8px gap behind as a ghost row, so it is simply not rendered while resting. */
.composer__form:not(.is-expanded) .composer__hint {
  display: none;
}

/* The send blocks on the Gmail API for about a second. Without a visible in-flight state the
   button reads as dead and invites a second click. */
.composer__spinner {
  display: none;
  width: 16px;
  height: 16px;
  border: 2px solid rgba(255, 255, 255, 0.4);
  border-top-color: #fff;
  border-radius: 50%;
  animation: composer-spin 0.6s linear infinite;
}

/* The button is disabled while sending, but the spinner is now the only signal that the reply
   is on its way — so it keeps the full accent background rather than the resting disabled look,
   which is transparent and would leave a white ring drawn on nothing. */
.composer__form.is-sending .composer__send:disabled {
  opacity: 1;
  background: var(--color-accent);
  color: #fff;
}

.composer__form.is-sending .composer__spinner {
  display: block;
}

.composer__form.is-sending .composer__send-icon {
  display: none;
}

@keyframes composer-spin {
  to {
    transform: rotate(360deg);
  }
}
/* Respect a reduced-motion preference: the state still reads from the swapped glyph and the
   "Sending…" line, so the spin itself is the only thing dropped. */
@media (prefers-reduced-motion: reduce) {
  .composer__spinner {
    animation: none;
  }
  /* The two states still read from what is on screen; only the movement between them is dropped. */
  .composer__collapse {
    transition: none;
  }
}
/* ---------- Empty states ---------- */
.empty-state {
  padding: var(--space-6);
  text-align: center;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* ---------- Responsive: stack panes on narrow viewports ---------- */
@media (max-width: 900px) {
  .app {
    grid-template-columns: 1fr;
    grid-template-rows: auto auto 1fr;
    height: auto;
    min-height: 100vh;
  }
  /* Every collapsed rule is scoped above this breakpoint, so down here the toggle would flip the
     class, write the cookie and change nothing on screen — a control that claims it worked and
     then surprises the reader with a narrow rail the next time they open a desktop. It only
     exists where it does something. */
  .sidebar__collapse-toggle {
    display: none;
  }
  .app__list, .app__thread {
    max-height: none;
  }
  /* Let the thread pane flow with the page (no fixed height to scroll inside), so the
     messages and composer stack naturally instead of collapsing the inner scroll region. */
  .app__thread {
    overflow: visible;
  }
  /* Flex rather than block so the rail can be ordered above the thread — see below. */
  #conversation {
    display: flex;
    flex-direction: column;
  }
  .thread__scroll {
    overflow: visible;
  }
  /* Left in DOM order the rail would land below a full message list AND the composer, which on a
     phone is the same as not being there. overflow:visible because the rail no longer has a height
     to scroll within — and because clipping here would shear the pickers' menus. */
  .thread-info {
    order: -1;
    width: auto;
    overflow: visible;
    border-left: 0;
    border-bottom: 1px solid var(--color-border);
  }
}
/* ---------- Button variants used by Settings ---------- */
.btn--sm {
  padding: 4px var(--space-2);
  font-size: 12px;
  width: auto;
}

.btn--danger {
  background: var(--color-danger-soft);
  color: var(--color-danger);
  border-color: #f6c9c4;
}

.btn--danger:hover {
  background: #fce4e2;
}

.btn--ghost.is-on {
  color: var(--color-accent);
  border-color: var(--color-accent);
}

/* ---------- Modal overlay (Settings) ---------- */
.modal-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: rgba(16, 24, 40, 0.45);
}

.modal {
  display: flex;
  flex-direction: column;
  width: 100%;
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg, 0 20px 50px rgba(16, 24, 40, 0.25));
  overflow: hidden;
  outline: none;
}

.modal.settings {
  max-width: 900px;
  min-height: 500px;
  max-height: 82vh;
}

.modal__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: var(--space-4) var(--space-5);
  border-bottom: 1px solid var(--color-border);
}

.modal__title {
  margin: 0;
  font-size: var(--text-lg);
}

.modal__close {
  appearance: none;
  border: 1px solid transparent;
  background: transparent;
  color: var(--color-muted);
  border-radius: var(--radius-md);
  padding: var(--space-1);
  cursor: pointer;
  display: inline-flex;
}

.modal__close:hover {
  color: var(--color-text);
  background: var(--color-surface-sunken);
}

.settings__flash:empty {
  display: none;
}

.settings__flash {
  padding: 0 var(--space-5);
}

.settings__flash-msg {
  margin: var(--space-3) 0 0;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
}

.settings__flash-msg--notice {
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

.settings__flash-msg--error {
  background: var(--color-danger-soft);
  color: var(--color-danger);
}

.settings__body {
  display: grid;
  grid-template-columns: 190px 1fr;
  min-height: 0;
  flex: 1;
}

.settings__menu {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: var(--space-4);
  border-right: 1px solid var(--color-border);
  background: var(--color-surface-sunken);
}

.settings__menu-item {
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-md);
  color: var(--color-text);
  font-weight: 600;
  font-size: var(--text-sm);
}

.settings__menu-item:hover {
  background: var(--color-surface);
}

.settings__menu-item[aria-current=true] {
  background: var(--color-accent-soft);
  color: var(--color-accent);
}

.settings__detail {
  display: block;
  overflow-y: auto;
  padding: var(--space-5);
  min-height: 0;
}

/* .btn--primary is full-width by default because its first home was the sidebar's + Project form,
   where it spans the rail. In either settings modal it sits inline beside the field it submits, so
   both surfaces opt out — the project modal included, or its Save would stretch and wrap. */
:is(.settings, .project-settings) .btn--primary {
  width: auto;
}

.settings-section__head {
  margin-bottom: var(--space-4);
}

.settings-section__title {
  margin: 0;
  font-size: var(--text-lg);
}

.settings-section__hint {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

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

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

.settings-form__row .field__input {
  flex: 1;
  min-width: 120px;
}

.settings-form__check {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-size: var(--text-sm);
  color: var(--color-muted);
  white-space: nowrap;
}

.settings-form__error {
  margin: 0 0 var(--space-2);
  color: var(--color-danger);
  font-size: var(--text-sm);
}

.settings-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.settings-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-row--stack {
  flex-direction: column;
  align-items: stretch;
}

.settings-row__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}

.settings-row__main {
  display: flex;
  flex-direction: column;
  gap: 1px;
  flex: 1;
  min-width: 0;
}

.settings-row__name {
  font-weight: 600;
}

.settings-row__sub {
  color: var(--color-muted);
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-row__badge {
  flex-shrink: 0;
}

.settings-row__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
}

.settings-row__action {
  display: inline;
  margin: 0;
}

.settings-sublist {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.settings-sublist__item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.settings-sublist__label {
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-sublist__empty {
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-subform {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

.settings-subform .field__input {
  flex: 1;
  min-width: 0;
}

/* The project row's Linear wiring. Wraps rather than squeezes, so the two selects drop onto their
   own lines at narrow width instead of shrinking past the point their option text is readable. */
.settings-linear {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-linear__label,
.settings-auto-reply__label {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--color-muted);
}

.settings-linear__form {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  flex: 1;
  min-width: 0;
}

.settings-linear__form .field__select {
  flex: 1;
  min-width: 140px;
  font-size: var(--text-sm);
}

.settings-linear__static {
  font-size: var(--text-sm);
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The reset link an admin generates for a user. Spaced like the two project-row blocks above so
   every stacked settings row breaks the same way.

   min-width:0 on the field is load-bearing, not defensive: a flex item's default min-width is its
   content, and a ~100-character URL is wide enough to shove the Copy button clean off the row.
   The hint claims a whole line of its own so it never competes with them for the remainder. */
.settings-reset-link {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-reset-link__url {
  flex: 1;
  min-width: 0;
  font-size: var(--text-sm);
}

.settings-reset-link__hint {
  flex-basis: 100%;
  margin: 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* The project row's acknowledgement switch. Sibling of the Linear block above and spaced to match
   it, so the two settings read as one column rather than two treatments.

   __note belongs to the per-project modal, where the section has room to explain itself: it states
   a consequence of the checkbox above it, so it trails the control rather than sitting in the
   section header where the other hints live. The admin list has no room to repeat that per row and
   says it once in its own header instead. */
.settings-auto-reply {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

.settings-auto-reply__note {
  margin-top: var(--space-2);
}

/* The two admin taxonomies — request types in Settings, project parts in the project modal. One
   rule set rather than two, because they are the same row at two scopes and a copy would drift.
   The row carries its own editor, revealed by the disclosure controller — so the read-only
   description is hidden while that editor is open rather than printing the same sentence twice. */
.settings-taxonomy__preview {
  margin: var(--space-2) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.settings-row.is-open .settings-taxonomy__preview {
  display: none;
}

/* margin-left:auto rather than relying on the head's space-between: with three children the middle
   one floats by the chip's width, so counts staggered across ~40px while the buttons beside them
   were flush. Pushed right, the numbers read as the column they are. */
.settings-taxonomy__usage {
  flex-shrink: 0;
  margin-left: auto;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* Page-specific override, not a change to the shared .badge: at the narrow breakpoint a two-word
   type name broke the pill onto two lines. Scoped here so every other badge keeps wrapping. */
.settings-taxonomy__badge {
  white-space: nowrap;
  flex-shrink: 0;
}

.settings-taxonomy__actions {
  display: flex;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-2);
}

/* The colour picker: swatches laid out inline, NOT in a .dropdown popover. The settings modal is
   overflow:hidden around a scrolling .settings__detail, so an absolutely positioned menu opened
   from a row near the bottom is clipped — the same constraint that keeps the Linear team pickers
   native selects.

   Real radio inputs do the work: the platform gives arrow-key movement, roving focus and :checked
   for free, so there is no controller here. The input itself is taken out of the visual flow but
   left focusable, and the swatch reacts to its state through :checked/:focus-visible siblings. */
.color-swatches {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  border: 0;
}

.color-swatches__legend {
  padding: 0;
  margin-bottom: var(--space-2);
  color: var(--color-muted);
  font-size: var(--text-sm);
}

.color-swatch {
  display: inline-flex;
  cursor: pointer;
}

/* Not display:none — that would take the radio out of the tab order and off the accessibility
   tree, which is the whole reason for using real inputs. */
.color-swatch__input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.color-swatch__dot {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 999px;
  background: var(--swatch-color);
  /* An inset ring rather than an outset one: .settings__detail scrolls, and an outset ring on the
     bottom row would be clipped by it. */
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12);
  color: #fff;
}

/* White tick on a colour nobody vetted — a pale Linear label would swallow it. The shadow is what
   makes it read on a light swatch as well as a dark one, without needing a second derived colour. */
.color-swatch__check {
  --icon-size: 12px;
  visibility: hidden;
  filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.7));
}

/* Selection reads two ways beyond the colour — the tick and the ring — never by colour alone.
   The ring's inner band is --color-text rather than the swatch's own colour: ringing a colour in
   itself is invisible, which left the tick carrying the whole signal. Dark-on-white reads at 24px
   whatever the swatch is.
   Still drawn INSET, and the dot does not change size: an outset ring on the bottom row of a
   scrolling pane is what gets clipped, and a dot that grew on selection would reflow the row
   every time the choice moved. */
.color-swatch__input:checked + .color-swatch__dot {
  box-shadow: inset 0 0 0 2px var(--color-surface), inset 0 0 0 4px var(--color-text);
}

.color-swatch__input:checked + .color-swatch__dot .color-swatch__check {
  visibility: visible;
}

.color-swatch__input:focus-visible + .color-swatch__dot {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

/* The app's first textarea. .field__input already gives it the border, padding and focus ring;
   only the resize handle's axis is left to say, so it cannot be dragged wider than its column. */
textarea.field__input {
  resize: vertical;
  min-height: calc(2lh + var(--space-4));
}

/* The add form's rows stack, unlike the one-line Users and Projects forms above. */
.settings-form__toggle {
  margin-bottom: var(--space-2);
}

.settings-form__row + .settings-form__row {
  margin-top: var(--space-2);
}

.settings-permissions {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.settings-permissions__user {
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

.settings-permissions__projects {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.settings-permissions__project {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.settings-permissions__project-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.settings-permissions__note {
  margin: var(--space-1) 0 0;
  color: var(--color-muted);
  font-size: var(--text-sm);
}

/* form_with wraps each select in a <form>; without this the block element breaks the row. */
.settings-permissions__form {
  display: contents;
}

/* Fixed width so every row's control shares one left and one right edge. A native select sizes
   to its content, and "No access" against "Can view & reply" would leave the column ragged —
   and the column edge is what the eye follows when scanning a card. */
.settings-permissions__access {
  flex: 0 0 auto;
  min-width: 148px;
  font-size: var(--text-sm);
}

/* Ungranted rows recede so the granted ones are what the eye catches. Supplementary only: the
   level is stated in the select's own text, never by colour alone. */
.settings-permissions__access--none {
  color: var(--color-muted);
}

/* ---------- Per-project settings modal ----------
   One scrolling panel rather than the admin modal's menu + detail pane: its four sections are a
   few rows each, so a 190px menu would add a click and leave half the pane empty. */
.modal.project-settings {
  max-width: 640px;
  max-height: 82vh;
}

/* The title carries a project name of arbitrary length and sits next to a fixed ✕. */
.project-settings .modal__title {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Mirrors .settings__detail, but as the modal's only body it also has to claim the height. */
.project-settings__body {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: var(--space-5);
}

/* No existing rule covers this: the admin detail pane only ever renders ONE .settings-section, so
   nothing there ever needed sibling spacing. */
/* Rails wraps a field that failed validation in <div class="field_with_errors">, and nothing in
   this app styles it. Dropped into .settings-form__row — a flex row whose .field__input is the
   flex:1 item — the div becomes the flex item instead and the input shrinks to its intrinsic
   width, exactly when the admin needs to edit what they just typed. display:contents takes the
   wrapper out of the layout while leaving it in the DOM.
   Scoped to this modal rather than global: the admin settings forms carry the same wrapper and the
   same latent collapse, but widening a layout fix onto surfaces this change was asked to leave
   alone is not this slice's call. */
.project-settings .field_with_errors {
  display: contents;
}

.project-settings__section + .project-settings__section {
  margin-top: var(--space-5);
  padding-top: var(--space-5);
  border-top: 1px solid var(--color-border);
}

.settings-account {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
}

/* The role tag sits in a flex column; hug its content instead of stretching full width. */
.settings-account .settings-row__badge {
  align-self: flex-start;
  margin-top: 2px;
}

@media (max-width: 640px) {
  .settings__body {
    grid-template-columns: 1fr;
  }
  .settings__menu {
    flex-direction: row;
    flex-wrap: wrap;
    border-right: 0;
    border-bottom: 1px solid var(--color-border);
  }
  .modal.settings {
    max-height: 92vh;
  }
  .modal.project-settings {
    max-height: 92vh;
  }
  /* Stack each row's actions below its name/badge so they don't collide at narrow width. */
  .settings-row {
    flex-wrap: wrap;
  }
  .settings-row__main {
    flex-basis: 60%;
  }
  .settings-row__actions {
    width: 100%;
  }
  /* The access select's fixed width is what aligns the column at full width; here there is no
     room for it beside the project name, so the name takes the line and the select the next. */
  .settings-permissions__project {
    flex-wrap: wrap;
  }
  .settings-permissions__access {
    min-width: 100%;
  }
  /* A request-type row carries three actions — triage can be switched off per type — which is one
     more than fits beside a name and a thread count here. Left to wrap on its own, each row broke
     at a different point depending on how wide its count happened to be ("1 thread" vs "12
     threads"), so a lone Delete button hung under some rows and not others. Giving the actions
     their own line makes every row break the same way. A parts row carries two rather than three,
     and takes the same treatment so the two sections break identically. */
  .settings-row__head {
    flex-wrap: wrap;
  }
  .settings-row__head .settings-row__actions {
    justify-content: flex-start;
  }
  .settings-taxonomy__usage {
    margin-left: 0;
  }
}

/*# sourceMappingURL=/assets/application-4440301b.css.map */
