/* =====================================================================
   components.css — buttons, cards, chips, modals, tables, task rows
   ===================================================================== */

/* ─── Buttons ─── */
.btn {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-2);
    padding: 8px 14px;
    border: 1px solid var(--border);
    background: var(--bg-elev);
    color: var(--text);
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-size: var(--fz-14);
    text-decoration: none;
    transition: background 120ms ease, border-color 120ms ease;
}
.btn:hover { background: var(--border); text-decoration: none; }
.btn:focus-visible { outline-offset: 2px; }
.btn[disabled], .btn.is-disabled { opacity: 0.5; pointer-events: none; }

/* Shown inside a submit button while its form is in flight (submit-lock.js).
   Any submit that proceeds gets one — not just uploads — because the wait
   people kept double-pressing through was a text-only message POST sending its
   notification emails inline. */
.btn-spinner {
    display: inline-block;
    width: 12px; height: 12px;
    border: 2px solid currentColor;
    border-right-color: transparent;
    border-radius: 50%;
    opacity: 0.7;
    animation: btn-spin 0.7s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
    .btn-spinner { animation: none; }
}

/* A form submit-lock.js has locked. The submit button is already disabled (and
   .btn[disabled] above takes its pointer events with it); this covers the rest
   of the form, so a stray click on "Attach files" or a second Enter in the
   textarea can't start anything either, and the cursor says why.

   Dimmed only slightly. The form is on its way out — the POST either navigates
   or redirects — so this is a "that's gone now" cue, not a disabled state the
   user has to read through. */
form.is-submitting {
    cursor: progress;
}
form.is-submitting input,
form.is-submitting textarea,
form.is-submitting select,
form.is-submitting label,
form.is-submitting .editor__surface,
form.is-submitting .ql-editor {
    pointer-events: none;
    opacity: 0.75;
}

.btn--primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--accent-fg);
}
.btn--primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); }

.btn--ghost { background: transparent; border-color: transparent; }
.btn--ghost:hover { background: var(--bg-elev); }

/* Dark-red in both themes, so the text must be light — otherwise it inherits
   the near-black --text in light mode (dark text on dark red). */
.btn--danger { background: #7f1d1d; border-color: #7f1d1d; color: #fff; }
.btn--danger:hover { background: #991b1b; border-color: #991b1b; color: #fff; }

.btn--sm { padding: 4px 10px; font-size: var(--fz-12); }
.btn--lg { padding: 11px 28px; font-size: var(--fz-16); font-weight: 600; }
.btn--icon { padding: 6px; }
.btn .icon { width: 16px; height: 16px; }

/* ─── Cards / panels ─── */
.card {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--card-pad);
    margin-bottom: var(--sp-4);
}
.card:last-child { margin-bottom: 0; }
.card__header {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    margin: calc(var(--card-pad) * -1) calc(var(--card-pad) * -1) var(--card-pad);
    padding: var(--card-pad);
    border-bottom: 1px solid var(--border);
}
.card__title { font-size: var(--fz-16); font-weight: 600; margin: 0; }
.card__actions { margin-left: auto; }

/* ─── Stat cards (dashboard) ─── */
.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
    gap: var(--sp-3);
    margin-bottom: var(--sp-4);
}
.stat {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: var(--sp-4);
}
.stat__label { font-size: var(--fz-12); color: var(--text-dim); text-transform: uppercase; letter-spacing: 0.04em; }
.stat__value { font-size: var(--fz-32); font-weight: 700; margin-top: var(--sp-1); }
.stat--accent { border-color: var(--accent-dim); }
.stat--warn   { border-color: #b45309; }
.stat--danger { border-color: #991b1b; }

/* ─── Chips ─── */
.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--sp-1);
    padding: 2px 8px;
    border-radius: 999px;
    font-size: var(--fz-12);
    border: 1px solid var(--border);
    background: var(--bg-elev);
    line-height: 1.6;
    white-space: nowrap;
}
/* Small chip, used wherever one rides along inside dense text (board cards, the
   activity feed, archived-task hits in search). Was duplicated in board.css and
   activity.css; board's copy said var(--fz-10), a token that doesn't exist, so
   its chips silently stayed 12px. One definition, with a literal fallback. */
.chip--xs {
    font-size: var(--fz-10, 10px);
    padding: 1px 6px;
}
/* Billable work (migration 084): a green "$" on task rows and board cards. */
.billable-mark { color: #22c55e; border-color: #166534; font-weight: 700; }
.chip--urgent {
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    background: rgba(239,68,68,.15);
    border-color: var(--priority-urgent);
    color: var(--priority-urgent);
}
/* Selected/active filter chip (Activity feed tabs, client Files tag filter) */
.chip--active {
    background: var(--accent-hi);
    border-color: var(--accent-hi);
    color: #fff !important;
    text-decoration: none;
}

.chip[data-status="not_started"]     { color: var(--status-not_started);     border-color: var(--status-not_started); }
.chip[data-status="in_progress"]     { color: var(--status-in_progress);     border-color: var(--status-in_progress); }
.chip[data-status="internal_review"] { color: var(--status-internal_review); border-color: var(--status-internal_review); }
.chip[data-status="client_review"]   { color: var(--status-client_review);   border-color: var(--status-client_review); }
.chip[data-status="implementation"]  { color: var(--status-implementation);  border-color: var(--status-implementation); }
.chip[data-status="on_hold"]         { color: var(--status-on_hold);         border-color: var(--status-on_hold); }
.chip[data-status="pending_client"]  { color: var(--status-pending_client);  border-color: var(--status-pending_client); }
.chip[data-status="completed"]       { color: var(--status-completed);       border-color: var(--status-completed); }

.chip--tag {
    background: transparent;
    border-color: currentColor;
}

/* ─── Urgent pulse — a glow that grows and fades, drawing the eye without
   blinking on/off. (Previous version interpolated between two effectively-
   invisible box-shadow states — 0 spread and 0 opacity — so nothing showed.) ─── */
@keyframes urgent-pulse {
    0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.55); }
    50%      { box-shadow: 0 0 10px 3px rgba(239,68,68,0.55); }
}
.is-urgent {
    border-color: var(--priority-urgent) !important;
    animation: urgent-pulse 1.8s ease-in-out infinite;
}

/* ─── Progress bar ─── */
.progress {
    width: 100%;
    height: 6px;
    background: var(--bg-elev);
    border-radius: 999px;
    overflow: hidden;
}
.progress__bar {
    height: 100%;
    background: linear-gradient(90deg, var(--accent), var(--accent-hi));
    transition: width 200ms ease;
}
.progress--tall { height: 10px; }

/* ─── Task row (dashboard / lists) ─── */
.task-row {
    display: flex;
    gap: var(--sp-3);
    padding: var(--sp-3);
    border-bottom: 1px solid var(--border);
    align-items: center;
    background: var(--bg-panel);
    transition: background 100ms ease;
}
.task-row:first-child { border-top-left-radius: var(--radius); border-top-right-radius: var(--radius); }
.task-row:last-child  { border-bottom: none; border-bottom-left-radius: var(--radius); border-bottom-right-radius: var(--radius); }
.task-row:hover { background: var(--bg-elev); cursor: pointer; }

/* ── Progress as the row's own fill ───────────────────────────────────────
   The row fills left-to-right to --pct with a hard colour stop, replacing the
   thin bar that used to sit under the text. Two things matter for legibility:
   the fill is dark enough (dark mode) / light enough (light mode) that ONE text
   colour reads on both the filled and unfilled halves, and every piece of text
   is forced to that colour — otherwise the dimmed meta line vanished into the
   fill. */
.task-row.has-progress {
    background: linear-gradient(to right,
        var(--row-fill) 0 var(--pct, 0%),
        var(--bg-panel) var(--pct, 0%) 100%);
}
.task-row.has-progress:hover {
    background: linear-gradient(to right,
        var(--row-fill-hover) 0 var(--pct, 0%),
        var(--bg-elev) var(--pct, 0%) 100%);
}
.task-row.has-progress,
.task-row.has-progress .task-row__title,
.task-row.has-progress .task-row__meta,
.task-row.has-progress .task-row__meta .dim,
.task-row.has-progress .task-row__meta .sep,
.task-row.has-progress .task-row__meta .truncate,
.task-row.has-progress .task-row__id,
.task-row.has-progress .task-row__date,
.task-row.has-progress .task-row__pct { color: var(--row-fill-text); }
/* Status chips keep their own colour meaning, but need a backing that works
   over both halves of the row. */
.task-row.has-progress .chip {
    background: var(--row-chip-bg);
    border-color: currentColor;
}
.task-row__pct { font-size: var(--fz-12); color: var(--text-mute); }

/* "Client - Task" titles: the client leads, slightly lighter than the task name
   so the eye still lands on the task itself. */
.task-name__client { font-weight: 500; }
.task-name__sep { opacity: 0.55; }
/* A completed row is crossed out and dimmed; a fill would fight that. */
.task-row--completed.has-progress { background: var(--bg-panel); }
.task-row--completed.has-progress .task-row__title { color: var(--text-dim); }
.task-row--completed.has-progress .task-row__meta,
.task-row--completed.has-progress .task-row__pct { color: var(--text-mute); }

/* Completed tasks: no special fill/box — just the normal row with a crossed-out,
   dimmed title so it reads as done. Reopening the task (status ≠ completed)
   drops this class server-side, restoring the normal look. */
.task-row--completed .task-row__title { color: var(--text-dim); text-decoration: line-through; text-decoration-thickness: 3px; }

/* Urgent task rows: boxed off with a pulsing red border, same treatment as
   completed rows but to grab attention rather than fade out. */
.task-row.is-urgent {
    border: 1px solid var(--priority-urgent);
    border-radius: var(--radius-sm);
    margin: var(--sp-2);
}
.task-row--completed .task-row__fresh { background: var(--status-completed) !important; }

/* The stripe down the left of a row carries the SAME colour as the date on the
   right, both driven by [data-date-state] on the row. It used to be painted
   inline from a days-to-due gradient, which meant an overdue row could show a
   red date beside an orange stripe. */
.task-row__fresh {
    align-self: stretch;
    flex: 0 0 4px;
    border-radius: 4px;
    /* currentColor, not --text: on a filled row the text (and so the neutral
       date) is repainted to --row-fill-text, and the stripe has to follow it. */
    background: currentColor;
}
.task-row[data-date-state="overdue"] .task-row__fresh { background: var(--date-overdue); }
.task-row[data-date-state="today"]   .task-row__fresh { background: var(--date-today); }
.task-row[data-date-state="none"]    .task-row__fresh { background: var(--text-mute); }
.task-row__project-icon {
    flex-shrink: 0;
    font-size: 22px;
    line-height: 1;
    display: flex;
    align-items: center;
}
.task-row__main { flex: 1; min-width: 0; }
.task-row__title {
    font-size: var(--fz-14);
    font-weight: 600;
    margin: 0;
    color: var(--text);
}
.task-row__meta {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
    margin-top: 4px;
    font-size: var(--fz-12);
    color: var(--text-dim);
}
.task-row__meta .sep { color: var(--text-mute); }
.task-row__progress {
    margin-top: 6px;
}
/* "New activity" count (#28) — unread notifications for one task. Shared by the
   board cards and the dashboard/list rows, so it lives here rather than in a
   page stylesheet. */
.notif-count {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 19px;
    height: 19px;
    padding: 0 5px;
    border: 1px solid var(--notif-new);
    border-radius: 999px;
    background: color-mix(in srgb, var(--notif-new) 15%, transparent);
    color: var(--notif-new);
    font-size: 11px;
    font-weight: 700;
    line-height: 1;
}
/* On a row it sits vertically centred, a little clear of the date column, and a
   size up from the board version - a row has the space for it. The colour
   override beats the has-progress repaint of row text. */
.task-row > .notif-count,
.task-row.has-progress > .notif-count {
    align-self: center;
    margin-right: 0.9em;
    color: var(--notif-new);
    min-width: 24px;
    height: 24px;
    padding: 0 7px;
    font-size: 13px;
}

