/* ============================================================================
   chat-surface.css — THE CHAT ENGINE'S SURFACE (TA design pass 2026-09)

   Magnus, 2026-09-07:
     "Chat - centered! I also want this to get a thorough overhaul like the
      mail page has. Containers and not lines as separators. The Staff portal
      chat should get the same overhaul."
   and the ruling that followed: the restyle goes through the SHARED chat
   engine so both chats change together.

   ── WHY THIS IS A NEW FILE, AND WHY IT IS SHIPPED TWICE ───────────────────

   The chat engine is five files that ship BYTE-IDENTICAL to every surface
   (doctrine/staff-portal/01-chat-canonical-files.md): chat-core.js,
   chat-ui.js, image-utils.js, chat-utils-shim.js and the page wiring file
   (chat.js on Threshold Admin, pages/chat.js on Staff Portal). Its LOOK,
   though, was never one file: Threshold Admin keeps it in css/chat.css and
   Staff Portal keeps the same rules pasted into a section of css/staff.css.
   Two copies of a stylesheet drift exactly the way the two copies of
   chat-core.js drifted before the doctrine, so this restyle does not go into
   either of them. It is a SIXTH canonical file, identical bytes in
   threshold-admin/src/css/ and staff-portal/css/, loaded last on both
   surfaces so it wins the cascade over whichever copy is underneath.

   ── WHY A LOCAL TOKEN BRIDGE ──────────────────────────────────────────────

   The two surfaces do not speak the same token vocabulary, and the shared
   chat CSS has been quietly asking both of them for names neither defines:

     var(--bg-card)   Threshold Admin has no such token. The eleven themes
     var(--bg-page)   define --bg-dark / --bg-medium / --bg-light. So the
     var(--fg-body)   chat sidebar, the chat page ground, the message text
     var(--fg-heading) colour and the author name were INVALID at computed-
                      value time on Threshold Admin - transparent surfaces and
                      inherited ink, silently, on every theme. panel.css has a
                      name for this failure mode; it calls it "the --bg-card
                      fault" twice.
     var(--bg-hover)  Staff Portal has no such token (it has --bg-card-hover).
                      So the message hover did nothing there.

   Rather than pick one surface's names and break the other, every colour,
   radius and size below is read once into a local variable on .chat-page with
   a fallback chain: Threshold Admin's token first, Staff Portal's second, a
   literal last. Nothing here can resolve to nothing.

   ── THE RULES THIS FILE APPLIES ───────────────────────────────────────────

   05-shape-and-border   No separator lines. Every border-top / border-bottom /
                         border-right that fenced one region off from the next
                         is replaced by a CONTAINER: its own ground, its own
                         radius, and whitespace around it.
   06-spacing-and-layout The 4px ladder, 16px gutters, 16px edge clearance.
                         Written as literal multiples of 4 rather than
                         --space-* because Staff Portal does not define that
                         scale and a shared file may not depend on it.
   03-typography         14px floor for anything carrying words; 13px only for
                         a counter or a badge.
   10-accessibility      Ink on the accent is --fg-on-accent, never #fff - the
                         accent is a THEME colour and half the themes set a
                         light one. A hover is --accent-hover, never
                         `opacity: .9`, which dilutes ink and fill together and
                         makes a compliant button less compliant.
   11-surface-rules      A container is LIGHTER than the page it sits on.

   ── SCOPE ─────────────────────────────────────────────────────────────────

   Section A is the full chat PAGE (.chat-page), rendered by the engine's page
   wiring file. On Threshold Admin that is Editorial > Chat. On Staff Portal
   it is pages/chat.js, which is loaded but not currently routed - see the
   note above Section B.

   Section B is the mini-chat card and its floating window (mini-chat-*, mc-*),
   rendered by chat-ui.js. That is what Staff Portal shows today. It is
   deliberately limited to changes that CANNOT move a box: colour, tone, the
   lines, and the two contrast faults. Its 8-12px type ladder is a density
   decision on a small dashboard card and is left for Magnus - see the note
   there.

   Nothing in this file changes an id, a handler, a label or a control.
   ============================================================================ */


/* ==========================================================================
   SECTION A — THE CHAT PAGE
   ========================================================================== */

/* ── The token bridge ─────────────────────────────────────────────────────
   Declared on .chat-page so it also reaches the modal, which is a sibling of
   the page in the engine's markup - hence the second selector. */
