@layer components {
  .message { position: relative; display: flex; gap: 9px; padding: 6px var(--gutter) 9px; font-size: 13px; }
  .message:hover { background: var(--surface); }
  .message:target, .message.is-mentioning-me { background: var(--jx-accent-soft); }
  /* The band's own opaque colour, for rings drawn on top of it. */
  .message { --band: var(--background); }
  .message:hover { --band: var(--surface); }
  .message--agent { --band: color-mix(in oklch, var(--background), var(--jx-accent) 5%); }
  .message--agent:is(:target, .is-mentioning-me) { --band: color-mix(in oklch, var(--background), var(--jx-accent) 9%); }
  .message-main { min-width: 0; flex: 1; }
  /* A person's messages in a row (lib/message_grouping.js): the name stays for screen readers, the time shows on
     hover where the avatar would be. */
  .message.is-continued { padding-top: 0; }
  .message:has(+ .message.is-continued) { padding-bottom: 2px; }
  .message.is-continued > .avatar { height: 0; visibility: hidden; }
  .message.is-continued .message-head { display: contents; }
  .message.is-continued :is(.message-author, .message-role) {
    position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap;
  }
  .message.is-continued .message-time {
    position: absolute; top: 2px; left: var(--gutter); opacity: 0; font-size: 11px; line-height: 1.8; /* decorative: the time also reads in its block's head */
  }
  .message.is-continued:focus-within .message-time { opacity: 1; }
  @media (hover: hover) { .message.is-continued:hover .message-time { opacity: 1; } }
  .message-head { display: flex; align-items: baseline; gap: 6px; line-height: 1.2; }
  .message-author { font-size: var(--text-body); font-weight: 600; }
  .message-role, .message-time, .message-edited, .message-provenance { color: var(--muted-foreground); }
  .message-time { font-variant-numeric: tabular-nums; }
  .message-body { margin-top: 2px; font-size: var(--text-body); line-height: 1.5; text-wrap: pretty; overflow-wrap: anywhere; }
  /* Text, tables and code follow the thread's width; cards keep their own sizes. */
  .message-table { margin: 4px 0 8px; overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-md); }
  .message-table table { width: 100%; border-collapse: collapse; overflow-wrap: normal; }
  .message-table :is(th, td) { min-width: 8rem; padding: 5px 9px; border-bottom: 1px solid var(--border); vertical-align: top; }
  .message-table :is(th, td) + :is(th, td) { border-left: 1px solid var(--border); }
  .message-table th { background: var(--muted); font-weight: 600; text-align: left; }
  .message-table tbody tr:last-child td { border-bottom: 0; }
  .message-table [align="right"] { text-align: right; }
  .message-table [align="center"] { text-align: center; }
  .message-body p { margin: 0 0 4px; }
  /* One to three emojis alone: large, with a tight line so the row grows by the glyph only. */
  .message-body--emoji { font-size: calc(var(--text-body) * 2.5); line-height: 1.2; }
  .message-body--emoji p { margin-bottom: 0; }
  .message-body pre { padding: 8px 10px; border-radius: var(--radius-md); background: var(--muted); overflow-x: auto; }
  /* The button stays put while a long line scrolls under it; shown on approach, always on a touch screen. */
  .code-block { position: relative; }
  .code-copy {
    position: absolute; top: 4px; right: 4px; padding: 1px 7px; border: 1px solid var(--border); border-radius: var(--radius-sm);
    background: var(--background); color: var(--foreground); font-size: var(--text-body); cursor: pointer;
  }
  @media (hover: hover) {
    .code-copy { opacity: 0; }
    .code-block:hover .code-copy, .code-copy:focus-visible { opacity: 1; }
  }
  .message-body blockquote { margin: 4px 0; padding-left: 10px; border-left: 2px solid var(--border); color: var(--muted-foreground); }
  /* Deleted for this person while « Annuler » is offered: looks as it will, the rest waits hidden. */
  .message.is-deleting > :is(.message-actions, .message-more),
  .message.is-deleting .message-main > :not(.message-head, .message-deleting-note) { display: none; }
  .message--deleted .message-body, .message-deleting-note { color: var(--muted-foreground); font-style: italic; }
  /* Shown at once by the composer, as the server will render it, until the server's copy takes its place: dimmed
     only when the send still runs after half a second, so an ordinary swap shows nothing. */
  .message.is-pending:not([data-state="waiting"]) { animation: message-pending-dim 0s .5s both; }
  @keyframes message-pending-dim { to { opacity: .6; } }
  /* Without animations (base.css), still dimmed: at once, as before. */
  @media (prefers-reduced-motion: reduce) {
    .message.is-pending:not([data-state="waiting"]) { opacity: .6; }
  }
  /* Waiting, it is dimmed part by part: what it says about its state, and its button, are not. */
  .message.is-pending[data-state="waiting"] > :not(.message-main),
  .message.is-pending[data-state="waiting"] .message-main > :not(.message-pending) { opacity: .6; }
  .message-pending { display: none; }
  .message.is-pending[data-state="waiting"] .message-pending {
    display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; margin: 2px 0 0;
    color: var(--muted-foreground); font-size: var(--text-meta);
  }
  .message-pending-label { display: inline-flex; align-items: center; gap: 4px; }
  .message-pending-cancel {
    position: relative; padding: 0; border: 0; background: none; color: var(--foreground); font: inherit;
    text-decoration: underline; text-underline-offset: 2px; cursor: pointer;
  }
  .message-pending-cancel:is(:hover, :active) { color: var(--muted-foreground); }
  /* A finger reaches it on 44 × 44 px, without drawing it larger. */
  @media (pointer: coarse) {
    .message-pending-cancel::after {
      content: ""; position: absolute; inset: 50% auto auto 50%; width: var(--tap); height: var(--tap);
      transform: translate(-50%, -50%);
    }
  }
  /* Agent replies: a faint tint, a rule and the name in the agent's color (--agent-color, inline). */
  .message--agent { background: var(--jx-agent-soft); box-shadow: inset 2px 0 0 var(--agent-color); }
  .message--agent .message-author { color: var(--agent-color); }
  .message-agent-avatar, .message-system-avatar {
    flex: none; display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px;
    border-radius: 6px;
  }
  .message-agent-avatar { color: var(--agent-color); background: color-mix(in oklch, var(--agent-color) 14%, transparent); }
  .message-system-avatar { color: var(--muted-foreground); background: var(--muted); }
  .message-agent-badge {
    padding: 1px 5px; border-radius: 4px; background: var(--jx-accent-soft); color: var(--jx-accent); font-weight: 700;
  }
  .message-state { display: inline-flex; align-self: center; align-items: center; gap: 3px; color: var(--muted-foreground); }
  .message[data-status="failed"] .message-state { color: var(--jx-no); }
  .message--system .message-author { color: var(--muted-foreground); }
  .message-quote {
    display: flex; gap: 6px; margin: 4px 0; padding: 4px 8px; border-left: 2px solid var(--border);
    color: var(--muted-foreground); text-decoration: none;
  }
  .message-quote-author { font-weight: 600; flex: none; }
  .message-quote-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .mention { padding: 0 3px; border-radius: 4px; background: var(--jx-accent-soft); color: var(--jx-accent); font-weight: 600; }
  /* Every chip looks the same (mockup): your own mentions show through the message background. */
  /* Two stacked accent tints drop the chip text below 4.5:1: opaque chip, outlined instead. */
  .message:is(:target, .is-mentioning-me) .mention {
    background: var(--background); box-shadow: inset 0 0 0 1px var(--jx-accent-line);
  }
  /* Hidden visually only: the buttons stay in the tab order and show once one takes focus. */
  .message-actions {
    position: absolute; top: 2px; right: 14px; display: flex; gap: 2px; padding: 2px; opacity: 0; pointer-events: none;
    border: 1px solid var(--border); border-radius: var(--radius-md); background: var(--popover); box-shadow: var(--shadow-sm);
  }
  /* Above the toast pile (30): the bar of the message in hand wins over a passing notice. */
  .message:is(:focus-within, .is-menu-open) .message-actions { z-index: 40; }
  .message:is(:focus-within, .is-menu-open) .message-actions { opacity: 1; pointer-events: auto; }
  /* Icons only, 28 px each; the commands the ⋯ menu clicks stay in the markup, hidden. */
  .message-actions .button--sm { gap: 1px; width: 28px; min-width: 0; height: 28px; min-height: 0; padding: 0; }
  .message-actions [hidden] { display: none; }
  /* A tap hovers too, and iOS Safari drops the click of a tap whose hover reveals content: a real hover only. */
  @media (hover: hover) {
    .message:hover .message-actions { z-index: 40; opacity: 1; pointer-events: auto; }
  }
  /* ⋯ opens the message's actions on a touch screen; a mouse has the row above. */
  .message-more { display: none; }
  @media (pointer: coarse) {
    /* The actions open in a sheet (long press or ⋯), which reads this row: it is never shown on a touch screen. */
    .message-actions { visibility: hidden; }
    /* A long press asks for the actions: no text selected, no system callout ("Copier" is among the actions). */
    .message {
      -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
      /* A swipe right answers a message (see below): the page still scrolls and zooms under the finger. */
      touch-action: pan-y pinch-zoom;
    }
    /* Without these, iOS gives a field being edited neither caret nor « Coller ». */
    .message :is(textarea, input, [contenteditable="true"]) {
      -webkit-user-select: text; user-select: text; -webkit-touch-callout: default;
    }
    /* Links and pictures keep the system's own long press (preview, open, save). */
    .message :is(a, img) { -webkit-touch-callout: default; }
    .message-more {
      position: absolute; top: 0; right: 4px; display: inline-flex; align-items: center; justify-content: center;
      width: var(--tap); height: var(--tap); padding: 0; border: 0; border-radius: var(--radius-md);
      background: transparent; color: var(--muted-foreground); opacity: 0; pointer-events: none;
    }
    .message:is(.is-touched, :focus-within) .message-more { opacity: 1; pointer-events: auto; }
    .message-more:is(:hover, :active) { background: var(--accent); color: var(--foreground); }
    .message-more:focus-visible { outline: 2px solid var(--jx-accent); outline-offset: -2px; }
    /* A swipe right answers a message (message_touch_controller): the content follows the finger, and the arrow it
       uncovers fills once a release would answer. */
    .message[data-swiping] > :not(.swipe-reply-cue) { transform: translateX(var(--swipe-x, 0px)); }
    .swipe-reply-cue {
      position: absolute; top: 50%; left: 8px; translate: 0 -50%; display: grid; place-items: center;
      width: 32px; height: 32px; border-radius: 50%; background: var(--muted); color: var(--muted-foreground);
    }
    .swipe-reply-cue[data-armed] { background: var(--primary); color: var(--primary-foreground); }
  }
  @media (pointer: coarse) and (prefers-reduced-motion: no-preference) {
    .message[data-swiping="settling"] > :not(.swipe-reply-cue) { transition: transform 0.2s ease-out; }
  }
  /* The ⋯ menu: its rules and entries show only when filled for the message chosen. */
  #message-actions-menu :is(.menu-item, .menu-separator)[hidden] { display: none; }
  html[data-read-only] .message-actions { display: none !important; }
  /* Stop and retry: hidden until the agent-run controller knows the reader may use them. */
  .message-run { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 4px; }
  .message-run [hidden] { display: none; }
  .message-run:not(:has(> :not([hidden]))) { display: none; }
  .message-failure { color: var(--jx-no); }
  /* Stop: a bordered button, 28px (36px on a phone), the same as the mockup's work line end. */
  .button.message-run-stop {
    height: 28px; padding: 0 10px; gap: 6px; border-color: var(--border); background: var(--background);
    color: var(--foreground); box-shadow: var(--shadow-xs);
  }
  .button.message-run-stop:is(:hover, :active) { background: var(--accent); }
  .button.message-run-stop:focus-visible { outline: 2px solid var(--jx-accent); outline-offset: 2px; }
  /* While the closed work line is on screen the button sits at its right end, on the same row; open, below the block. */
  .message-main--run:has(.agent-work--running:not([open])) > .message-run {
    height: 28px; margin-top: -28px; justify-content: flex-end; pointer-events: none;
  }
  .message-main--run:has(.agent-work--running:not([open])) > .message-run > .button { pointer-events: auto; }
  @media (max-width: 767px) {
    .button.message-run-stop { height: 36px; padding: 0 12px; }
    .message-main--run:has(.agent-work--running:not([open])) > .message-run { height: var(--tap); margin-top: calc(var(--tap) * -1); }
  }
  /* Amber, like everything that waits for a person. */
  .button--wait { color: var(--jx-wait); border-color: color-mix(in oklch, var(--jx-wait) 35%, transparent); }
  .button--wait:is(:hover, :active) { background: var(--jx-wait-soft); color: var(--jx-wait); }
  .day-separator {
    display: flex; align-items: center; gap: 10px; margin: 12px var(--gutter) 4px;
    color: var(--muted-foreground); font-size: var(--text-meta); font-weight: 600;
  }
  .day-separator::before, .day-separator::after { content: ""; flex: 1; border-top: 1px solid var(--border); }
}