.task-row__side {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;
    flex-shrink: 0;
    min-width: 90px;
}
.task-row__star {
    background: none;
    border: none;
    color: var(--text-mute);
    cursor: pointer;
    padding: 0;
    font-size: 16px;
    line-height: 1;
}
.task-row__star.is-on { color: #fbbf24; }

/* Due date, given the star's old spot — dates matter more day-to-day than
   a favorite toggle, so it's bold and large enough to scan at a glance. */
.task-row__date {
    font-size: 20px;
    font-weight: 800;
    line-height: 1.1;
    color: var(--text);
    white-space: nowrap;
}
/* Overdue = red, due today = yellow. These have to outrank the has-progress
   rule that repaints every piece of row text, hence the .task-row prefix. */
.task-row .task-row__date.is-overdue,
.task-row.has-progress .task-row__date.is-overdue { color: var(--date-overdue); }
.task-row .task-row__date.is-due-today,
.task-row.has-progress .task-row__date.is-due-today { color: var(--date-today); }
.task-row__date--none {
    font-size: var(--fz-12);
    font-weight: 400;
    color: var(--text-mute);
}

.task-list {
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
    /* A hair of space between rows. The gap shows the page background, which
       reads as a thin divider — needed now that a row's progress fill runs the
       full height and rows would otherwise butt straight into each other. */
    display: flex;
    flex-direction: column;
    gap: 3px;
    background: var(--bg-base);
}
.task-list > .task-row { border-bottom: none; }
.task-list__section-label {
    margin: var(--sp-5) 0 var(--sp-2);
    font-size: var(--fz-14);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.04em;
}
.task-list--completed {
    background: transparent;
    border: none;
    overflow: visible;
}
.task-list__empty {
    padding: var(--sp-6);
    text-align: center;
    color: var(--text-dim);
}

/* ─── Modal ─── */
.modal-backdrop {
    position: fixed; inset: 0;
    background: rgba(0,0,0,0.6);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 100;
    padding: var(--sp-4);
}
.modal-backdrop.is-open { display: flex; }
.modal {
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    width: 100%;
    max-width: 520px;
    max-height: calc(100vh - var(--sp-7));
    overflow: auto;
    box-shadow: var(--shadow-2);
}
.modal--wide { max-width: 720px; }
.modal__header {
    display: flex; align-items: center;
    padding: var(--sp-4);
    border-bottom: 1px solid var(--border);
}
.modal__header h2 { margin: 0; font-size: var(--fz-16); }
.modal__close {
    margin-left: auto;
    background: none; border: none; cursor: pointer; color: var(--text-dim);
    font-size: 20px; line-height: 1; padding: 4px;
}
.modal__close:hover { color: var(--text); }
.modal__body { padding: var(--sp-4); }
.modal__footer {
    padding: var(--sp-3) var(--sp-4);
    border-top: 1px solid var(--border);
    display: flex; gap: var(--sp-2);
    justify-content: flex-end;
}

/* ─── Tables ─── */
.table {
    width: 100%;
    border-collapse: collapse;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    overflow: hidden;
}
.table th, .table td { padding: var(--sp-3); text-align: left; border-bottom: 1px solid var(--border); }
.table th { font-size: var(--fz-12); text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-dim); font-weight: 600; background: var(--bg-elev); }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: var(--bg-elev); }

/* ─── Dropdowns / selects (custom-styled) ─── */
select { -webkit-appearance: none; appearance: none; background-image:
    linear-gradient(45deg, transparent 50%, var(--text-dim) 50%),
    linear-gradient(135deg, var(--text-dim) 50%, transparent 50%);
    background-position: calc(100% - 14px) 50%, calc(100% - 9px) 50%;
    background-size: 5px 5px;
    background-repeat: no-repeat;
    padding-right: 28px;
}

/* ─── Icons ─── */
.icon { display: inline-block; vertical-align: middle; flex-shrink: 0; }
svg.icon { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }

/* ─── Activity log ─── */
.log {
    list-style: none; padding: 0; margin: 0;
    border-left: 2px solid var(--border);
    margin-left: var(--sp-2);
}
.log__entry {
    position: relative;
    padding: var(--sp-2) var(--sp-3);
}
.log__entry::before {
    content: '';
    position: absolute;
    left: -5px; top: 16px;
    width: 8px; height: 8px;
    background: var(--accent);
    border-radius: 50%;
}
.log__when { font-size: var(--fz-12); color: var(--text-mute); }
.log__what { color: var(--text); }
.log__who  { color: var(--accent-hi); font-weight: 600; }

/* ─── Mention chip (inside posted messages) ───
   Matches the pill the composer shows while you're writing. */
.mention {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    /* NOT baseline: an inline-flex box borrows the baseline of its first flex
       item, and the avatar's baseline depends on which kind it is — an
       initials <span> donates its text baseline, an <img> synthesizes one at
       its bottom edge. Mixing both in a sentence put the chips ~6px apart.
       `middle` is content-independent, so every chip lands on the same line. */
    vertical-align: middle;
    /* 5px left (not 3px): preset avatars carry a 2px box-shadow ring that is
       outside layout, and 3px let it touch the pill border. */
    padding: 1px 8px 1px 5px;
    margin: 0 1px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--status-completed, #3fb950) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--status-completed, #3fb950) 55%, transparent);
    white-space: nowrap;
}
.mention .avatar { width: 18px; height: 18px; font-size: 9px; }
.mention__name {
    color: var(--status-completed);
    font-weight: 600;
    font-size: .95em;
}
/* No photo (client portal): keep the pill tight around the name. */
.mention:not(:has(.avatar)) { padding: 1px 8px; }

/* ─── Avatars ─── */
.avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    flex-shrink: 0;
    color: #fff;
    font-weight: 700;
    text-shadow: 0 1px 1px rgba(0,0,0,.2);
    object-fit: cover;
    overflow: hidden;
}
.avatar--xs { width: 18px; height: 18px; font-size: 9px; }
.avatar--sm { width: 22px; height: 22px; font-size: 10px; }
.avatar--md { width: 36px; height: 36px; font-size: 13px; }
.avatar--lg { width: 64px; height: 64px; font-size: 26px; }
.avatar--xl { width: 50px; height: 50px; font-size: 20px; flex-shrink: 0; }
.avatar--square { border-radius: var(--radius-sm); }

.avatar-settings { display: flex; gap: var(--sp-4); align-items: flex-start; flex-wrap: wrap; }
.avatar-settings__pickers { flex: 1; min-width: 260px; }
.avatar-settings__divider { text-align: center; color: var(--text-mute); font-size: var(--fz-12); margin: 10px 0; }
.avatar-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.avatar-grid input[type="radio"] {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
}
.avatar-swatch {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 34px; height: 34px;
    border-radius: 50%;
    border: 2px solid var(--border);
    background: var(--bg-elev);
    font-size: 16px;
    cursor: pointer;
    overflow: hidden;
    transition: border-color 0.12s ease, transform 0.12s ease;
}
.avatar-swatch:hover { transform: scale(1.08); }
.avatar-swatch--img { width: 72px; height: 72px; }
.avatar-swatch--img:hover { transform: scale(1.12); z-index: 1; }
.avatar-swatch--img img { width: 100%; height: 100%; object-fit: cover; }
.avatar-grid input[type="radio"]:checked + .avatar-swatch {
    border-color: var(--accent-hi);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent-hi) 30%, transparent);
}
.avatar-swatch--color { border-color: transparent; }
.avatar-grid input[type="radio"]:checked + .avatar-swatch--color {
    border-color: var(--text);
}

/* ─── Messages (task message board) ─── */
.messages-card .card__title {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    margin-bottom: var(--sp-4);
}
.notes { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: var(--sp-3); }
.note {
    display: flex;
    gap: var(--sp-3);
    background: var(--msg-other-bg);
    border: none;
    border-radius: var(--radius);
    padding: var(--sp-3);
    transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.note:hover {
    transform: translateY(-1px);
    box-shadow: var(--shadow-1);
}
/* Text-message style: my messages hug the right, everyone else's the left. */
.note { max-width: 82%; align-self: flex-start; position: relative; }
/* When a message's ⋯ menu is open, lift the whole message above the composer
   (which comes later in the DOM) so the dropdown isn't covered. */
.note:has(.note__menu-btn[aria-expanded="true"]) { z-index: 40; }
/* Blue for mine, purple for theirs — see --msg-mine-bg in base.css for why this
   is no longer a mix of the author's own colour.
   .note--pinned and .note--pending both still win: equal specificity, declared
   later. That ordering is deliberate — a pinned message reads as pinned before
   it reads as yours, and one awaiting deletion goes deliberately colourless. */
.note--mine {
    align-self: flex-end;
    flex-direction: row-reverse;
    background: var(--msg-mine-bg);
}
.note--mine .note__head { flex-direction: row-reverse; }
.note--mine .note__reactions { justify-content: flex-end; }
.note__content { flex: 1; min-width: 0; }
.note__head { display: flex; align-items: baseline; gap: var(--sp-2); font-size: var(--fz-12); color: var(--text-dim); margin-bottom: 4px; flex-wrap: wrap; }
.note__author { color: var(--text); font-weight: 700; font-size: var(--fz-14); }
.note__body { color: var(--text); word-wrap: break-word; font-size: var(--fz-16); line-height: 1.55; }
/* Scoped to the body, because the bubble behind it is now tinted — see
   --msg-link. Not applied to .note__head or the reply chip, which use
   --text-dim and clear AA on both bubbles unchanged. */
.note__body a { color: var(--msg-link); }

/* ── An absorbed email inside a message thread (#37) ─────────────────────────
   A forwarded chain arrives as one very tall message and buries everything
   below it, so the sender's text is clamped to ~200px and expands on demand —
   one at a time (message-email-clamp.js). The "Email from … / Subject: …" meta
   line weeb prefixes to the body is lifted out above the clamp, so the visible
   window starts at the actual words rather than at our own header. */
.note-email__meta {
    color: var(--text-dim);
    font-size: var(--fz-13);
    margin-bottom: var(--sp-2);
}
.note-email {
    position: relative;
    max-height: 200px;
    overflow: hidden;
}
.note-email.is-expanded { max-height: none; }
/* Faded with a mask rather than a gradient to a background token, unlike
   .task-desc__body: a note's background is per-message — .note--mine mixes
   --note-color into --bg-elev — so any fixed colour would band visibly on your
   own messages, and an absorbed email IS yours if you were the one who linked
   it. A mask doesn't care what's behind it. Only applied once JS has confirmed
   the text actually overflows, so a short email gets no phantom fade. */
.note-email.is-clipped:not(.is-expanded) {
    -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
            mask-image: linear-gradient(to bottom, #000 calc(100% - 40px), transparent);
}
.note-email__toggle {
    margin-top: 6px;
    background: none;
    border: none;
    padding: 0;
    font: inherit;
    font-size: var(--fz-13);
    color: var(--accent);
    cursor: pointer;
}
.note-email__toggle:hover { text-decoration: underline; }

.note-composer { margin-top: var(--sp-4); }
/* …but not when it's a labelled form field (quick-add's Description): the gap
   belongs between the message list and the composer, not under a label. */
.field > .note-composer { margin-top: 0; }
/* Newest-first sort floats the composer above the thread (messages-sort.js), so
   the gap has to move with it — otherwise the Send/GIF/Voice row sits flush
   against the first message. */
.note-composer + .notes { margin-top: var(--sp-4); }
.note-composer textarea {
    border-color: var(--border-strong);
}
.note-composer textarea:focus {
    border-color: var(--accent-hi);
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-hi) 20%, transparent);
}
.note-composer__file {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 6px 12px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: var(--fz-12);
    color: var(--text-dim);
    cursor: pointer;
}
.note-composer__file:hover { border-color: var(--accent-hi); color: var(--text); }
.note-composer__file input[type="file"] {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; overflow: hidden;
    clip: rect(0,0,0,0);
    border: 0;
}

/* The composer's action row (attach, Teams toggle, mic dictation, voice-note
   record, GIF, Send) already wraps (.note-composer .row below) rather than
   overflowing, but on a phone it had no room for every button's full label —
   Attach, the Teams checkbox and the voice-note button all carried text,
   which meant several rows of buttons instead of the one tidy line there's
   width for once they're icon-only. */
@media (max-width: 768px) {
    .note-composer__file { padding: 6px 9px; }
    /* Clipped, not display:none — the label has to leave the layout without
       leaving the accessibility tree. The 📎 beside it is aria-hidden, so
       display:none here would strip the wrapped file input of its only
       accessible name and leave it announced as a bare "button". Same
       trick the file input itself uses just above. */
    .note-composer__file-label {
        position: absolute;
        width: 1px; height: 1px;
        padding: 0; overflow: hidden;
        clip: rect(0,0,0,0);
        border: 0;
    }
    /* voice.js keeps an aria-label on the button itself (and an element's
       aria-label beats its contents), so this one really can just go. */
    .voice-btn__label { display: none; }
    /* .teams-post-toggle__short's display:inline override, and the
       .voice-btn icon-square padding override, both live further down next
       to their base rule — same selector, so source order (not just
       specificity) decides which wins, and each has to come after its own
       base rule to win the cascade. */
}