.chat-page,
.chat-modal-overlay {
    /* Grounds. --chat-surface is the CONTAINER and must be lighter than the
       page on the dark themes; --chat-raised is a container inside a
       container (a message row inside the message card). */
    --chat-page-bg:   var(--bg-dark,   var(--bg-body,        #14161a));
    --chat-surface:   var(--bg-medium, var(--bg-card,        #20232a));
    --chat-raised:    var(--bg-light,  var(--bg-card-hover,  #2a2e37));
    --chat-hover:     var(--bg-hover,  var(--bg-card-hover,  #343943));
    --chat-input-bg:  var(--bg-dark,   var(--bg-input,       #14161a));

    /* Ink.

       --chat-fg-dim ASKS FOR --fg-secondary FIRST, not --fg-dim, and that is
       not a preference. Measured across the eleven Threshold Admin themes:
       --fg-dim is a single hardcoded value in panel.css's :root (#6b7589, the
       Nord default) that NO theme overrides, so it is the same grey on a
       near-black ground and on a white one. On a card it measures 1.9:1 in
       nord and fails the 4.5:1 floor on seven of the eleven; on the group
       tint it fails on all eleven. --fg-secondary is themed, and measures
       4.99:1 at worst on a card and 5.27:1 on the raised tone.

       This is a house-wide fault (368 uses of var(--fg-dim) in the Threshold
       Admin stylesheets, including .thx-small) and its fix belongs in the
       foundation, which this lane may not touch - so the chat surface simply
       stops routing text through it. On Staff Portal --fg-secondary does not
       exist and the fallback lands on that surface's OWN --fg-dim, which is
       themed there (#777 light / #aaa dark) and correct. */
    --chat-fg:        var(--fg-primary,   var(--fg,      #e8e8e8));
    --chat-fg-2:      var(--fg-secondary, var(--fg-dim,  #a0a0a0));
    --chat-fg-dim:    var(--fg-secondary, var(--fg-dim,  #8b8b8b));
    --chat-on-accent: var(--fg-on-accent, #ffffff);

    /* Shape. Threshold Admin's radius ladder first, Staff Portal's single
       --radius second. */
    --chat-radius:    var(--radius-lg, 10px);
    --chat-radius-sm: var(--radius-md, 8px);
    --chat-radius-xs: var(--radius-sm, 4px);

    /* Type. Every theme sets --font-size-ui to 14 and --font-size-badge to
       13; Staff Portal sets neither, so the literals are the floor there. */
    --chat-fs:        var(--font-size-ui,    14px);
    --chat-fs-title:  var(--font-size-title, 17px);
    --chat-fs-badge:  var(--font-size-badge, 13px);

    /* TA-2163. The four values below were written as var() FALLBACKS inside
       ordinary declarations - `background: var(--accent-light, rgba(59,142,234,
       0.10))` - which is a raw colour literal in a component rule however it
       is dressed (doctrine 12). A custom property is where a colour is
       allowed to carry a literal, so the fallback chain moves HERE and the
       rules below read one name. No hue moves. */
    /* TA-2163, measured on all eleven Threshold Admin themes. These were
       var(--accent-light, ...) at two strengths, and --accent-light is a
       THEME value: several themes set it strong enough that the wash lifted
       an own-message row far enough to take its timestamp under 4.5:1 and its
       active reaction to 3.69:1. doctrine 05's tint rule already says why -
       "a wash moves the ground", so the text on it must be measured on the
       washed row. A color-mix off the accent is the same hue in every theme
       at a strength this file controls, which is what makes the measurement
       hold on all eleven rather than on the theme it was written in. */
    --chat-accent-wash:   color-mix(in srgb, var(--accent)  8%, transparent);
    --chat-accent-wash-2: color-mix(in srgb, var(--accent) 12%, transparent);
    --chat-error-bg:      var(--status-error-bg,   rgba(232,64,87,0.18));
    --chat-danger:        var(--danger,            #ef4444);
    --chat-shadow:        var(--shadow,            rgba(0,0,0,0.35));
    --chat-shadow-strong: var(--shadow-strong,     rgba(0,0,0,0.5));
}

/* ── The page: two containers and a gutter ────────────────────────────────
   Was a bare flex row with the sidebar fenced off by a border-right. It is a
   grid of two cards now, 16px apart, with 16px of clearance top and bottom -
   the page's own left/right clearance belongs to .thx-page on the host, which
   is what centres the whole thing. */
.chat-page {
    display: grid;
    grid-template-columns: 260px minmax(0, 1fr);
    gap: 16px;
    padding: 16px 0;
    align-items: stretch;
    /* kept from the original: the parent is a flex column and this box asks
       it for the remaining height (TA-1754). */
    flex: 1;
    min-height: 0;
    overflow: hidden;
    background: transparent;
    color: var(--chat-fg);
}

/* ── Sidebar ──────────────────────────────────────────────────────────── */
.chat-page .chat-sidebar {
    /* The grid column owns the width now; the old fixed 240px + border-right
       fought it. */
    width: auto;
    min-width: 0;
    background: var(--chat-surface);
    border-right: none;
    border-radius: var(--chat-radius);
    padding: 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    overflow: hidden;
}
.chat-page .chat-sidebar-section {
    padding: 8px 4px 4px;
    min-height: 0;
}
/* The last section (ONLINE) takes the slack and scrolls inside itself, so the
   card edge stays where it is. */
.chat-page .chat-sidebar-section:last-child {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-height: 0;
}
.chat-page .chat-user-list {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
}
.chat-page .chat-channel-list,
.chat-page .chat-dm-list {
    max-height: 30vh;
    overflow-y: auto;
}
.chat-page .chat-sidebar-heading,
.chat-page .chat-sidebar-section h3 {
    /* A section label is TEXT. It was 11px on Staff Portal and
       --font-size-sm on Threshold Admin; both read the floor now, and the
       label earns its quietness from colour and letter-spacing instead of
       from being too small to read (doctrine 03: dimmer, not smaller). */
    /* TA-2163: sentence case. Doctrine 03 - a label is not shouted; the
       weight and the dim ink carry it. The tracking stays, it is what makes
       a quiet label read as a section rather than as a line of prose.

       `none` and not simply the absence of a declaration: these headings are
       h3, and Threshold Admin's panel.css carries a bare
       `h3 { text-transform: uppercase }`. Taking the card's own declaration
       out left that one standing, which is a thing a rule count cannot see
       and a screenshot can. */
    font-size: var(--chat-fs);
    font-weight: 600;
    text-transform: none;
    letter-spacing: 0.06em;
    color: var(--chat-fg-2);
    margin: 0 0 4px 0;
}
.chat-page .chat-online-count {
    font-size: var(--chat-fs-badge);
    background: var(--chat-raised);
    color: var(--chat-fg-2);
}
/* The unread/mention counter beside a channel. The engine emits the mini-chat's
   .mc-badge here, and the two surfaces size that class differently in their own
   stylesheets - 13px on Threshold Admin, 9px on Staff Portal, which is the
   drift this file exists to stop. It is a counter, so it takes the doctrine's
   one exception to the floor, and now it takes the same one on both. */
.chat-page .mc-badge {
    font-size: var(--chat-fs-badge);
    line-height: 1.4;
}
.chat-page .chat-create-channel-btn {
    color: var(--chat-fg-2);
    font-size: var(--chat-fs);
    border-radius: var(--chat-radius-xs);
}
.chat-page .chat-create-channel-btn:hover {
    color: var(--chat-fg);
    background: var(--chat-hover);
}

/* A channel is a row CONTAINER, not a line of text. */
.chat-page .chat-channel,
.chat-page .chat-user-item {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 8px;
    margin-bottom: 4px;
    border-radius: var(--chat-radius-xs);
    font-size: var(--chat-fs);
    line-height: 1.4;
    color: var(--chat-fg);
    cursor: pointer;
    transition: background-color 0.12s;
}
.chat-page .chat-user-item { cursor: default; }
.chat-page .chat-user-item--clickable { cursor: pointer; }
.chat-page .chat-channel:hover,
.chat-page .chat-user-item--clickable:hover { background-color: var(--chat-hover); }
.chat-page .chat-channel.active {
    background-color: var(--accent);
    /* NEVER #fff: the accent is a theme colour and half the eleven themes set
       a light one (doctrine 10, and the same fault the foundation lane fixed
       on button.primary). */
    color: var(--chat-on-accent);
}
.chat-page .chat-channel-hash { opacity: 0.55; }
.chat-page .chat-channel-lock { margin-left: auto; font-size: var(--chat-fs); opacity: 0.5; }
.chat-page .chat-dm-empty {
    /* Was an inline style="font-size:11px" written into the markup, which no
       token could reach. */
    font-size: var(--chat-fs);
    color: var(--chat-fg-dim);
    padding: 4px 8px;
}

/* ── The conversation card ────────────────────────────────────────────── */
.chat-page .chat-main {
    background: var(--chat-surface);
    border-radius: var(--chat-radius);
    display: flex;
    flex-direction: column;
    min-width: 0;
    overflow: hidden;
}
.chat-page .chat-header {
    /* The rule under the header is gone; the card's own edge and the
       whitespace under the title are the boundary. */
    border-bottom: none;
    background: transparent;
    padding: 12px 16px 8px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
}
.chat-page .chat-header-name {
    font-size: var(--chat-fs-title);
    font-weight: 600;
    color: var(--chat-fg);
}
.chat-page .chat-header-topic,
.chat-page .chat-header-desc {
    font-size: var(--chat-fs);
    color: var(--chat-fg-2);
}
.chat-page .chat-header-btn {
    color: var(--chat-fg-2);
    font-size: var(--chat-fs);
    border-radius: var(--chat-radius-xs);
    padding: 8px 8px;
}
.chat-page .chat-header-btn:hover {
    color: var(--chat-fg);
    background-color: var(--chat-hover);
}
.chat-page .chat-pin-count {
    background: var(--accent);
    color: var(--chat-on-accent);
    font-size: var(--chat-fs-badge);
    border-radius: var(--chat-radius-xs);
}

/* ── Messages ─────────────────────────────────────────────────────────── */
.chat-page .chat-messages {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
    padding: 8px 16px 12px;
    display: flex;
    flex-direction: column;
    gap: 4px;
    background: transparent;
}
/* The engine writes its own connection state into this element - "Connecting…",
   "Loading…", "No messages yet. Say hello!", "Reconnecting in 5s…". The shared
   rule underneath is `.chat-status { display: none }` with a `.visible`
   modifier the page wiring never adds, so on the full chat page every one of
   those lines has been INVISIBLE. Scoped back on here: an operator waiting on
   a reconnect should be able to see that it is happening. */
.chat-page .chat-messages .chat-status {
    display: block;
    background: var(--chat-raised);
    color: var(--chat-fg-2);
    font-size: var(--chat-fs);
    border-radius: var(--chat-radius-sm);
    padding: 12px 12px;
    text-align: center;
    margin: 4px 0;
}
.chat-page .chat-messages .chat-status.error {
    background: var(--chat-error-bg);
    color: var(--chat-fg);
}

/* A message is a container. Each one carries its own ground and radius, so
   the eye separates them by tone and padding instead of by a rule - and the
   old hover, which yanked the row 20px wider with a negative margin, is gone. */
.chat-page .chat-msg {
    position: relative;
    display: flex;
    align-items: flex-start;
    /* The reply reference is the message row's FIRST CHILD in the engine's
       markup, so in a nowrap row it landed to the LEFT of the avatar, reading
       as a column rather than as a quotation above the message. It takes the
       full width below and the avatar starts the second line. */
    flex-wrap: wrap;
    gap: 12px;
    padding: 8px 12px;
    margin: 0;
    border-radius: var(--chat-radius-sm);
    background-color: var(--chat-raised);
    transition: background-color 0.12s;
}
.chat-page .chat-msg:hover {
    background-color: var(--chat-hover);
    margin: 0;
    padding: 8px 12px;
    border-radius: var(--chat-radius-sm);
}
/* Own messages read as a light accent wash rather than a different shape, so
   the column stays one column (doctrine 05: tone, not geometry). Written as a
   background-IMAGE over the row's own ground so the hover rule above, which
   sets background-COLOR, does not wipe it. */
.chat-page .chat-msg-own {
    background-image: linear-gradient(var(--chat-accent-wash),
                                      var(--chat-accent-wash));
}
.chat-page .chat-msg-avatar {
    width: 34px;
    height: 34px;
    border-radius: 50%;
    background: var(--accent);
    color: var(--chat-on-accent);
    font-size: var(--chat-fs);
    font-weight: 600;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;
    text-transform: uppercase;
}
.chat-page .chat-msg-body { flex: 1; min-width: 0; }
.chat-page .chat-msg-header {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin-bottom: 4px;
}
.chat-page .chat-msg-author {
    font-size: var(--chat-fs);
    font-weight: 600;
    color: var(--chat-fg);
}
/* A timestamp and an (edited) marker are WORDS, not counters, so they take the
   14px floor and earn their quietness from colour. The 13px exception in
   doctrine 03 belongs to counters and badges only - on this page that is
   .chat-online-count and .chat-pin-count, and nothing else. */
.chat-page .chat-msg-time,
.chat-page .chat-msg-edited {
    font-size: var(--chat-fs);
    /* TA-2163: --chat-fg-2, not --chat-fg-dim. The dim step measured 3.93:1
       to 4.4:1 on an own-message row in four of the eleven themes, because
       the accent wash lifts that row's ground. Quietness comes from the
       secondary step, which every theme measures over the floor. */
    color: var(--chat-fg-2);
}
.chat-page .chat-msg-text,
.chat-page .chat-msg-content {
    font-size: var(--chat-fs);
    line-height: 1.5;
    color: var(--chat-fg);
    word-wrap: break-word;
    white-space: pre-wrap;
}
.chat-page .chat-msg-deleted { color: var(--chat-fg-dim); font-style: italic; }
.chat-page .chat-system-msg,
.chat-page .chat-history-loading,
.chat-page .chat-messages-loading {
    font-size: var(--chat-fs);
    color: var(--chat-fg-dim);
    text-align: center;
    padding: 8px 0;
}
.chat-page .chat-msg-highlight {
    background-color: var(--chat-accent-wash-2) !important;
}
.chat-page .chat-drag-active {
    background-color: var(--chat-accent-wash);
}

/* The reply reference above a message: its own small container. */
.chat-page .chat-reply-ref {
    flex: 0 0 100%;
    display: flex;
    align-items: center;
    gap: 4px;
    font-size: var(--chat-fs);
    color: var(--chat-fg-2);
    margin-bottom: 4px;
    cursor: pointer;
}
.chat-page .chat-reply-ref-text { color: var(--chat-fg-dim); }

/* Hover actions sit on the raised ground, not on a line. */
.chat-page .chat-msg-actions {
    background: var(--chat-surface);
    border: none;
    border-radius: var(--chat-radius-xs);
    padding: 4px;
    gap: 4px;
    top: 6px;
    right: 8px;
}
.chat-page .chat-msg-action-btn {
    color: var(--chat-fg-2);
    font-size: var(--chat-fs);
    border-radius: var(--chat-radius-xs);
}
.chat-page .chat-msg-action-btn:hover {
    color: var(--chat-fg);
    background-color: var(--chat-hover);
}

/* Reactions. */
.chat-page .chat-msg-reaction {
    background-color: var(--chat-surface);
    border: none;
    border-radius: 999px;
    padding: 4px 8px;
    font-size: var(--chat-fs);
    color: var(--chat-fg-2);
    cursor: pointer;
}
.chat-page .chat-msg-reaction:hover { background-color: var(--chat-hover); }
.chat-page .chat-msg-reaction.active {
    background-color: var(--chat-accent-wash-2);
    /* TA-2163: --accent-ink is the step Threshold Admin's tokens lane built
       for exactly this - the accent walked in lightness until it clears
       4.5:1 as TEXT on its own wash (TA-2116). The staff portal's equivalent
       is --accent-text, pass 7's token of the same shape and for the same
       reason; the last link is the card's own ink for a surface with
       neither. */
    color: var(--accent-ink, var(--accent-text, var(--chat-fg)));
    font-weight: 600;
}
.chat-page .chat-reaction-picker {
    background: var(--chat-surface);
    border: none;
    border-radius: var(--chat-radius-sm);
    box-shadow: 0 4px 16px var(--chat-shadow);
}

/* Attachments. */
.chat-page .chat-attachment {
    margin-top: 4px;
    background-color: var(--chat-surface);
    border-radius: var(--chat-radius-xs);
    padding: 8px 8px;
}
.chat-page .chat-attachment-img { border-radius: var(--chat-radius-xs); }
.chat-page .chat-attachment-name,
.chat-page .chat-attachment-link,
.chat-page .chat-attachment-size {
    font-size: var(--chat-fs);
    color: var(--chat-fg-2);
}

/* The inline edit box. */
.chat-page .chat-edit-input {
    background: var(--chat-input-bg);
    border: none;
    border-radius: var(--chat-radius-xs);
    color: var(--chat-fg);
    font-size: var(--chat-fs);
    padding: 8px 8px;
}
/* TA-2163. `outline: none` used to sit on the RESTING rule, which is doctrine
   10's one unforgivable thing: the field had no focus mark at all until the
   :focus rule below happened to fire. It sits inside :focus-visible now,
   beside the 2px ring that replaces it, so a field at rest keeps whatever the
   browser gives it and a focused one wears ours. Same on all four fields. */
.chat-page .chat-edit-input:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--accent);
}
.chat-page .chat-edit-hint {
    font-size: var(--chat-fs);
    color: var(--chat-fg-dim);
    opacity: 1;
    margin-top: 4px;
}

/* ── Pinned, search, reply bar: containers, not fenced strips ─────────── */
.chat-page .chat-pinned-bar,
.chat-page .chat-search-bar,
.chat-page .chat-search-results,
.chat-page .chat-reply-bar {
    border-top: none;
    border-bottom: none;
    background-color: var(--chat-raised);
    border-radius: var(--chat-radius-sm);
    margin: 0 16px 8px;
}
.chat-page .chat-pinned-bar { max-height: 220px; overflow-y: auto; }
.chat-page .chat-pinned-header {
    border-bottom: none;
    font-size: var(--chat-fs);
    font-weight: 600;
    color: var(--chat-fg-2);
    padding: 8px 12px 4px;
    text-transform: none;
    letter-spacing: 0.05em;
}
.chat-page .chat-pinned-close,
.chat-page .chat-search-close,
.chat-page .chat-reply-bar-cancel {
    color: var(--chat-fg-2);
    font-size: var(--chat-fs-title);
    background: none;
    border: none;
    cursor: pointer;
    border-radius: var(--chat-radius-xs);
    padding: 0 4px;
}
.chat-page .chat-pinned-close:hover,
.chat-page .chat-search-close:hover,
.chat-page .chat-reply-bar-cancel:hover {
    color: var(--chat-fg);
    background-color: var(--chat-hover);
}
.chat-page .chat-pinned-item,
.chat-page .chat-search-item {
    border-bottom: none;
    background-color: var(--chat-surface);
    border-radius: var(--chat-radius-xs);
    margin: 0 8px 4px;
    padding: 8px 8px;
}
.chat-page .chat-pinned-item:hover,
.chat-page .chat-search-item:hover { background-color: var(--chat-hover); }
.chat-page .chat-pinned-item-author,
.chat-page .chat-pinned-item-text,
.chat-page .chat-pinned-empty,
.chat-page .chat-search-item-text,
.chat-page .chat-search-empty,
.chat-page .chat-search-count,
.chat-page .chat-search-item-header {
    font-size: var(--chat-fs);
}
.chat-page .chat-pinned-item-time { font-size: var(--chat-fs); color: var(--chat-fg-dim); }
.chat-page .chat-pinned-unpin {
    /* Was revealed only on hover (opacity 0 → 1), which hides an action from
       anyone not using a mouse. It is always there, quietly. */
    opacity: 1;
    color: var(--chat-fg-dim);
    font-size: var(--chat-fs);
}
.chat-page .chat-pinned-unpin:hover { color: var(--chat-danger); }
.chat-page .chat-search-input {
    flex: 1;
    background: var(--chat-input-bg);
    border: none;
    border-radius: var(--chat-radius-xs);
    color: var(--chat-fg);
    font-size: var(--chat-fs);
    padding: 8px 8px;
}
.chat-page .chat-search-input:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent); }
.chat-page .chat-search-bar { display: flex; padding: 8px; gap: 8px; }
.chat-page .chat-reply-bar { padding: 8px 12px; font-size: var(--chat-fs); color: var(--chat-fg-2); }
.chat-page .chat-reply-bar-text { opacity: 1; color: var(--chat-fg-dim); }

/* ── Typing line ──────────────────────────────────────────────────────── */
.chat-page .chat-typing {
    padding: 0 16px 4px;
    font-size: var(--chat-fs);
    color: var(--chat-fg-dim);
    min-height: 18px;
    font-style: normal;
}

/* ── The composer ─────────────────────────────────────────────────────── */
.chat-page .chat-input-area {
    /* The rule above the composer is gone; it is a container with its own
       ground sitting inside the conversation card. */
    border-top: none;
    background-color: var(--chat-raised);
    border-radius: var(--chat-radius-sm);
    margin: 0 16px 16px;
    padding: 8px;
    display: flex;
    align-items: center;
    gap: 8px;
}
.chat-page .chat-input {
    flex: 1;
    min-width: 0;
    background: var(--chat-input-bg);
    border: none;
    border-radius: var(--chat-radius-xs);
    color: var(--chat-fg);
    /* 16px, not 14: this is the one field on the page that is typed into all
       day, and doctrine 03 gives an input the larger step. */
    font-size: 16px;
    font-family: inherit;
    line-height: 1.4;
    padding: 8px 12px;
    min-height: 40px;
}
.chat-page .chat-input:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent); }
.chat-page .chat-input::placeholder { color: var(--chat-fg-dim); }
.chat-page .chat-upload-btn {
    color: var(--chat-fg-2);
    font-size: var(--chat-fs-title);
    background: none;
    border: none;
    border-radius: var(--chat-radius-xs);
    padding: 8px 8px;
    cursor: pointer;
}
.chat-page .chat-upload-btn:hover:not(:disabled) { color: var(--accent); background-color: var(--chat-hover); }
.chat-page .chat-send-btn {
    background-color: var(--accent);
    color: var(--chat-on-accent);
    border: none;
    border-radius: var(--chat-radius-xs);
    font-size: var(--chat-fs);
    font-weight: 600;
    padding: 0 16px;
    min-height: 40px;
    cursor: pointer;
    white-space: nowrap;
    /* NOT `opacity: .9`: opacity composites ink and fill together, so the
       hover state of a compliant button is a less compliant one. */
    transition: background-color 0.15s;
}
.chat-page .chat-send-btn:hover:not(:disabled) {
    background-color: var(--accent-hover, var(--accent));
    color: var(--chat-on-accent);
    opacity: 1;
}
.chat-page .chat-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.chat-page .chat-upload-progress {
    margin: 0 16px 8px;
    background-color: var(--chat-raised);
    border-radius: var(--chat-radius-xs);
    padding: 4px 8px;
}
.chat-page .chat-upload-progress-text { font-size: var(--chat-fs); color: var(--chat-fg-2); }

/* ── Empty state ──────────────────────────────────────────────────────── */
.chat-page .chat-empty { color: var(--chat-fg-dim); }
.chat-page .chat-empty p { font-size: var(--chat-fs); }

/* ── The create-channel dialog ────────────────────────────────────────── */
.chat-modal-overlay .chat-modal {
    background: var(--chat-surface);
    border-radius: var(--chat-radius);
    box-shadow: 0 12px 40px var(--chat-shadow-strong);
}
.chat-modal-overlay .chat-modal-header {
    border-bottom: none;
    font-size: var(--chat-fs-title);
    font-weight: 600;
    color: var(--chat-fg);
    padding: 16px 16px 8px;
}
.chat-modal-overlay .chat-modal-close { color: var(--chat-fg-2); }
.chat-modal-overlay .chat-modal-close:hover { color: var(--chat-fg); }
.chat-modal-overlay .chat-modal-label {
    font-size: var(--chat-fs);
    color: var(--chat-fg-2);
    font-weight: 600;
}
.chat-modal-overlay .chat-modal-input,
.chat-modal-overlay .chat-modal-select {
    background: var(--chat-input-bg);
    border: none;
    border-radius: var(--chat-radius-xs);
    color: var(--chat-fg);
    font-size: 16px;
    padding: 8px 12px;
}
.chat-modal-overlay .chat-modal-input:focus-visible,
.chat-modal-overlay .chat-modal-select:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent); }
.chat-modal-overlay .chat-modal-submit {
    background-color: var(--accent);
    color: var(--chat-on-accent);
    border: none;
    border-radius: var(--chat-radius-xs);
    font-size: var(--chat-fs);
    font-weight: 600;
    padding: 12px 16px;
    cursor: pointer;
    transition: background-color 0.15s;
}
.chat-modal-overlay .chat-modal-submit:hover {
    background-color: var(--accent-hover, var(--accent));
}

/* ── Narrow ───────────────────────────────────────────────────────────── */
@media (max-width: 900px) {
    .chat-page {
        grid-template-columns: minmax(0, 1fr);
    }
    .chat-page .chat-sidebar { display: none; }
}


/* ==========================================================================
   SECTION B — THE MINI-CHAT CARD AND ITS FLOATING WINDOW

   This is the chat Staff Portal actually shows today: chat-ui.js builds a
   card on the dashboard and a floating window, both in the mc-* vocabulary.
   Threshold Admin loads chat-ui.js but never mounts the card, so on that
   surface every rule below matches nothing - which is exactly why it is safe
   to ship the same bytes to both.

   WHAT IS DELIBERATELY NOT HERE: the mini-chat's type ladder. It runs from
   8px to 12px, which is under the doctrine floor everywhere, but it is a
   dense card on a dashboard and lifting it to 14px would change the size of
   every box on it. That is a design decision on a surface this lane cannot
   see running, so it is Magnus's to make, not one to ship blind. Flagged in
   the completion record.

   WHAT IS HERE: the things that cannot move a box - the separator lines, the
   two contrast faults, and the tokens that resolve to nothing.
   ========================================================================== */

.mc-card-layout,
.mc-float-window,
.chat-card {
    --chat-surface:   var(--bg-medium, var(--bg-card,       #20232a));
    --chat-raised:    var(--bg-light,  var(--bg-card-hover, #2a2e37));
    --chat-hover:     var(--bg-hover,  var(--bg-card-hover, #343943));
    --chat-fg:        var(--fg-primary,   var(--fg,     #e8e8e8));
    --chat-fg-2:      var(--fg-secondary, var(--fg-dim, #a0a0a0));
    --chat-on-accent: var(--fg-on-accent, #ffffff);
    /* TA-2163. The presence dot's hue, asked for by each surface's own name
       first. A custom property is where doctrine 12 allows a literal to be
       written; the last link in each chain is the value the engine drew
       before this task, so no surface's dot changes colour. */
    --chat-dot-online:  var(--chat-presence-online,  var(--status-ok,   #22c55e));
    --chat-dot-offline: var(--chat-presence-offline, var(--fg-secondary, #6b7280));
}

/* Lines → tone. Each of these fenced one region off from the next; the tone
   step does the same job without drawing anything (doctrine 05). */
.mc-card-layout { border-top: none; }
.mc-card-layout .mc-card-sidebar {
    border-right: none;
    background-color: var(--chat-raised);
}
.mc-card-layout .mc-card-header,
.mc-float-window .mc-card-header { border-bottom: none; }
.mc-card-layout .mini-chat-input-area,
.mc-float-window .mini-chat-input-area { border-top: none; }
.mc-card-layout .mc-upload-progress,
.mc-float-window .mc-upload-progress { border-top: none; }
.mc-card-layout .mc-search-bar,
.mc-float-window .mc-search-bar { border-bottom: none; }
.mc-float-window .mc-float-titlebar { border-bottom: none; }
/* TA-2163 took the box off rather than making it invisible. `border-color:
   transparent` is still a declared perimeter, and doctrine 05 bans the
   perimeter and not the hue - a transparent one is a rule nobody can see that
   the next pass has to read anyway. These three are `box-sizing: border-box`
   so the 2px comes out of the padding, not off the outside: the element's
   width and height do not move. */
.mc-card-layout .mc-msg-attachment--file .mc-msg-attachment-link,
.mc-float-window .mc-msg-attachment--file .mc-msg-attachment-link,
.mc-card-layout .mc-msg-reaction,
.mc-float-window .mc-msg-reaction,
.mc-card-layout .mc-msg-reaction.active,
.mc-float-window .mc-msg-reaction.active,
.mc-card-layout .mini-chat-input,
.mc-float-window .mini-chat-input { border: none; }

/* A message row becomes a container. Only the ground and the corner change -
   the padding the rule already had is untouched, so nothing moves. */
.mc-card-layout .mc-msg,
.mc-float-window .mc-msg {
    background-color: var(--chat-raised);
    border-radius: 6px;
}
.mc-card-layout .mc-msg:hover,
.mc-float-window .mc-msg:hover { background-color: var(--chat-hover); }

/* The two contrast faults, same as on the page: ink on the accent is the
   ink the THEME names, and a hover is a colour rather than a dilution. */
.mc-card-layout .mc-card-channel.active,
.mc-float-window .mc-card-channel.active {
    background-color: var(--accent);
    color: var(--chat-on-accent);
}
.mc-card-layout .mini-chat-send,
.mc-float-window .mini-chat-send {
    color: var(--chat-on-accent);
    transition: background-color 0.15s;
}
.mc-card-layout .mini-chat-send:hover:not(:disabled),
.mc-float-window .mini-chat-send:hover:not(:disabled) {
    background-color: var(--accent-hover, var(--accent));
    opacity: 1;
}

/* --fg-body is a name neither surface defines; the message text was taking
   whatever it inherited. */
.mc-card-layout .mc-msg-text,
.mc-float-window .mc-msg-text,
.mc-card-layout .mc-card-channel,
.mc-float-window .mc-card-channel { color: var(--chat-fg); }


/* ==========================================================================
   SECTION C — WHAT THE ENGINE USED TO WRITE INTO A STYLE ATTRIBUTE  (TA-2163)

   doctrine 12 + 14. Before this task the chat engine wrote 108 styles from
   JavaScript across chat-ui.js, chat-core.js and the page wiring file: a rule
   there is invisible to this stylesheet, to the design audit and to the next
   design pass, and it ships to both surfaces where nobody can theme it.

   They sorted into three kinds and each kind has a different right answer.

   A VISIBILITY TOGGLE is a class. Sixty-one of the 108 were
   `el.style.display = 'none' / '' / 'flex'`, which also made "is this open?"
   a string comparison against a value the stylesheet chose. One class does
   all of it, and the two helpers that set it live on the engine
   (StaffChat.showEl / StaffChat.elHidden) so three files share one
   definition.

   A GEOMETRY VALUE is a custom property. Where a drag left a window, where a
   right-click put a menu and how far an upload has got are facts about this
   moment, not design decisions - no stylesheet can know them. Written as a
   custom property the rules below read, the STYLESHEET still owns what the
   value means and the script only supplies the number.

   A STYLE DECISION is a rule, and it moves here: the heading icon's gap, the
   online dot, the collapse chevron, the dot before a DM name, the delete
   button's position in the dialogue.
   ========================================================================== */

/* ── A. Visibility ────────────────────────────────────────────────────────
   `!important` is deliberate and it is the narrow kind. This one class has to
   beat every rule that sets `display` on these elements across TWO surfaces'
   stylesheets - css/staff.css and css/chat.css each carry their own copy of
   the chat vocabulary - and it carries no other declaration, so there is
   nothing else it can win by accident. */
.chat-hidden { display: none !important; }

/* The six elements whose OWN rule said `display: none` because a style
   attribute was expected to turn them on. The stylesheet says what they are
   when they are showing; .chat-hidden says whether they are. */
.chat-search-bar,
.mc-search-bar,
.chat-upload-progress,
.mc-upload-progress,
.chat-reply-bar,
.chat-context-submenu { display: flex; }

/* ── B. Geometry the script supplies and this file interprets ────────────*/
.mc-float-window.mc-float--placed {
    left: var(--mc-float-x);
    top: var(--mc-float-y);
    right: auto;
    bottom: auto;
}
.mc-float-window.mc-float--sized-w { width: var(--mc-float-w); }
.mc-float-window.mc-float--sized-h { height: var(--mc-float-h); }
/* While the operator is dragging, the window follows the mouse and must not
   also be easing towards it. doctrine 08: motion is a cue, not a lag. */
.mc-float-window.mc-float--dragging { transition: none; }

.mc-mention-dropdown {
    left: var(--mc-drop-x, 0);
    bottom: var(--mc-drop-b, auto);
    width: var(--mc-drop-w, auto);
}
.mc-dialog-autocomplete {
    left: var(--mc-drop-x, 0);
    top: var(--mc-drop-y, auto);
    width: var(--mc-drop-w, auto);
}
.chat-context-menu {
    left: var(--mc-menu-x, 0);
    top: var(--mc-menu-y, 0);
}

/* An upload's progress. The bar's colour, height and corner are the
   stylesheet's; only how far along it is comes from the script. */
.mc-upload-bar,
.chat-upload-progress-bar { width: var(--mc-progress, 0%); }

/* ── C. The staff name colour ────────────────────────────────────────────
   TA-2163 moved the chosen colour out of a style attribute and into a custom
   property the row carries, so the picker owns the VALUE and this stylesheet
   owns the RULE. SF-2164 replaced the 25 named hues with one hex code, and
   with it came a second property, because one hex cannot be one colour here.

   The card renders on twenty-one measured grounds — the portal's chat layers
   light and dark, and the eleven Threshold Admin themes — running from
   #001123 to #ffffff. Nothing clears doctrine 10's 4.5:1 at both ends of
   that. So a name colour is stored once and drawn twice: the stored hex on a
   dark ground, and its derived deeper twin on a light one. js/chat-colour.js
   computes both and writes both; which one applies is a property of the
   GROUND, and the ground is measured at runtime and published as
   data-chat-ground on :root — so a theme nobody has built yet is handled too.
   No colour is chosen here and none is written from a script. */
.mc-msg-author,
.chat-msg-author,
.chat-page .chat-msg-author { color: var(--chat-name-colour, inherit); }

:root[data-chat-ground="light"] .mc-msg-author,
:root[data-chat-ground="light"] .chat-msg-author,
:root[data-chat-ground="light"] .chat-page .chat-msg-author { color: var(--chat-name-colour-alt, inherit); }

/* ── D. The style decisions that were markup attributes ──────────────────*/
/* The heading's icon: was `style="margin-right:6px;opacity:0.6"` in three
   places, now one rule on the doctrine 06 ladder. */
.mc-heading-icon { margin-right: 4px; opacity: 0.6; }

/* The online dot beside the count in the card's heading. It was
   `<i class="fas fa-circle" style="font-size:6px;color:#22c55e;margin-right:
   3px">` - a 6px GLYPH, which is text under the doctrine 03 floor written
   where no floor could reach it, in a hue with no token. A status dot is a
   circle, so it is drawn as one: no font-size at all, the colour off the
   surface's own token, and the badge around it still carries title="Online"
   so the dot is never the only carrier (doctrine 10). */
.mc-online-dot {
    display: inline-block;
    width: 8px;
    height: 8px;
    border-radius: 50%;
    background: var(--chat-dot-online);
    margin-right: 4px;
    vertical-align: baseline;
}
/* The dot before a DM name, twice a style attribute at 2px and 4px. */
.mc-card-user-dot { margin-right: 4px; }
/* Sentence case, said rather than merely not-said - see the note on the chat
   page's sidebar heading. These are divs rather than h3, so nothing shouts
   over them today; the declaration is here so nothing can. */
.mc-card-sidebar-heading,
.mc-float-sidebar-heading,
.mc-card-category,
.mini-chat-heading { text-transform: none; }
/* The collapse chevron. Was `style.transform = 'rotate(-90deg)'`. */
.mc-chevron--collapsed { transform: rotate(-90deg); }
/* The dialogue's delete button sits away from the other two, on the left. */
#mc-dialog-delete { margin-right: auto; }