/* ─── Subtasks ─── */
.subtask-progress {
    height: 4px;
    background: var(--border);
    border-radius: 2px;
    margin-bottom: var(--sp-3);
    overflow: hidden;
}
.subtask-progress__bar {
    height: 100%;
    background: var(--accent);
    border-radius: 2px;
    transition: width 0.4s ease;
}
.subtasks { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.subtask {
    display: flex; flex-direction: column; gap: 6px;
    padding: 10px 12px;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    transition: border-color 0.15s;
}
.subtask__row { display: flex; align-items: center; gap: var(--sp-3); }
.subtask:hover:not(.is-done) { border-color: var(--border-strong); }
.subtask__check {
    width: 18px; height: 18px;
    accent-color: var(--accent);
    flex-shrink: 0;
    cursor: pointer;
}
.subtask__title { flex: 1; font-size: var(--fz-14); cursor: text; border-radius: 4px; padding: 1px 3px; }
.subtask__title:hover { background: var(--bg-panel); }
.subtask__edit { flex: 1; font-size: var(--fz-14); padding: 1px 3px; }
/* Completed subtask — a clear, non-fading signifier (ticket #12). A finished
   subtask DROPS its bright color for a muted green "done" look (green tint +
   border + check) with a strikethrough — so completion is obvious even on
   color-coded subtasks. !important overrides the inline color those carry.
   Unchecking removes .is-done and restores the color. */
.subtask.is-done {
    background: color-mix(in srgb, var(--status-completed) 13%, var(--bg-elev)) !important;
    border-color: color-mix(in srgb, var(--status-completed) 45%, var(--border)) !important;
}
.subtask.is-done > .subtask__row .subtask__title { text-decoration: line-through; text-decoration-thickness: 3px; }
.subtask.is-done .subtask__title,
.subtask.is-done .subtask__meta { color: var(--text-mute) !important; }
.subtask.is-done .subtask__actions button { color: var(--text-dim) !important; }
.subtask.is-done .subtask__check { accent-color: var(--status-completed); }
.subtask__meta { font-size: var(--fz-12); color: var(--text-mute); white-space: nowrap; }

/* Colored subtask: background is the color; text + controls inherit the
   contrast ink set inline (white or dark) so everything stays legible. */
.subtask.is-colored .subtask__meta { color: inherit; }
.subtask.is-colored .subtask__title:hover { background: rgba(0, 0, 0, .12); }
.subtask.is-colored .subtask__actions button { color: inherit; }
.subtask.is-colored .subtask__actions button:hover { background: rgba(0, 0, 0, .14); color: inherit; }

/* Per-subtask controls (color / add-child / delete) */
.subtask__actions { margin-left: auto; flex-shrink: 0; display: inline-flex; gap: 2px; }
.subtask__actions button {
    background: none; border: none; color: var(--text-mute);
    cursor: pointer; font-size: 15px; line-height: 1; padding: 2px 6px; border-radius: 4px;
    opacity: 0; transition: opacity .15s, color .15s, background .15s;
}
.subtask:hover > .subtask__row .subtask__actions button,
.subtask__actions button:focus,
.subtask.is-colored > .subtask__row .subtask__actions button { opacity: 1; }
.subtask__actions button:hover { color: var(--text); background: var(--bg-panel); }
.subtask__del:hover { color: var(--priority-urgent, #e5484d); }

/* Color palette popover */
.subtask-palette {
    position: absolute; z-index: 60; display: flex; flex-wrap: wrap; gap: 6px;
    width: 150px; padding: 8px; background: var(--bg-panel);
    border: 1px solid var(--border); border-radius: var(--radius-sm); box-shadow: var(--shadow-md, 0 6px 20px rgba(0,0,0,.3));
}
.subtask-palette__swatch { width: 22px; height: 22px; border: 1px solid rgba(0,0,0,.2); border-radius: 50%; cursor: pointer; }
.subtask-palette__swatch:hover { transform: scale(1.12); }
.subtask-palette__none {
    width: 100%; margin-top: 2px; font-size: var(--fz-12); cursor: pointer;
    background: var(--bg-elev); border: 1px solid var(--border); border-radius: 4px; padding: 3px; color: var(--text-dim);
}

/* Nesting (subtasks of subtasks) */
.subtasks--nested { margin: 2px 0 0 22px; padding-left: 10px; border-left: 2px solid var(--border); }
.subtask__child-form { margin: 2px 0 2px 26px; }
.subtask__child-form input[name="title"] { width: 100%; }

/* A DONE subtask's row packs checkbox + title + the "by X · date" meta +
   the actions into one nowrap flex line. .subtask__meta is white-space:nowrap
   and .subtask__actions is flex-shrink:0, so on a phone-width row neither
   can give ground — .subtask__title gets squeezed down to its min-content
   (wrapping onto several short lines) and the row's content still doesn't
   fit, so .subtask__actions (last, non-shrinking) gets pushed clean outside
   the row's own box. That's invisible per-row (the buttons are opacity:0
   until hover) but it forces the row, the card, and — because this sits in
   the single-column mobile grid alongside the sidebar — the WHOLE page into
   horizontal scroll. Dropping the meta to its own full-width line frees up
   the room checkbox + title + actions need to fit on one line again. */
@media (max-width: 768px) {
    .subtask__row { flex-wrap: wrap; }
    .subtask__meta {
        flex-basis: 100%;
        white-space: normal;
        margin-left: 30px;   /* lines up under the title, past the checkbox */
    }
}

/* ─── Attachments ─── */
/* Attachments — a left-aligned row of uniform tiles. Every non-media file
   (image, PDF, sheet, generic) is the SAME footprint; video/audio are the
   deliberate exceptions and get a wider player. Each tile carries a download
   button pinned to its bottom-right corner. */
.attachments { display: flex; flex-wrap: wrap; gap: var(--sp-2); justify-content: flex-start; }
.dl-icon { width: 15px; height: 15px; display: block; }

.attach-tile {
    position: relative;
    display: inline-flex;
    /* Inline-level boxes align on their baseline, and a tile's baseline depends
       on what is inside it — so a thumbnail tile and an icon-card tile sat at
       different heights in the same row. Always align to the top instead. */
    vertical-align: top;
    flex-direction: column;
    width: 150px;
    border: 1px solid var(--border);
    border-radius: var(--radius);
    background: var(--bg-elev);
    overflow: hidden;
    transition: transform 0.12s ease, border-color 0.12s ease;
}
.attach-tile:hover { transform: translateY(-1px); border-color: var(--accent-hi); }

.attach-tile__preview {
    display: grid;
    place-items: center;
    width: 100%;
    height: 108px;
    background: var(--bg-input);
    overflow: hidden;
    text-decoration: none;
}
.attach-tile__preview img {
    width: 100%; height: 100%;
    object-fit: cover;
    display: block;
}
.attach-tile__icon { font-size: 34px; line-height: 1; }

/* PDF/sheet render target — attachment-preview.js swaps the icon for a real
   first-page canvas / rows table once pdf.js / SheetJS load. */
.attach-tile__doc {
    display: grid; place-items: center;
    width: 100%; height: 100%;
    overflow: hidden;
}
.attach-tile__doc.is-loaded { background: #fff; }
.attach-tile__doc canvas { display: block; max-width: 100%; max-height: 108px; }
.attach-preview__sheet-table {
    border-collapse: collapse;
    font-size: 9.5px;
    font-variant-numeric: tabular-nums;
    color: #111;
}
.attach-preview__sheet-table td {
    border: 1px solid #ddd;
    padding: 2px 4px;
    white-space: nowrap;
    max-width: 60px;
    overflow: hidden;
    text-overflow: ellipsis;
}

.attach-tile__footer {
    display: flex; flex-direction: column; gap: 1px;
    padding: 6px 40px 6px 9px;   /* right room so text clears the ⬇ button */
    min-width: 0;
    border-top: 1px solid color-mix(in srgb, var(--attach-color) 22%, var(--border));
}
.attach-tile__name {
    font-size: var(--fz-12); font-weight: 600;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    color: var(--text);
}
.attach-tile__meta {
    font-size: 10.5px; font-weight: 700; letter-spacing: 0.03em;
    color: var(--attach-color);
}

/* Download — pinned to the bottom-right corner of the tile. */
.attach-tile__dl {
    position: absolute; bottom: 6px; right: 6px;
    display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-dim);
    text-decoration: none;
    transition: background 0.12s ease, color 0.12s ease, border-color 0.12s ease;
}
.attach-tile__dl:hover { background: var(--border); color: var(--accent-hi); border-color: var(--accent-hi); text-decoration: none; }

/* Optional delete control — top-right corner, appears on hover. */
.attach-tile__delform { position: absolute; top: 6px; right: 6px; z-index: 2; margin: 0; }
.attach-tile__del {
    display: inline-flex; align-items: center; justify-content: center;
    width: 22px; height: 22px; padding: 0; line-height: 1;
    border: none; border-radius: 50%; cursor: pointer;
    background: rgba(10,15,25,.72); color: #fff; font-size: 15px;
    opacity: 0; transition: opacity .12s ease, background .12s ease;
}
.attach-tile:hover .attach-tile__del, .attach-tile__del:focus { opacity: 1; }
.attach-tile__del:hover { background: var(--priority-urgent, #e5484d); }

/* Media exceptions — wider, natural size, download in the corner. */
.attach-tile--media {
    width: auto;
    padding: 8px 8px 42px;
    background: var(--bg-elev);
}
.attach-tile__video {
    width: 260px; max-width: 100%; max-height: 160px;
    border-radius: var(--radius-sm);
    background: #000;
    display: block;
}
.attach-tile--audio {
    flex-direction: row; align-items: center; gap: 8px;
    width: 300px; max-width: 100%;
    padding: 10px 8px 42px 10px;
}
.attach-tile__audio-icon { font-size: 18px; flex-shrink: 0; }
.attach-tile__audio { width: 100%; height: 34px; }

/* ─── WYSIWYG editor ─── */
.editor {
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-input);
}
.editor__toolbar {
    display: flex; flex-wrap: wrap;
    padding: 6px;
    gap: 2px;
    border-bottom: 1px solid var(--border);
}
.editor__toolbar button {
    background: none; border: none;
    color: var(--text-dim);
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 12px;
}
.editor__toolbar button:hover { background: var(--bg-elev); color: var(--text); }
.editor__toolbar .sep { width: 1px; background: var(--border); margin: 0 4px; }
.editor__surface {
    min-height: 140px;
    padding: var(--sp-3);
    outline: none;
    color: var(--text);
}
.editor__surface:empty::before { content: attr(data-placeholder); color: var(--text-mute); }
.editor__surface h2, .editor__surface h3 { margin: 8px 0 4px; }
.editor__surface pre,
.kn-entry pre,
.note__body pre {
    background: var(--bg-base);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    padding: var(--sp-3);
    overflow-x: auto;
    font-family: var(--font-mono);
    font-size: 13px;
    line-height: 1.5;
    margin: var(--sp-2) 0;
}
.editor__surface code,
.kn-entry code,
.note__body code {
    font-family: var(--font-mono);
    font-size: 0.92em;
    background: var(--code-bg);
    padding: 1px 4px;
    border-radius: 3px;
}
.editor__surface pre code,
.kn-entry pre code,
.note__body pre code {
    background: none;
    padding: 0;
}

/* ─── Mention autocomplete ─── */
.mention-popup {
    position: absolute;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    box-shadow: var(--shadow-2);
    min-width: 200px;
    max-height: 200px;
    overflow-y: auto;
    z-index: 1100;   /* above modals (z 1000) — composers live inside modals */
    display: none;
}
.mention-popup.is-open { display: block; }
.mention-popup__item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 10px;
    cursor: pointer;
    font-size: var(--fz-14);
}
.mention-popup__item.is-active,
.mention-popup__item:hover { background: var(--bg-elev); }
.mention-popup__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mention-popup__handle { color: var(--text-dim); font-size: var(--fz-12); margin-left: auto; }

/* ─── Recovery codes ─── */
.recovery-codes__grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
    gap: var(--sp-2);
    margin-top: var(--sp-3);
}
.recovery-codes__code {
    display: block;
    padding: 8px 12px;
    background: var(--bg-base);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--fz-14);
    letter-spacing: 0.08em;
    text-align: center;
    color: var(--text);
}

/* ─── New note modal: dropzone + previews ─── */
.new-note__body-grid {
    display: grid;
    grid-template-columns: 1fr 220px;
    gap: var(--sp-4);
    margin-top: var(--sp-4);
}
@media (max-width: 720px) {
    .new-note__body-grid { grid-template-columns: 1fr; }
}
.dropzone {
    border: 2px dashed var(--border);
    border-radius: var(--radius);
    padding: var(--sp-4);
    text-align: center;
    cursor: pointer;
    color: var(--text-dim);
    font-size: var(--fz-12);
    transition: border-color 0.15s, background 0.15s;
    min-height: 100px;
    display: flex;
    align-items: center;
    justify-content: center;
}
.dropzone:hover, .dropzone.is-dragover {
    border-color: var(--accent);
    background: var(--bg-elev);
}
.dropzone__previews {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin-top: var(--sp-2);
}
.dropzone__chip {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 6px 8px;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    font-size: var(--fz-12);
}
.dropzone__chip.is-uploading { opacity: 0.6; }
.dropzone__chip-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropzone__chip-remove {
    background: none; border: none; color: var(--text-mute); cursor: pointer;
    font-size: 14px; padding: 0 2px; line-height: 1;
}
.dropzone__chip-remove:hover { color: var(--text); }

/* ─── Floating action buttons ─── */
/* Floating create menu — one + button that rolls out actions above it. */
.fab-group {
    position: fixed; right: var(--sp-5); bottom: var(--sp-5); z-index: 90;
    display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-3);
}
.fab-toggle {
    width: 56px; height: 56px; border-radius: 50%; border: none; cursor: grab;
    background: var(--accent); color: var(--accent-fg); display: grid; place-items: center;
    box-shadow: var(--shadow-2); transition: transform .25s ease, background .15s ease;
    touch-action: none;   /* let it be dragged on touch without scrolling the page */
}
.fab-toggle:active { cursor: grabbing; }
.fab-toggle:hover { background: var(--accent-hi); }
.fab-toggle__plus { width: 26px; height: 26px; fill: none; stroke: currentColor; stroke-width: 2.5; stroke-linecap: round; }
.fab-group.is-open .fab-toggle { transform: rotate(135deg); }   /* + morphs to × */

.fab-menu { display: flex; flex-direction: column; align-items: flex-end; gap: 10px; position: fixed; z-index: 91; }
.fab-item {
    display: inline-flex; align-items: center; gap: 10px; padding: 10px 16px;
    width: 190px; box-sizing: border-box; justify-content: flex-start;
    border: none; border-radius: 999px; cursor: pointer; font-size: var(--fz-14); font-weight: 600;
    color: #fff; box-shadow: var(--shadow-2); white-space: nowrap;
    transform: translateY(10px) scale(.95); opacity: 0; animation: fab-in .18s ease forwards;
}
.fab-item__ico { display: grid; place-items: center; width: 20px; height: 20px; flex: none; }
.fab-item__ico svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.fab-item--task { background: var(--accent); color: var(--accent-fg); }
.fab-item--note { background: #a855f7; }
.fab-item--feature { background: #0d9488; }   /* teal — distinct from task/note/ticket */
.fab-item--ticket { background: #e08a2b; color: #1a1205; }
/* Rose, not red: --priority-urgent is #ef4444, and an alarm-red button sitting
   next to it would read as "urgent thing" rather than "timed reminder".
   #be185d is 6.04:1 against the white label — clear of 1.4.3's 4.5:1. */
.fab-item--alert { background: #be185d; }
.fab-item:hover { filter: brightness(1.08); }
.fab-menu .fab-item:nth-child(1) { animation-delay: .02s; }
.fab-menu .fab-item:nth-child(2) { animation-delay: .06s; }
.fab-menu .fab-item:nth-child(3) { animation-delay: .10s; }
.fab-menu .fab-item:nth-child(4) { animation-delay: .14s; }
.fab-menu .fab-item:nth-child(5) { animation-delay: .18s; }
@keyframes fab-in { to { transform: translateY(0) scale(1); opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .fab-item { animation: none; opacity: 1; transform: none; } }

/* Alert recipient picker (modal-set-alert). A plain checkbox list — see the
   comment in that partial for why it isn't the assignee chip picker. */
.alert-people {
    display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 6px 12px; max-height: 190px; overflow-y: auto;
    padding: 10px 12px; border: 1px solid var(--border); border-radius: var(--radius);
    background: var(--bg-input);
}
.alert-people__opt {
    display: flex; align-items: center; gap: 8px; cursor: pointer;
    text-transform: none; letter-spacing: 0; margin: 0;
    font-size: var(--fz-14); font-weight: 500; color: var(--text);
}
.alert-people__opt input { width: auto; margin: 0; flex: none; }
/* The "everyone has to check it off" box (#189). Outside the scrolling
   .alert-people list but inside its field, so it can't scroll out of sight
   behind a long staff list while still reading as part of the same question. */
/* --fz-12, not --fz-13: there is no --fz-13 token, so the fallback was doing
   all the work and this label alone ignored the per-user Text size setting —
   at scale 6 the recipient labels above it grow 14px -> 20px on --fz-14 while
   a hard .82rem stays put. Only the defined tokens rescale (base.css). */
.alert-people__all { margin-top: 8px; font-size: var(--fz-12); color: var(--text-dim); }

/* Task ID badge (unique reference number) */
.task-row__id { color: var(--text-dim); font-variant-numeric: tabular-nums; font-weight: 600; }
.task-detail__id {
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
    font-weight: 700;
    font-size: 1rem;
    margin-right: 8px;
    align-self: center;
}

/* ── Message board: reactions, ⋯ menu, pin, pending-deletion ── */
.note__head { position: relative; }
.note__head .note-client-toggle { margin-left: 0; }
/* Quick-reply button — sits with the ⋯ menu at the message's top-right. */
.note__reply-btn {
    margin-left: auto; background: none; border: none; cursor: pointer;
    color: var(--text-dim); padding: 2px 4px; display: inline-flex; align-items: center;
    opacity: 0; transition: opacity .12s ease, color .12s ease;
}
.note__reply-btn svg { width: 15px; height: 15px; }
.note:hover .note__reply-btn, .note__reply-btn:focus { opacity: 1; }
.note__reply-btn:hover { color: var(--accent-hi); }
.note__reply-btn + .note__menu { margin-left: 4px; }   /* hug the reply button */

/* "↩ Replying to …" inside a reply's own bubble.
 *
 * Deliberately NOT an extension of .reply-chip below. That one is a composer
 * control — transient, full-width, with a dismiss button, and loud on purpose
 * because it describes what you are doing right now. This is permanent, sits
 * above a message, has to stay quieter than the words under it, and has to
 * survive .note--mine's row-reverse. Sharing them would mean every later change
 * to one needing an :not() on the other.
 *
 * The message list stays flat, so this element is the only thing carrying the
 * thread — it has to be legible and obviously clickable while still reading as
 * subordinate to the message itself. */
.note__reply-ref {
    display: flex; align-items: center; gap: 6px;
    margin: 0 0 6px; padding: 3px 8px; max-width: 100%;
    border-left: 2px solid var(--border-strong);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    background: color-mix(in srgb, var(--text-mute) 8%, transparent);
    font-size: var(--fz-12); line-height: 1.45;
    color: var(--text-dim); text-decoration: none; cursor: pointer;
}
.note__reply-ref:hover { border-left-color: var(--accent-hi, #4a86ff); color: var(--text); }
.note__reply-ref__icon { width: 12px; height: 12px; flex: none; }
.note__reply-ref__text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.note__reply-ref__who  { color: var(--text); font-weight: 600; }

/* The parent is not on this page — the client board renders only the most
   recent 100. message-jump.js sets this at click time, not render time,
   because a polled-in reply's parent may well be here. */
.note__reply-ref.is-orphan { cursor: default; opacity: .72; border-left-color: var(--border); }
.note__reply-ref.is-orphan:hover { border-left-color: var(--border); color: var(--text-dim); }

/* My own bubble is row-reverse and right-aligned, so the reference follows it.
 *
 * border-inline-start would be the tidy way to write this and it is WRONG here:
 * logical properties follow writing direction, not flex direction, so it would
 * not flip for .note--mine. The explicit left/right swap is the point. */
.note--mine .note__reply-ref {
    flex-direction: row-reverse; text-align: right;
    border-left: none; border-right: 2px solid var(--border-strong);
    border-radius: var(--radius-sm) 0 0 var(--radius-sm);
}
.note--mine .note__reply-ref:hover { border-right-color: var(--accent-hi, #4a86ff); }

/* Note on --note-color: it DOES inherit here, from the inline style on the
   <li>, and is deliberately unused. That is the colour of whoever wrote THIS
   message, so tinting a chip that says "Replying to Rachel" with Mark's colour
   would actively mislead. If the parent's colour is ever wanted, pass it as its
   own property rather than reaching for the inherited one. */

/* Teams channel link + "also post to Teams" toggle on the message composer */
.teams-link { display: flex; align-items: center; gap: 8px; margin: 8px 0; }
.teams-link__label { margin: 0; font-size: var(--fz-12); color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; }
.teams-link select { width: auto; }
.teams-post-toggle {
    display: inline-flex; align-items: center; gap: 6px; margin: 0 auto 0 0;
    font-size: var(--fz-12); color: var(--text-dim); text-transform: none; letter-spacing: 0; cursor: pointer;
}
.teams-post-toggle__short { display: none; }   /* the full label's stand-in on mobile */
@media (max-width: 768px) {
    /* Clipped rather than display:none, for the same reason as
       .note-composer__file-label: __short is decorative (aria-hidden), so
       hiding this outright would leave the checkbox with no accessible name.
       Clipped, it keeps announcing the full "Also post to Teams (Channel)"
       while only the short "Teams" is on screen. */
    .teams-post-toggle__label {
        position: absolute;
        width: 1px; height: 1px;
        padding: 0; overflow: hidden;
        clip: rect(0,0,0,0);
        border: 0;
    }
    .teams-post-toggle__short { display: inline; }
}

/* Nudge picker popover */
.nudge-pop {
    position: absolute; z-index: 1100; width: 220px; max-width: 90vw;
    display: flex; flex-direction: column; gap: 8px; padding: 10px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0,0,0,.28);
}
.nudge-pop__title { font-size: var(--fz-12); font-weight: 700; color: var(--text-dim); }
.nudge-pop__select { width: 100%; }

/* "Replying to …" chip above the composer */
.reply-chip {
    display: flex; align-items: center; gap: 8px;
    padding: 6px 10px; margin-bottom: 8px;
    background: var(--bg-elev); border-left: 3px solid var(--accent);
    border-radius: var(--radius-sm); font-size: var(--fz-12); color: var(--text-dim);
}
.reply-chip svg { width: 14px; height: 14px; flex: none; color: var(--accent); }
.reply-chip__text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.reply-chip__text strong { color: var(--text); font-weight: 600; }
.reply-chip__x { background: none; border: none; color: var(--text-mute); cursor: pointer; font-size: 16px; line-height: 1; padding: 0 2px; flex: none; }
.reply-chip__x:hover { color: var(--priority-urgent, #e5484d); }

/* Quoted original inside a posted message (quick-reply blockquote).
 *
 * Both selectors are load-bearing, for two different producers.
 * .msg-quote is what TextRenderer::blocks() emits for a plain-text body's
 * "> " lines. The bare element is what everything on the rich path produces:
 * HtmlSanitizer allows `blockquote` with no attributes at all, so a class set
 * on one is stripped before it is ever stored — and Quill's own blockquote
 * toolbar button emits a bare one too, which until now rendered as
 * indistinguishable plain text. */
.note__body .msg-quote,
.note__body blockquote {
    margin: 0 0 6px; padding: 2px 0 2px 10px;
    border-left: 3px solid var(--border-strong);
    color: var(--text-dim); font-size: var(--fz-13);
}

.note__menu { margin-left: auto; position: relative; }
.note__menu-btn {
    background: none; border: none; color: var(--text-mute); cursor: pointer;
    font-size: 18px; line-height: 1; padding: 0 6px; border-radius: 4px;
}
.note:hover .note__menu-btn { color: var(--text); }
.note__menu-btn:hover { background: var(--bg-panel); }
.note__menu-list {
    position: absolute; top: calc(100% + 4px); right: 0; z-index: 30; min-width: 210px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius);
    box-shadow: 0 8px 24px rgba(0,0,0,.28); padding: 6px; display: flex; flex-direction: column; gap: 2px;
}
.note__menu-list form { margin: 0; }
.note__menu-item {
    display: block; width: 100%; text-align: left; background: none; border: none; cursor: pointer;
    color: var(--text); font: inherit; font-size: var(--fz-14); padding: 8px 10px; border-radius: var(--radius-sm);
    text-decoration: none;
}
.note__menu-item:hover { background: var(--bg-elev); }
.note__menu-item--danger { color: var(--status-danger, #e05252); }

.note__pinned-tag { font-size: var(--fz-12); color: #e0a83d; font-weight: 600; }
.note--pinned { background: color-mix(in srgb, #e0a83d 12%, var(--bg-elev)); }

/* Reactions / boosts */
.note__reactions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: var(--sp-2); }

/* "Seen by" — quiet by design. It is a receipt, not part of the conversation. */
.note__seen { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; margin-top: 6px; }
.note__seen-label { font-size: var(--fz-12); color: var(--text-mute); margin-right: 2px; }
.note__seen-more { font-size: var(--fz-12); color: var(--text-mute); }
.note--mine .note__seen { justify-content: flex-end; }
.boost {
    display: inline-flex; align-items: center; gap: 4px;
    background: var(--bg-elev); border: 1px solid var(--border); border-radius: 999px;
    padding: 2px 9px; font-size: var(--fz-13, .82rem); color: var(--text); cursor: pointer; line-height: 1.4;
}
.boost:hover { border-color: var(--border-strong); }
.boost.is-mine { background: color-mix(in srgb, var(--accent-hi, #4a86ff) 22%, transparent); border-color: color-mix(in srgb, var(--accent-hi, #4a86ff) 45%, transparent); }
.boost__count { font-variant-numeric: tabular-nums; color: var(--text-dim); }
.boost.is-mine .boost__count { color: var(--text); }
.boost--add { color: var(--text-mute); opacity: 0; transition: opacity .15s; }
.note:hover .boost--add { opacity: 1; }
.boost--add__plus { font-weight: 700; margin-left: 1px; }
.boost-add { position: relative; display: inline-block; }
.boost-picker {
    position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 30;
    display: flex; gap: 2px; padding: 6px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: 999px; box-shadow: 0 8px 24px rgba(0,0,0,.28);
}
.boost-picker__btn { background: none; border: none; cursor: pointer; font-size: 20px; line-height: 1; padding: 4px 6px; border-radius: 8px; }
.boost-picker__btn:hover { background: var(--bg-elev); transform: scale(1.2); }

/* Pending deletion — tinted + compacted (no text fade — ADA) */
.note--pending { background: var(--bg-elev); border-radius: var(--radius-sm); }
.note--pending .note__content { min-width: 0; }
.note__pending {
    display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap;
    font-size: var(--fz-13, .82rem); color: var(--text-dim);
    background: color-mix(in srgb, var(--status-danger, #e05252) 8%, transparent);
    border: 1px dashed color-mix(in srgb, var(--status-danger, #e05252) 40%, transparent);
    border-radius: var(--radius-sm); padding: 6px 10px; margin-top: 4px;
}
.note__pending-actions { display: inline-flex; gap: 6px; margin-left: auto; }
.note__pending-actions form { margin: 0; }

/* Avatar + name cell (users list) */
.user-cell { display: inline-flex; align-items: center; gap: 8px; text-decoration: none; color: var(--text); }
.user-cell:hover span { text-decoration: underline; }

/* Generic tabs (used on the client page; see [data-tabs] handler in app.js) */
.tabs { display: flex; flex-wrap: wrap; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: var(--sp-4); }
.tabs__btn {
    background: none; border: none; border-bottom: 2px solid transparent;
    color: var(--text-dim); cursor: pointer; font-size: var(--fz-14); font-weight: 500;
    padding: 10px 14px; margin-bottom: -1px;
}
.tabs__btn:hover { color: var(--text); }
.tabs__btn.is-active { color: var(--text); border-bottom-color: var(--accent-hi, #4a86ff); }
.tabs__count {
    display: inline-block; min-width: 18px; padding: 0 5px; border-radius: 999px;
    background: var(--bg-elev); color: var(--text-dim); font-size: var(--fz-12); text-align: center;
}
.tabs__btn.is-active .tabs__count { background: var(--accent-hi, #4a86ff); color: #fff; }
.tabpanel { display: none; }
.tabpanel.is-active { display: block; }

/* ── Routing rules panel (matched keywords / addresses / domains) ───────── */
.email-rules { display: flex; flex-direction: column; gap: var(--sp-3); margin-bottom: var(--sp-4); }
.email-rules__head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--sp-3); }
.email-rules__head .btn { flex: none; white-space: nowrap; }

/* Collapsed = ~2 rows of chips; JS reveals the toggle only when it overflows. */
.email-rules__list {
    display: flex; flex-wrap: wrap; gap: var(--sp-2);
    overflow: hidden; transition: max-height .2s ease;
}
.email-rules__list.is-collapsed { max-height: 4.6rem; }

.rule-chip {
    display: inline-flex; align-items: center; gap: 6px;
    padding: 4px 6px 4px 8px; border: 1px solid var(--border); border-radius: 999px;
    background: var(--bg-elev); font-size: var(--fz-12); line-height: 1.4; max-width: 100%;
}
.rule-chip__type {
    font-size: 10px; text-transform: uppercase; letter-spacing: .04em; font-weight: 700;
    color: var(--text-dim); background: var(--bg); border: 1px solid var(--border);
    border-radius: 4px; padding: 1px 5px;
}
.rule-chip--keyword .rule-chip__type { color: var(--accent); border-color: var(--accent); }
.rule-chip__val { font-family: var(--font-mono, monospace); color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 16rem; }
.rule-chip__arrow { color: var(--text-dim); }
.rule-chip__client { font-weight: 600; color: var(--text); }
.rule-chip__rm { display: inline; margin: 0; }
.rule-chip__x {
    border: none; background: none; cursor: pointer; color: var(--text-dim);
    font-size: 15px; line-height: 1; padding: 0 2px; border-radius: 4px;
}
.rule-chip__x:hover { color: var(--danger, #d33); background: var(--bg); }

.email-rules__add { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: center; padding-top: var(--sp-2); border-top: 1px solid var(--border); }
.email-rules__add select, .email-rules__add input { font-size: var(--fz-13); }
.email-rules__add input[name="match_value"] { flex: 1 1 16rem; min-width: 12rem; }

/* Voice dictation / voice-note buttons (#17) */
.voice-btn {
    font-size: var(--fz-12); cursor: pointer; padding: 3px 8px; border-radius: 6px;
    border: 1px solid var(--border); background: var(--bg-elev); color: var(--text-dim);
    line-height: 1;
}
.voice-btn:hover { color: var(--text); border-color: var(--border-strong); }
.voice-btn .icon { width: 16px; height: 16px; display: block; }
.voice-btn--mic { margin: 6px 0 0; padding: 5px 7px; display: inline-flex; align-items: center; }
.voice-btn--mic.is-recording { color: #fff; background: var(--priority-urgent, #e5484d); border-color: var(--priority-urgent, #e5484d); }
.voice-btn.is-recording { color: #fff; background: var(--priority-urgent, #e5484d); border-color: var(--priority-urgent, #e5484d); }
.voice-note-status { font-size: var(--fz-12); align-self: center; }

/* GIF reactions + picker (#8) */
.boost--gif { padding: 2px 6px 2px 3px; }
.boost__gif { height: 22px; width: auto; border-radius: 4px; vertical-align: middle; display: inline-block; }
.boost-picker__emojis { display: flex; gap: 2px; }
.boost-picker__gif-toggle {
    margin-top: 6px; width: 100%; font-family: var(--mono, monospace); font-size: 11px; font-weight: 700;
    letter-spacing: .06em; cursor: pointer; border: 1px solid var(--border); background: var(--bg-elev);
    color: var(--text-dim); border-radius: 6px; padding: 4px;
}
.boost-picker__gif-toggle:hover { color: var(--text); }
.gif-picker { margin-top: 6px; width: 280px; max-width: 72vw; }
.gif-picker__search { width: 100%; font-size: var(--fz-13); margin-bottom: 6px; }
.gif-picker__results {
    display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
    max-height: 220px; overflow-y: auto;
}
.gif-picker__item { padding: 0; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; cursor: pointer; background: var(--bg-elev); aspect-ratio: 1; }
.gif-picker__item:hover { border-color: var(--accent-hi, #4a86ff); }
.gif-picker__item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gif-picker__msg { grid-column: 1 / -1; color: var(--text-mute); font-size: var(--fz-12); padding: 12px; text-align: center; }
.gif-picker__powered { font-size: 10px; color: var(--text-mute); text-align: right; margin-top: 4px; letter-spacing: .04em; }

/* Board view switcher — styled as a compact dropdown control rather than a
   page heading: border, hover/open background, and a chevron that flips when
   open. Sized to sit just above the board's column headers, not to dominate
   them (it was briefly page-title sized, which looked wrong). */
.board-switcher { position: relative; display: inline-block; }
.board-switcher__btn {
    cursor: pointer; color: inherit;
    /* Explicit, not `font: inherit` (buttons don't inherit font) and not
       .page-title's --fz-24 either: at title size this control dwarfed the
       board's own column headers. Sized as what it is — a control — one step
       above the column headings, not a page heading. */
    font-family: inherit;
    font-size: var(--fz-16);
    font-weight: 700;
    /* center, not baseline: an inline SVG's baseline is its bottom edge, so
       baseline alignment would push the chevron out of line with the text. */
    display: inline-flex; align-items: center; gap: 7px;
    padding: 6px 10px;
    background: var(--bg-panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    transition: background .12s ease, border-color .12s ease;
}
.board-switcher__btn:hover {
    background: var(--bg-elev);
    border-color: var(--text-dim);
}
.board-switcher__btn:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 2px;
}
/* "Board" sits above nothing — it's an inline eyebrow that names what the
   control changes, so the button isn't just an ambiguous word like "Date". */
.board-switcher__eyebrow {
    /* Token, not a hardcoded px, so it still grows with the per-user text-size
       setting (#103); weight + colour carry the hierarchy instead of size. */
    font-size: var(--fz-12);
    font-weight: 600;
    letter-spacing: .06em;
    text-transform: uppercase;
    color: var(--text-dim);
}
.board-switcher__label { font: inherit; }
.board-switcher__caret {
    width: 15px; height: 15px; flex: 0 0 15px;
    color: var(--text-dim);
    transition: transform .15s ease, color .12s ease;
}
.board-switcher__btn:hover .board-switcher__caret { color: var(--text); }
.board-switcher__btn[aria-expanded="true"] {
    background: var(--bg-elev);
    border-color: var(--accent);
}
.board-switcher__btn[aria-expanded="true"] .board-switcher__caret {
    transform: rotate(180deg);
    color: var(--accent);
}
@media (prefers-reduced-motion: reduce) {
    .board-switcher__btn, .board-switcher__caret { transition: none; }
}
.board-switcher__menu {
    position: absolute; top: calc(100% + 6px); left: 0; z-index: 40;
    min-width: 180px; padding: 6px;
    background: var(--bg-panel); border: 1px solid var(--border);
    border-radius: var(--radius); box-shadow: 0 8px 24px rgba(0,0,0,.25);
    display: flex; flex-direction: column; gap: 2px;
}
.board-switcher__item {
    display: flex; align-items: center; justify-content: space-between;
    padding: 8px 12px; border-radius: var(--radius-sm);
    text-decoration: none; color: var(--text); font-size: var(--fz-14); font-weight: 500;
}
.board-switcher__item:hover { background: var(--bg-elev); }
.board-switcher__item.is-active { color: var(--accent-hi, #4a86ff); }
.board-switcher__check { color: var(--accent-hi, #4a86ff); }

/* New-task template picker (searchable combobox) */
.tpl-picker { position: relative; }
.tpl-picker__menu {
    position: absolute; z-index: 50; left: 0; right: 0; top: 100%; margin-top: 4px;
    max-height: 240px; overflow-y: auto; padding: 4px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0,0,0,.25); display: flex; flex-direction: column; gap: 2px;
}
.tpl-picker__opt {
    text-align: left; background: none; border: none; color: var(--text); cursor: pointer;
    padding: 8px 10px; border-radius: var(--radius-sm); font-size: var(--fz-14);
}
.tpl-picker__opt:hover { background: var(--bg-elev); }
.tpl-picker__empty { padding: 8px 10px; color: var(--text-mute); font-size: var(--fz-13); }

/* Edited-message marker + inline editor + revision history */
.note__edited {
    background: none; border: none; padding: 0; cursor: pointer;
    font-size: var(--fz-12); font-style: italic; color: var(--text-mute);
    text-decoration: underline dotted;
}
.note__edited:hover { color: var(--text-dim); }
/* While editing, let the bubble span the full width: message bubbles size to
   their content, which otherwise leaves a short message with a tiny editor
   that can't be widened. */
.note:has(.note-edit) { max-width: 100%; width: 100%; }
.note:has(.note-edit) .note__content { width: 100%; }
.note-edit { margin-top: 4px; width: 100%; }
.note-edit__area {
    width: 100%; resize: vertical; min-height: 120px;
    font: inherit; font-size: var(--fz-14); line-height: 1.5;
}
.note-edit__actions { display: flex; gap: 6px; margin-top: 6px; }
.rev { padding: 8px 0; border-top: 1px solid var(--border); }
.rev__meta { font-size: var(--fz-12); color: var(--text-mute); margin-bottom: 3px; }
.rev__body { white-space: pre-wrap; }

/* Report-an-issue: the auto-captured page screenshot preview. Constrain it to
   the modal width so a large capture can't push content off to the right. */
.report-shot {
    max-width: 100%;
    overflow: hidden;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-input);
}
.report-shot img { display: block; max-width: 100%; height: auto; }

/* "X is typing…" live indicator above the composer */
.typing-indicator {
    display: flex; align-items: center; gap: 8px;
    padding: 4px 2px 8px; font-size: var(--fz-12); color: var(--text-dim); font-style: italic;
}
.typing-indicator__dots { display: inline-flex; gap: 3px; }
.typing-indicator__dots i {
    width: 5px; height: 5px; border-radius: 50%; background: var(--text-mute);
    animation: typing-bounce 1.2s infinite ease-in-out;
}
.typing-indicator__dots i:nth-child(2) { animation-delay: .15s; }
.typing-indicator__dots i:nth-child(3) { animation-delay: .30s; }
@keyframes typing-bounce { 0%, 60%, 100% { opacity: .3; transform: translateY(0); } 30% { opacity: 1; transform: translateY(-3px); } }
@media (prefers-reduced-motion: reduce) { .typing-indicator__dots i { animation: none; } }

/* Message-board sort — a compact icon toggle in the card's top-right corner. */
.msg-sort-btn {
    position: absolute; top: var(--sp-3); right: var(--sp-3); z-index: 2;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    height: 30px; padding: 0 10px;
    background: var(--bg-elev); border: 1px solid var(--border); border-radius: var(--radius-sm);
    color: var(--text-dim); cursor: pointer;
    font-size: var(--fz-12); font-weight: 600;
    transition: color .12s ease, border-color .12s ease, background .12s ease;
}
.msg-sort-btn:hover { color: var(--text); border-color: var(--border-strong); }
.msg-sort-btn.is-newest { color: var(--accent); border-color: var(--accent); }
.msg-sort-btn__label { line-height: 1; }
.msg-sort__icon { width: 15px; height: 15px; }

/* Quill rich-text editor — knowledge notes AND the message composer.

   READ THIS BEFORE CHANGING A SELECTOR HERE.

   Both editors call `new Quill(el)` where el ALREADY carries .kn-quill /
   .rich-quill (quill-init.js mounts on the markup div; rich-composer.js builds
   the host then mounts on it). Quill turns that element into .ql-container and
   inserts the toolbar as its PREVIOUS SIBLING:

       <div class="ql-toolbar ql-snow"> … </div>
       <div class="rich-quill ql-container ql-snow"><div class="ql-editor"> …

   So `.rich-quill .ql-toolbar` is a descendant selector with no descendant to
   match. Every toolbar rule written that way was dead from the day it was
   written, which is why the icons sat at Quill's hardcoded #444 — a near-black
   on a near-black toolbar — through two separate attempts to change their
   colour. Neither attempt was wrong about the colour; neither selector ever
   applied. Verified by mounting the real script and reading
   getComputedStyle(.ql-stroke).stroke, which returned rgb(68,68,68).

   Hence: the toolbar is addressed DIRECTLY (.ql-toolbar.ql-snow — the app has
   no other Quill instances), and the container is the .kn-quill/.rich-quill
   element ITSELF, so it takes a COMPOUND selector, never a descendant one.
   .ql-editor is a genuine child of the container, so those rules were always
   fine and are left alone. */
.kn-quill, .rich-quill { background: var(--bg-input); border-radius: var(--radius-sm); }
.ql-toolbar.ql-snow,
.kn-quill.ql-container, .rich-quill.ql-container { border-color: var(--border); }
.ql-toolbar.ql-snow { border-top-left-radius: var(--radius-sm); border-top-right-radius: var(--radius-sm); }
.kn-quill.ql-container, .rich-quill.ql-container { border-bottom-left-radius: var(--radius-sm); border-bottom-right-radius: var(--radius-sm); font-size: var(--fz-14); }
.kn-quill .ql-editor, .rich-quill .ql-editor { min-height: 180px; color: var(--text); }
.kn-quill .ql-editor.ql-blank::before, .rich-quill .ql-editor.ql-blank::before { color: var(--text-mute); font-style: normal; }
.kn-quill .ql-editor a, .rich-quill .ql-editor a { color: var(--accent-hi); }

/* Icon colour. Quill hardcodes the strokes #444, which is invisible on a dark
   toolbar, so these must win — and quill.snow.css is injected into <head> at
   RUNTIME by quill-loader.js, i.e. AFTER this file in document order, so an
   equal-specificity rule here would LOSE the tie. Each selector below is
   therefore deliberately one step more specific than the vendor rule it beats.

   --text, not --text-dim: the dimmed value is 8.2:1 on the dark toolbar, well
   past WCAG 1.4.11's 3:1, and still reads grey — a contrast ratio describes
   solid areas, and these are 1–2px SVG strokes, which read far weaker than
   text at the same ratio. So both levers: full-strength colour and a little
   more stroke weight. Driving it off --text also serves both themes from one
   rule: near-white on the dark toolbar, near-black on the light one. */
.ql-toolbar.ql-snow .ql-stroke { stroke: var(--text); stroke-width: 2.4; }
.ql-toolbar.ql-snow .ql-stroke-miter { stroke: var(--text); stroke-width: 2.4; }
.ql-toolbar.ql-snow .ql-fill { fill: var(--text); }
.ql-toolbar.ql-snow .ql-picker { color: var(--text); }
/* Vendor's hover/active rules carry :not(.ql-active), which buys them an extra
   class of specificity; the doubled .ql-snow matches it and the later-loading
   tie-break, without reaching for !important. */
.ql-snow.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-snow.ql-toolbar button.ql-active .ql-stroke,
.ql-snow.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
.ql-snow.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter { stroke: var(--accent-hi); }
.ql-snow.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow.ql-snow.ql-toolbar button.ql-active .ql-fill { fill: var(--accent-hi); }
.ql-snow.ql-snow.ql-toolbar .ql-picker-label:hover,
.ql-snow.ql-snow.ql-toolbar .ql-picker-label.ql-active { color: var(--accent-hi); }
/* Dropdown (header sizes) panel — lives inside the toolbar, not the container. */
.ql-toolbar.ql-snow .ql-picker-options { background: var(--bg-panel); border-color: var(--border) !important; }
.ql-toolbar.ql-snow .ql-picker-options .ql-picker-item { color: var(--text); }

/* The message composer shares every rule above: one editor theme, not two that
   drift. Only the height differs — a message box should not open at the height
   of a knowledge article. */
.rich-quill .ql-editor { min-height: 96px; }

/* Drag-to-reorder overlay (#42).
   A sibling of .ql-editor inside the container, NOT inside the editor: markup
   in there would be submitted as part of the body. .ql-container is already
   position:relative from Quill's own CSS, so this needs no positioning context
   of its own. pointer-events:none on the layer so it never steals a click from
   the text; the grip opts back in. */
.rc-reorder { position: absolute; inset: 0; pointer-events: none; z-index: 2; }
.rc-reorder__grip {
    position: absolute; left: 2px; width: 14px; display: none;
    align-items: center; justify-content: center;
    pointer-events: auto; cursor: grab;
    color: var(--text-mute); border-radius: 3px;
    /* Prevents a touch-drag from scrolling the page instead of dragging. */
    touch-action: none;
}
/* Two dots, the same grip vocabulary as .bote-col__grip. */
.rc-reorder__grip::before { content: '⠿'; font-size: 12px; line-height: 1; }
.rc-reorder.is-on .rc-reorder__grip { display: flex; }
.rc-reorder__grip:hover { color: var(--text); background: var(--bg-hover, rgba(127,127,127,.14)); }
.rc-reorder.is-dragging .rc-reorder__grip { cursor: grabbing; }
.rc-reorder__mark {
    position: absolute; left: 16px; right: 6px; height: 2px; display: none;
    background: var(--accent-hi); border-radius: 2px;
}
.rc-reorder__mark.is-on { display: block; }
/* The editor needs room for the gutter the grip sits in, or the grip overlaps
   the first character of every line. */
.rich-quill .ql-editor { padding-left: 30px; }

/* Touch. A 14px-wide grip is not a touch target, and there is no hover to
   reveal it either — on a finger the caret does that (see rich-composer.js),
   so it has to be big enough to hit deliberately once it appears. */
@media (pointer: coarse) {
    .rc-reorder__grip { width: 26px; min-height: 26px; left: 1px; }
    .rc-reorder__grip::before { font-size: 15px; }
    .rich-quill .ql-editor { padding-left: 36px; }
    .rc-reorder__mark { left: 30px; height: 3px; }
}
.rich-quill.ql-container { font-size: var(--fz-14); }
/* The real <textarea> stays in the DOM as the form field and the no-JS
   fallback; it is only hidden once an editor is actually mounted in front of it. */
.rich-composer__source { display: none; }

/* Emoji picker. Anchored to the composer's button row, which is position:
   relative already for the GIF controls. */
/* Global search scope selector — a filled chip at the trailing end of the
   search pill. It was previously transparent with --text-dim text and a hairline
   border-left, which read as a floating word rather than a control: Rob's
   screenshot came with "I can't tell what that is next to the search bar".
   A scope that silently narrows every search must announce that it is a control
   and that it is set.

   --accent is declared once in :root and deliberately NOT overridden for
   [data-theme="light"], so one rule gives the same weeb blue in both themes,
   with --accent-fg (#fff in both) on top. No per-theme branch needed.

   Sizes come from the --fz-* tokens, never literal px, so the chip keeps
   scaling with the [data-text-scale] accessibility setting. */
.topbar__search-scope {
    flex: 0 0 auto;
    appearance: none; -webkit-appearance: none;
    margin-left: 8px;
    padding: 5px 29px 5px 11px;          /* right side leaves room for the chevron */
    /* Matches the search input it sits beside, which takes the global
       input rule's var(--radius-sm). NOT a 999px pill: the two controls read as
       one unit only if their corners agree. */
    border-radius: var(--radius-sm);
    /* The fill had to darken to --accent-strong to carry white text at 4.5:1,
       but #1d4ed8 is only 2.60:1 against the dark topbar — so in dark mode the
       chip's own edge failed 1.4.11's 3:1 and the control had no findable
       boundary. --accent is the one blue that clears 3:1 against BOTH surfaces
       (4.74 dark / 3.68 light), so the brand colour that was too light to sit
       behind text is exactly right as the rim in front of it. */
    border: 1px solid var(--accent);
    background-color: var(--accent-strong);
    color: var(--accent-fg);
    font-size: var(--fz-14); font-weight: 600; line-height: 1.2;
    cursor: pointer; max-width: 150px;
    /* Own chevron, because appearance:none removes the native one and the
       platform arrow renders near-black on the blue chip where it survives. */
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
    background-repeat: no-repeat;
    background-position: right 10px center;
    background-size: 12px 12px;
}
.topbar__search-scope:hover { background-color: var(--accent-dim); }
/* The app-wide focus ring is --accent-hi, which is 2.54:1 on a white surface —
   under 1.4.11's 3:1. --text is theme-aware and clears it in both (16:1 / 18:1).
   Scoped to this chip; the global rule in base.css is a separate question. */
.topbar__search-scope:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
/* The open dropdown is drawn by the OS and does NOT inherit the chip's colours.
   Without this it is white-on-white in light mode — the list would vanish. */
.topbar__search-scope option {
    background: var(--bg-panel); color: var(--text); font-weight: 400;
}
@media (max-width: 640px) { .topbar__search-scope { max-width: 120px; } }

.emoji-wrap { position: relative; display: inline-flex; }
.emoji-trigger {
    background: none; border: 1px solid var(--border); border-radius: var(--radius-sm);
    color: var(--text-dim); cursor: pointer; font-size: 15px; line-height: 1;
    padding: 5px 8px;
}
.emoji-trigger:hover { border-color: var(--accent-hi); background: var(--bg-input); }
.emoji-panel {
    display: none; position: absolute; bottom: calc(100% + 6px); left: 0; z-index: 60;
    width: 300px; max-height: 280px; overflow-y: auto; padding: 8px;
    background: var(--bg-panel); border: 1px solid var(--border);
    border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(0,0,0,.35);
}
.emoji-panel.is-open { display: block; }
.emoji-panel__search {
    width: 100%; margin-bottom: 8px; padding: 6px 8px;
    background: var(--bg-input); color: var(--text);
    border: 1px solid var(--border); border-radius: var(--radius-sm);
    font-size: var(--fz-13);
}
.emoji-panel__label {
    font-size: var(--fz-11); text-transform: uppercase; letter-spacing: .06em;
    color: var(--text-mute); margin: 8px 0 4px;
}
.emoji-panel__row { display: flex; flex-wrap: wrap; gap: 2px; }
.emoji-panel__btn {
    background: none; border: 0; border-radius: 4px; cursor: pointer;
    font-size: 19px; line-height: 1; padding: 4px;
}
.emoji-panel__btn:hover { background: var(--bg-input); }
.emoji-panel__empty { color: var(--text-mute); font-size: var(--fz-13); padding: 8px 2px; }
/* Mention pills inside the editor. The .mention-pill rules further down already
   style the avatar and name — these only make it behave as one atomic token in
   an editable surface: it must not be split by a caret, and it must sit with
   the text rather than stretching the line. */
.rich-quill .ql-editor .mention-pill {
    display: inline-flex;
    align-items: center;
    vertical-align: middle;
    user-select: none;
    white-space: nowrap;
}
.rich-quill .ql-editor .mention-pill * { pointer-events: none; }


/* Info dot — a small "i" that reveals help text in a popover on click, so
   hints don't take up permanent screen space (View::info + info.js). */
.info-dot {
    display: inline-flex; align-items: center; justify-content: center;
    width: 16px; height: 16px; padding: 0; margin-left: 5px;
    vertical-align: middle; flex-shrink: 0;
    border: 1px solid var(--border); border-radius: 50%;
    background: var(--bg-elev); color: var(--text-dim);
    font-size: 10px; font-weight: 700; font-style: italic; line-height: 1;
    font-family: Georgia, "Times New Roman", serif;
    cursor: pointer;
    transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.info-dot:hover, .info-dot[aria-expanded="true"] {
    background: var(--accent); color: #fff; border-color: var(--accent);
}
.info-pop {
    position: absolute; z-index: 1100; display: none;   /* above modals (z 1000) */
    max-width: 280px; padding: 8px 11px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0,0,0,.28);
    color: var(--text); font-size: var(--fz-13); line-height: 1.45;
}
.info-pop.is-open { display: block; }

/* Instant hover tooltip (tooltip.js). Same visual language as .info-pop above
   — they are the same idea at two speeds: .info-pop is click-to-reveal help
   text, .tip is hover-to-reveal text that is on screen but cut off.

   Deliberately NOT a ::after on the host. .attach-tile sets overflow:hidden to
   clip its thumbnail to the rounded corners, so a pseudo-element would be
   clipped to the 150px tile — the width that was too narrow to begin with.

   word-break, because filenames are the main thing this shows and they have no
   spaces to wrap at: "Epika-Fleet-Services-LP-System-final-v3-APPROVED.pdf"
   would otherwise force the box past max-width and off the screen edge. */
.tip {
    position: absolute; z-index: 1100; display: none;   /* above modals (z 1000) */
    max-width: 320px; padding: 6px 10px;
    background: var(--bg-panel); border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0,0,0,.28);
    color: var(--text); font-size: var(--fz-12); line-height: 1.4;
    overflow-wrap: anywhere; word-break: break-word;
    pointer-events: none;   /* never let the tooltip eat the hover that spawned it */
}
.tip.is-open { display: block; }

/* "Assign to" chip picker (quick-add modal) */
.assignee-picker { position: relative; }
.assignee-picker__control {
    display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
    min-height: 40px; padding: 5px 8px;
    background: var(--bg-input); border: 1px solid var(--border); border-radius: var(--radius-sm);
    cursor: text;
}
.assignee-picker.is-invalid .assignee-picker__control { border-color: var(--priority-urgent, #e5484d); }
.assignee-picker__chips { display: contents; }
.assignee-chip { display: inline-flex; align-items: center; gap: 4px; }
.assignee-chip--team { border-style: dashed; }
.assignee-chip__x {
    background: none; border: none; color: inherit; cursor: pointer;
    font-size: 13px; line-height: 1; padding: 0 0 0 2px;
}
.assignee-picker__input {
    flex: 1; min-width: 120px; border: none; background: none; outline: none;
    color: var(--text); font-size: var(--fz-14); padding: 2px;
}
.assignee-picker__menu {
    position: absolute; z-index: 50; left: 0; right: 0; top: 100%; margin-top: 4px;
    max-height: 260px; overflow-y: auto; padding: 4px;
    background: var(--bg-panel); border: 1px solid var(--border); border-radius: var(--radius-sm);
    box-shadow: 0 8px 24px rgba(0,0,0,.25); display: flex; flex-direction: column; gap: 2px;
}
.assignee-picker__group {
    padding: 6px 10px 2px; font-size: 10.5px; font-weight: 700; letter-spacing: .05em;
    text-transform: uppercase; color: var(--text-mute);
}
.assignee-picker__opt {
    text-align: left; background: none; border: none; color: var(--text); cursor: pointer;
    padding: 8px 10px; border-radius: var(--radius-sm); font-size: var(--fz-14);
    display: flex; align-items: center; justify-content: space-between; gap: 8px;
}
.assignee-picker__opt:hover { background: var(--bg-elev); }
.assignee-picker__kind {
    font-size: 10px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase;
    color: var(--text-mute);
}
.assignee-picker__empty { padding: 8px 10px; color: var(--text-mute); font-size: var(--fz-13); }

/* Composer GIF picker (attach a GIF to your message) */
.note-composer { position: relative; }
.gif-compose__panel {
    position: absolute; right: 0; bottom: 46px; z-index: 55; width: 300px; max-width: 90vw;
    padding: 8px; background: var(--bg-panel); border: 1px solid var(--border);
    border-radius: var(--radius-sm); box-shadow: 0 8px 24px rgba(0,0,0,.28);
}
.gif-compose-btn { align-self: center; }

/* Composer GIF preview strip — picked GIFs appear here before sending */
.gif-compose__previews { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.gif-compose__preview { position: relative; width: 120px; border-radius: var(--radius-sm); overflow: hidden; border: 1px solid var(--border); }
.gif-compose__preview img { display: block; width: 100%; height: auto; }
.gif-compose__preview.is-loading { opacity: .55; }
.gif-compose__remove {
    position: absolute; top: 3px; right: 3px; width: 22px; height: 22px; border-radius: 50%;
    border: none; cursor: pointer; background: rgba(0,0,0,.6); color: #fff; font-size: 15px; line-height: 1;
}
.gif-compose__remove:hover { background: rgba(0,0,0,.85); }

/* Inline GIF in a posted message (stored as gif_url, not an attachment) */
.note__gif { display: block; max-width: 260px; width: 100%; height: auto; border-radius: var(--radius-sm); margin-top: var(--sp-2); }
.note--mine .note__gif { margin-left: auto; }

/* Images pasted into a message body (TextRenderer::decorateBodyImages).
 *
 * The max-width is not cosmetic: nothing else constrains an <img> inside a
 * message, so a 3840px phone screenshot renders at its natural width and blows
 * the column — and with it the whole message list — wide open. Applied to every
 * surface a body is rendered on, not just .note__body, because the ticket
 * detail, task description, chat and the client portal all echo the same HTML
 * through different wrappers. */
.body-img { margin: var(--sp-2) 0; display: block; max-width: 100%; }
.body-img img,
.note__body img,
.chat-msg__text img,
.task-desc__body img,
.ticket-detail__body img,
.kn-entry__body img {
    display: block; max-width: 100%; height: auto;
    border-radius: var(--radius-sm); cursor: zoom-in;
}
.body-img__dl {
    display: inline-block; margin-top: var(--sp-1);
    font-size: var(--fz-12); color: var(--text-dim); text-decoration: none;
}
.body-img__dl:hover { color: var(--text); text-decoration: underline; }

/* Paste-time upload feedback. The original bug was an image disappearing with
 * no error anywhere, so a rejection has to be visible right under the composer
 * while the user is still looking at what they pasted. */
.rich-composer__upload-note {
    margin-top: var(--sp-1); font-size: var(--fz-12); color: var(--text-dim);
}
/* --date-overdue rather than --priority-urgent: it is the one red defined for
 * both the dark and light palettes, so this stays legible in either. */
.rich-composer__upload-note.is-error { color: var(--date-overdue); font-weight: 600; }

/* Composer action bar order + sizing: [Attach ....... Voice · GIF · Mic · Send] */
.note-composer .row { flex-wrap: wrap; }
.note-composer .row .note-composer__file { order: 0; margin-right: auto; }
.note-composer .row .voice-btn--rec  { order: 1; }
.note-composer .row .gif-compose-btn { order: 2; }
.note-composer .row .voice-btn--mic  { order: 3; }
/* Send/Post is the composer's only primary button — always last (far right).
   (It has no type="submit" attribute, so target the primary button.) */
.note-composer .row [type="submit"],
.note-composer .row .btn--primary    { order: 10; }
.note-composer .row .voice-note-status { order: 11; flex-basis: 100%; text-align: right; }
/* Match these buttons to the Send button's size. */
.note-composer .row .voice-btn { font-size: var(--fz-14); padding: 8px 12px; margin: 0; }
.note-composer .row .voice-btn .icon { width: 18px; height: 18px; }
/* Icon-only below 768px (see the note-composer__file-label rule up near
   .note-composer__file) — a tighter, square-ish footprint since there's no
   label sharing the button anymore. */
@media (max-width: 768px) {
    .note-composer .row .voice-btn { padding: 6px 8px; }
}

/* Drag-and-drop highlight for the composer */
.note-composer.is-dragover { outline: 2px dashed var(--accent-hi, #4a86ff); outline-offset: 3px; border-radius: var(--radius-sm); }

/* ── Task templates list (Settings → Templates) ── */
.tpl-list { list-style: none; margin: var(--sp-2) 0 var(--sp-4); padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.tpl-item {
    display: flex; align-items: center; gap: var(--sp-3); justify-content: space-between;
    padding: 12px 14px; border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--bg-elev); transition: border-color .12s ease;
}
.tpl-item:hover { border-color: var(--border-strong); }
.tpl-item__main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.tpl-item__main strong { font-size: var(--fz-14); color: var(--text); }
.tpl-item__meta { font-size: var(--fz-12); color: var(--text-dim); }
.tpl-item__actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
.tpl-item__actions form { margin: 0; }

/* Composer attachment chip (voice notes etc.) — paperclip over its extension */
.compose-previews { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
.attach-chip {
    position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center;
    gap: 4px; width: 72px; height: 72px; padding: 6px;
    border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--bg-elev); color: var(--text-dim);
}
.attach-chip__clip { width: 22px; height: 22px; }
.attach-chip__ext { font-size: 10px; font-weight: 700; letter-spacing: .05em; }
.attach-chip__x {
    position: absolute; top: 3px; right: 3px; width: 18px; height: 18px; border: none; border-radius: 50%;
    background: rgba(0,0,0,.55); color: #fff; cursor: pointer; font-size: 12px; line-height: 1;
}
.attach-chip__x:hover { background: rgba(0,0,0,.8); }

/* Completed task: subtasks panel is read-only (editing controls hidden). */
#subtasks.is-locked .subtask__actions,
#subtasks.is-locked #subtask-add-form { display: none; }
#subtasks.is-locked .subtask__check { pointer-events: none; opacity: .6; }
#subtasks.is-locked .subtask__title { pointer-events: none; cursor: default; }

/* Completed task: locked fields stay in the form (values still save) but can't
   be changed. Uses pointer-events, NOT disabled, so nothing gets wiped on Save. */
/* Locked (completed-task) fields: not editable, but text stays full-contrast
   (no opacity fade — ADA). The pointer-events lock conveys the disabled state. */
.lockable.is-locked { pointer-events: none; }

/* #task references */
.task-ref { color: var(--accent-hi, #4a86ff); text-decoration: none; font-weight: 600; }
.task-ref:hover { text-decoration: underline; }
.task-ref-popup__num { color: var(--accent-hi, #4a86ff); font-weight: 700; font-variant-numeric: tabular-nums; }

/* Image lightbox overlay */
body.lightbox-open { overflow: hidden; }
/* Mobile search overlay (layout.css) — same "don't scroll the page behind
   a full-screen panel" rule. */
body.search-open { overflow: hidden; }
.lightbox {
    position: fixed; inset: 0; z-index: 1000; display: flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,.82); padding: 24px; cursor: zoom-out;
}
.lightbox__img { max-width: 94vw; max-height: 88vh; border-radius: var(--radius-sm); box-shadow: 0 12px 48px rgba(0,0,0,.5); cursor: default; }
.lightbox__bar { position: absolute; top: 14px; right: 18px; display: flex; align-items: center; gap: 10px; }
.lightbox__dl, .lightbox__close {
    border: none; cursor: pointer; background: rgba(255,255,255,.14); color: #fff;
    display: inline-flex; align-items: center; justify-content: center; text-decoration: none;
}
.lightbox__dl { gap: 7px; padding: 0 14px; height: 40px; border-radius: 999px; font-size: var(--fz-14); font-weight: 600; }
.lightbox__dl svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.lightbox__close { width: 40px; height: 40px; border-radius: 50%; font-size: 26px; line-height: 1; }
.lightbox__dl:hover, .lightbox__close:hover { background: rgba(255,255,255,.28); }
@media (max-width: 600px) { .lightbox__dl span { display: none; } .lightbox__dl { padding: 0; width: 40px; border-radius: 50%; } }

/* Task description block (first message) shown above subtasks */
.task-desc__label { font-size: 10px; text-transform: uppercase; letter-spacing: .05em; color: var(--text-dim); font-weight: 700; margin-bottom: 6px; }
.task-desc__body { color: var(--text); font-size: var(--fz-15, 15px); line-height: 1.55; word-wrap: break-word; }

/* Quick-add stacked checkboxes */
/* Quick-add: pair short fields into 50/50 rows so the modal stays compact on
   desktop (Template|Client, Assign to|Due date, urgent|project). Collapses to a
   single column on narrow screens. */
.qa-row {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: var(--sp-3);
    align-items: start;
}
.qa-row--single { grid-template-columns: 1fr; }   /* no templates configured */
.qa-row > .field { margin-bottom: 0; }

.qa-checks { display: grid; grid-template-columns: 1fr 1fr; gap: 10px var(--sp-3); align-items: center; margin-bottom: var(--sp-4); }

@media (max-width: 640px) {
    .qa-row, .qa-checks { grid-template-columns: 1fr; }
}
/* inline-flex + align-self so the clickable label only covers its text/box,
   not the empty space to the right of the row. */
.qa-check { display: inline-flex; align-self: flex-start; align-items: center; gap: 8px; text-transform: none; letter-spacing: 0; color: var(--text); cursor: pointer; }

/* ── Rich message composer (mention-composer.js) ──────────────────────────
   A contenteditable surface standing in for the message textarea, so
   @mentions render inline as pills. The real textarea is hidden but still
   the form field. */
.rich-composer__source {
    position: absolute !important;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
}
.rich-composer {
    background: var(--bg-input);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text);
    padding: 8px 10px;
    min-height: 80px;
    max-height: 320px;
    overflow-y: auto;
    line-height: 1.5;
    white-space: pre-wrap;
    overflow-wrap: anywhere;
}
.rich-composer:focus {
    border-color: var(--accent-hi);
    outline: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-hi) 20%, transparent);
}
.rich-composer:empty::before {
    content: attr(data-placeholder);
    color: var(--text-mute);
    pointer-events: none;
}
.rich-composer.is-invalid {
    border-color: var(--priority-urgent, #e5484d);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--priority-urgent, #e5484d) 22%, transparent);
}

/* A confirmed mention, inline in the message you're writing. */
.mention-pill {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    vertical-align: middle;     /* see .mention — avatar type must not set the baseline */
    padding: 1px 8px 1px 5px;   /* room for the preset avatar's 2px ring */
    margin: 0 1px;
    border-radius: 999px;
    background: color-mix(in srgb, var(--status-completed, #3fb950) 16%, transparent);
    border: 1px solid color-mix(in srgb, var(--status-completed, #3fb950) 55%, transparent);
    user-select: none;
    white-space: nowrap;
}
.mention-pill .avatar { width: 18px; height: 18px; font-size: 9px; }
.mention-pill__name {
    font-size: var(--fz-13, .85rem);
    font-weight: 600;
    color: var(--status-completed, #3fb950);
}

/* ─── Collapsible section (<details data-remember>) ───
   Used for long, low-priority lists like Completed Tasks on the dashboard. */
.collapse-section { margin-top: var(--sp-5); }
.collapse-section__summary {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 6px 0;
    cursor: pointer;
    list-style: none;
    font-size: var(--fz-14);
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    user-select: none;
}
.collapse-section__summary::-webkit-details-marker { display: none; }
.collapse-section__summary:hover { color: var(--text); }
.collapse-section__summary:focus-visible {
    outline: 2px solid var(--accent-hi, #4a86ff);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}
.collapse-section__chev {
    width: 15px; height: 15px;
    flex: 0 0 auto;
    fill: none;
    stroke: currentColor;
    stroke-width: 2.2;
    stroke-linecap: round;
    stroke-linejoin: round;
    transition: transform .15s ease;
}
.collapse-section[open] > .collapse-section__summary .collapse-section__chev { transform: rotate(180deg); }
.collapse-section__count {
    background: var(--bg-elev);
    color: var(--text-dim);
    border-radius: 999px;
    padding: 1px 8px;
    font-size: var(--fz-12);
    letter-spacing: 0;
}
.collapse-section > .task-list { margin-top: var(--sp-2); }

/* Description card: label + Edit on one row. Edit jumps to the same message in
   the Messages tab and opens its editor there. */
.task-desc__head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: 6px; }
.task-desc__head .task-desc__label { margin-bottom: 0; }
.task-desc__edit {
    margin-left: auto;
    background: none;
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-dim);
    font-size: var(--fz-12);
    padding: 2px 10px;
    cursor: pointer;
}
.task-desc__edit:hover { border-color: var(--accent-hi, #4a86ff); color: var(--text); }

/* Brief highlight on the message we jumped to, so it's obvious where you landed.
 *
 * box-shadow, NOT background. This used to animate `background` to transparent,
 * which for 1.2s overrode two things that own that property: .note--mine, whose
 * background is a color-mix of the author's own colour, and .note--pinned's
 * amber. Both flashed blue, went flat, then snapped back. It survived because
 * the only thing that triggered it was "Edit description", which lands on the
 * first message of a task — rarely yours, never pinned. The reply chip makes
 * this the common path, so it had to stop borrowing a property it does not own.
 *
 * The one interaction: .note:hover's box-shadow loses for the 1.2s. That beats
 * the alternative (an ::after overlay), which would have to argue with the
 * z-index:40 :has() rule above. */
.note--flash {
    animation: note-flash 1.2s ease;
}
@keyframes note-flash {
    0%   { box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent-hi, #4a86ff) 55%, transparent); }
    100% { box-shadow: 0 0 0 3px transparent; }
}

/* Settings → browser notifications block. */
.browser-notif {
    display: flex;
    align-items: flex-start;
    gap: var(--sp-3);
    flex-wrap: wrap;
    padding: var(--sp-3);
    margin-bottom: var(--sp-3);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    background: var(--bg-elev);
}
.browser-notif__main { flex: 1 1 260px; min-width: 0; }
.browser-notif__actions { display: flex; gap: 6px; flex: 0 0 auto; }

/* Dashboard stat bubbles as filter toggles (#27). */
.stat--link {
    display: block;
    text-decoration: none;
    color: inherit;
    cursor: pointer;
    transition: border-color .15s, background .15s, transform .05s;
}
.stat--link:hover { border-color: var(--border-strong); background: var(--bg-elev); }
.stat--link:active { transform: translateY(1px); }
.stat--link.is-active {
    border-color: var(--accent-hi, #4a86ff);
    box-shadow: inset 0 0 0 1px var(--accent-hi, #4a86ff);
}
.stat--link.is-active .stat__label { color: var(--text); }
/* A checkable control needs a visible keyboard focus ring. */
.stat--link:focus-visible { outline: 2px solid var(--accent-hi, #4a86ff); outline-offset: 2px; }

/* Team image upload in Admin → Teams (#24). */
.team-avatar-form { display: inline; }
.team-avatar-form__file {
    display: inline-flex;
    align-items: center;
    padding: 4px 10px;
    border: 1px dashed var(--border-strong);
    border-radius: var(--radius-sm);
    font-size: var(--fz-12);
    color: var(--text-dim);
    cursor: pointer;
    white-space: nowrap;
}
.team-avatar-form__file:hover { border-color: var(--accent-hi); color: var(--text); }
.team-avatar-form__file input[type="file"] { display: none; }
/* A team reads as a group, so square it off slightly to differ from a person. */
.avatar--team { border-radius: 22%; }

/* ── Team image picker modal ───────────────────────────────────────────── */
.team-avatar { display: flex; gap: var(--sp-4); align-items: flex-start; flex-wrap: wrap; }
.team-avatar__current {
    display: flex; flex-direction: column; align-items: center; gap: 6px;
    flex: 0 0 auto; font-size: var(--fz-12);
}
.team-avatar__current .avatar { width: 64px; height: 64px; font-size: 22px; }
.team-avatar__panes { flex: 1 1 340px; min-width: 0; }
.team-avatar__or {
    display: flex; align-items: center; gap: var(--sp-2);
    color: var(--text-mute); font-size: var(--fz-12);
    text-transform: uppercase; letter-spacing: .05em;
    margin: var(--sp-3) 0;
}
.team-avatar__or::before, .team-avatar__or::after {
    content: ''; flex: 1; height: 1px; background: var(--border);
}
.team-avatar__upload { margin: 0; }
.team-avatar__upload .dropzone { cursor: pointer; }
.team-avatar__upload .dropzone.is-over {
    border-color: var(--accent-hi, #4a86ff);
    background: color-mix(in srgb, var(--accent-hi, #4a86ff) 10%, transparent);
}
.team-avatar__filename { margin: 6px 0; font-size: var(--fz-12); }
.team-avatar__filename.is-bad { color: var(--priority-urgent, #e5484d); }
/* Result of the notification test — always says what happened. */
.browser-notif__result {
    flex: 1 1 100%;
    margin: var(--sp-2) 0 0;
    font-size: var(--fz-12);
    line-height: 1.5;
}

/* ── Filter button (shared: boards and the Activity page) ─────────────────
   Shows how many filters are on and names them on hover. Lives here, not in a
   page stylesheet, because more than one page uses it — that scoping mistake is
   what left the Activity page's button unstyled. */
.filter-btn {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    padding: 6px 12px;
    background: var(--bg-elev);
    border: 1px solid var(--border);
    border-radius: var(--radius-sm);
    color: var(--text-dim);
    font-size: var(--fz-13);
    line-height: 1.2;
    cursor: pointer;
    white-space: nowrap;
}
.filter-btn svg {
    width: 15px; height: 15px;
    fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
.filter-btn:hover { border-color: var(--border-strong); color: var(--text); }
/* Filters on: make that obvious at a glance. */
.filter-btn.is-active {
    border-color: var(--accent-hi, #4a86ff);
    color: var(--text);
}
.filter-btn:focus-visible { outline: 2px solid var(--accent-hi, #4a86ff); outline-offset: 2px; }
.filter-btn__count {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-width: 18px;
    height: 18px;
    padding: 0 5px;
    border-radius: 999px;
    background: var(--accent, #4a86ff);
    color: var(--accent-fg, #fff);
    font-size: 11px;
    font-weight: 700;
}

/* ── Client email aliases (Emails tab on a client) ────────────────────────
   The alias is the exact recipient address mail is filed by, so it reads as a
   value you can edit rather than a passive label. */
.alias-list { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--sp-2); }
.alias-chip {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 4px 8px 4px 12px;
    border: 1px solid var(--accent-hi, #4a86ff);
    border-radius: 999px;
    background: color-mix(in srgb, var(--accent-hi, #4a86ff) 12%, transparent);
    font-size: var(--fz-13);
}
.alias-chip__val { font-family: var(--font-mono, monospace); }
.alias-chip__x {
    background: none;
    border: none;
    padding: 0 2px;
    color: var(--text-mute);
    font-size: 16px;
    line-height: 1;
    cursor: pointer;
}
.alias-chip__x:hover { color: var(--priority-urgent, #e5484d); }
.alias-add { display: flex; gap: var(--sp-2); align-items: center; margin: 0; }
.alias-add input[type="email"] { flex: 1 1 260px; max-width: 340px; }
/* An avatar that opens full size in the lightbox. */
.avatar-zoom { display: inline-flex; cursor: zoom-in; }
.avatar-zoom:hover .avatar { outline: 2px solid var(--accent-hi, #4a86ff); outline-offset: 1px; }

/* ── Task rows on mobile ────────────────────────────────────────────────────
   The desktop row is one line of title + a wrapping meta line + a fixed 90px
   right column. Squeezed to a phone that column ate the title's space, the meta
   line broke into three, and the date and percent collided with each other and
   with the floating action button.

   Fixes, in the order they matter: give the title room by letting it use two
   lines instead of ellipsing mid-word; drop the long "Due <date>" from the meta
   line, since the right column already says it; let the right column shrink to
   its content instead of reserving 90px; and top-align everything so a two-line
   title doesn't drag the date down with it. */
@media (max-width: 768px) {
    .task-row {
        gap: var(--sp-2);
        padding: 10px var(--sp-2);
        align-items: flex-start;
    }
    /* Not centred any more, so the stripe has to be told to span the row. */
    .task-row__fresh { align-self: stretch; }

    .task-row__title {
        white-space: normal;
        overflow: hidden;
        display: -webkit-box;
        -webkit-line-clamp: 2;
        -webkit-box-orient: vertical;
        line-height: 1.3;
    }
    .task-row__meta-due,
    .task-row__meta-team { display: none; }
    /* What's left — #id · URGENT · status — fits one line at phone width. */
    .task-row__meta { gap: 6px; row-gap: 4px; }

    /* Was vertically centred, which floated it into the middle of the row once
       everything else moved to the top. */
    .task-row > .notif-count,
    .task-row.has-progress > .notif-count {
        align-self: flex-start;
        margin-right: 2px;
        min-width: 21px;
        height: 21px;
        font-size: 12px;
    }

    .task-row__side {
        min-width: 0;
        gap: 2px;
    }
    /* 20px bold was set to be scannable across a wide row; at phone width it's
       the single biggest thing crowding the title. */
    .task-row__date { font-size: 15px; font-weight: 700; }
    .task-row__date--none { font-size: 11px; }

    .task-row__project-icon { font-size: 17px; }
    /* Urgent rows inset themselves, which costs width twice over on a phone. */
    .task-row.is-urgent { margin: 4px; }
}
