/* --- Message creation-time on hover ---
   Every message row (user + agent) is tagged with a data-time in appendEvent;
   reveal it at the bottom-right on hover. Descendant selector (not `>`) so a
   tool-call that got reparented into a .tool-group still shows its time; only
   leaf message rows carry data-time, so no nested/double label. Bottom-right
   avoids the user bubble's top-right pin/resend action buttons. */
#messages [data-time] { position: relative; }
#messages [data-time]:hover::after {
  content: attr(data-time);
  position: absolute;
  bottom: 2px;
  right: 6px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  background: var(--bg);
  padding: 0 5px;
  border-radius: 4px;
  pointer-events: none;
  opacity: 0.9;
  z-index: 3;
}

/* Copy button (⧉) on messages and code blocks — duplicated top-left and
   bottom-left so long content is copyable without scrolling either end. The
   glyph lives in ::before so it never lands in the copied text; a subtle chip
   background keeps it legible over content. Message copies reveal on message-row
   hover; code-block copies reveal only on that <pre>'s hover. */
/* Tucked flush into the host's corners (left/top/bottom 0, tight padding): the
   host's own padding is only 10-16px, so any inset here pushes the chip over the
   first characters of the content it overlays. */
.msg-copy {
  position: absolute;
  left: 0;
  opacity: 0;
  transition: opacity 0.1s;
  cursor: pointer;
  background: var(--bg);
  border: none;
  border-radius: 4px;
  padding: 0 3px;
  color: var(--text-muted);
  font-size: 13px;
  line-height: 1.5;
  z-index: 4;
}
/* Top copy button only on hosts >= 100px tall (JS ResizeObserver adds .copy-tall)
   — short content doesn't need it and would just stack top+bottom. */
.msg-copy.top { top: 0; display: none; }
.copy-tall > .msg-copy.top { display: block; }
.msg-copy.bottom { bottom: 0; }
/* The mark is written by makeCopyBtn now, not by `content` — the sprite cannot
   be reached from a CSS content value, and one drawing of "copy" for the whole
   app beats a second one that only exists here. */
.msg-copy { display: inline-flex; align-items: center; justify-content: center; }
.msg-copy.copied { color: var(--accent); }
#messages [data-time]:hover .msg-copy:not(.code-copy),
pre:hover .msg-copy.code-copy,
.msg-copy:hover { opacity: 0.85; }
/* THE ONE UNDER THE POINTER, and it is the family's rule because "the mark you
   are about to click lights up" is one fact about all of them, not six. It used
   to be written once for the star and once for reply, which left the copy button
   — the member the class is NAMED after — with no colour hover at all: the only
   rule that reached it was the opacity above, and 0.85 is the opacity the row's
   own hover had already given it, so nothing happened when you pointed at it
   (the owner, 2026-09-19). Stated on `.msg-copy`, it reaches every one of them,
   here and in a code block's corner, and cannot be given to a new button by
   remembering to.
   `:where()` holds it at one class of specificity — deliberately BELOW the
   plain `.pin-msg-btn:hover` / `.resend-msg-btn:hover` (07-prompts.css), which
   say blue-with-a-chip and green: those two are about what pinning and
   resending MEAN, they are written in a sheet that loads earlier, and this must
   not win over them by being later. Everything else has nothing to say about
   hover and takes this. */
@media (hover: hover) {
  :where(.msg-copy):hover { color: var(--accent); }
}

/* --- The star, on a message row ---
   The copy button's TWIN, and that is a statement about mechanism, not just
   about looks: `.msg-star` also carries `.msg-copy`, so every rule above — the
   box, the chip background, the muted colour, the 13px mark, the accent it takes
   under the pointer, and above all the
   `#messages [data-time]:hover .msg-copy:not(.code-copy)` reveal — applies to
   it without being restated. A second rule that merely resembled that one could
   drift from it; this cannot.
   The star BUTTON stands third on the chrome line under the message (below) —
   last of the three every message carries, and under your own the three that are
   only yours follow it — where `position: static` makes the three offsets here
   mean nothing. They are
   for the star that is still pinned to a corner: the saved MARK a phone keeps,
   which is this same element drawn in the top right of the bubble it belongs
   to. One declaration, read by the one drawing that is still in a corner. */
.msg-star {
  left: auto;
  right: 0;
  top: 0;
}
/* SAVED — the one way it differs from copy, and the reason the difference
   exists: a mark that only appears under a pointer or a tap cannot tell you
   what is saved while you scroll past it. So it is up with nothing hovering, on
   every viewport. A state class on the button, exactly as `.msg-copy.copied`
   recolours in place; there is no second marker anywhere, on the row or beside
   it. The hover rule is the more specific one, so its selector is repeated here
   — otherwise a saved star would DIM to 0.85 when the row came under the
   pointer, which is the wrong way round. */
.msg-star.on,
#messages [data-time]:hover .msg-star.on { opacity: 1; color: var(--accent); }
/* Filled, and `i-star` is the only symbol in the sprite that ever is: the set is
   stroke-only, and at 13px an outline going slightly heavier is not a difference
   anyone reads at a glance. */
.msg-star.on .icon { fill: currentColor; }

/* --- Reply, on a message row ---
   The third member of the family, and a `.msg-copy` like the other two: the box,
   the muted colour, the 13px mark, the accent under the pointer and the one
   reveal rule are already written above and are not restated for it. It stands
   between copy and the star on the chrome line of whichever row it is on
   (.msg-chrome below), so there is nothing to say about where it goes that the
   line does not already say. */
/* ARMED — the message the composer is answering. The state lives on the button,
   exactly as `.msg-star.on` holds a message's saved state, and the hover rule's
   selector is repeated for the same reason it is repeated there: without it, the
   armed mark would DIM when its row came under the pointer. */
.msg-reply.on,
#messages [data-time]:hover .msg-reply.on { opacity: 1; color: var(--accent); }

/* --- The plate above a reply's bubble ---
   "↩ the first line of what this answers", muted, one line, at the bubble's own
   right edge (`.msg-user` is a column with `align-items: flex-end`, so this is a
   sibling above the bubble and nothing about the bubble moves). It is a button
   because it does something — it takes you to that message — and the ellipsis is
   the whole of its wrapping policy: a plate that wrapped would be a second
   message above the message. */
.reply-plate {
  display: flex;
  align-items: center;
  gap: 4px;
  max-width: 80%;
  margin-bottom: 2px;
  padding: 0;
  background: none;
  border: none;
  border-right: 2px solid var(--border);
  padding-right: 6px;
  color: var(--text-muted);
  font: inherit;
  font-size: 12px;
  line-height: 1.5;
  cursor: pointer;
  text-align: left;
}
.reply-plate .reply-plate-text {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.reply-plate .icon { flex: none; }
@media (hover: hover) {
  .reply-plate:hover { color: var(--text); }
}
/* Its original is not in this session's history any more: it still says what the
   message was about, and no longer offers to go there. */
.reply-plate.inert { cursor: default; opacity: 0.6; }
@media (hover: hover) {
  .reply-plate.inert:hover { color: var(--text-muted); }
}

/* --- A message's line of chrome ---
   Everything you can do to a message, on ONE line UNDER it, on both kinds of
   row: copy · reply · star · pin · insert · resend under your own bubble at its
   RIGHT edge, copy · reply · star under the agent's text at its LEFT edge — the
   three both kinds carry first and in the same order, then the three that are
   only yours, resend last because it is the one that sends something (the owner,
   2026-09-19). Each line is flush with the edge its message is aligned to, which
   is the edge the eye is already at. No corner of a bubble holds a control (the
   owner, 2026-09-18): a corner is where a MARK goes, and what you can do to a
   message is a row you read in one place.
   All of them are `.msg-copy`, so everything above — the box, the muted colour,
   the 13px mark, the fade, and the `[data-time]:hover` reveal — already
   describes them, and this section only has to say where the line is.
   In FLOW, not over the message: the group is a sibling of the bubble, so the
   bubble is the same box it always was and the text inside it does not move by a
   pixel. What grows is the row, by this line and nothing else — which is why the
   height is stated here rather than left to the buttons: one number, and the
   time label below reads it. A row says whether it has one (`has-chrome`,
   written by appendEvent), because a tool card and an error banner do not.
   Neither line says which edge it is flush with, because neither has to: the
   user's is a flex ITEM of a column that is `align-items: flex-end`
   (05-messages.css), so it is as wide as its buttons and pinned to the same
   right edge the bubble is; the agent's row is a plain block, so its line is
   full width and its buttons start at the left with the message. One rule, two
   alignments, both of them the row's own. */
.msg { --msg-chrome-h: 0px; }
.msg.has-chrome { --msg-chrome-h: 17px; }
.msg-chrome {
  display: flex;
  align-items: center;
  gap: 2px;
  height: var(--msg-chrome-h);
}
/* Undoes the corner-pinning for every control in the group at once — they are
   items of a line here, not chips in a corner. `position` is all that changes:
   `left` / `right` / `top` mean nothing to a static box, so the `left: 0` every
   `.msg-copy` carries and the star's own `right` / `top` fall away together
   without being unset one by one. */
.msg-chrome .msg-copy { position: static; }
/* The hover time label sits at the row's bottom-right, which is now the chrome
   row's own place — and being generated content of the row it paints OVER the
   buttons, background and all. Lifted by the height of that line, which puts it
   back where it has always been: the bubble's bottom-right corner. A row without
   a line keeps its own 2px, and so does every row on touch, where the line is
   not drawn and the number is 0. */
#messages [data-time]:hover::after { bottom: calc(var(--msg-chrome-h, 0px) + 2px); }

/* --- Touch: no line at all, and the message is the control ---
   A line of 19x13 chips revealed by a pointer is a drawing for a pointer. A
   phone has none: the reveal it gets is whatever `:hover` a tap happens to
   supply, and buttons 2px apart cannot each carry the 44px target a fingertip
   needs without claiming one another's centres — with Resend, which sends the
   message again and asks nothing, among them.
   So where there is no hover the line is simply not drawn, and a TAP on the
   message opens the same acts as rows you can read (msg-menu.js). There is no
   `⋯`: the owner's ruling (2026-09-18) is that one is neither visible nor
   findable, and that a long press belongs to iOS's own text selection.
   `(hover: none)` is the query, and it is the one this app already writes this
   with: it is the absence of a pointer that makes the small drawing unusable,
   not the size of the finger. */
@media (hover: none) {
  /* The line goes, and with it the height the row kept for it and the lift the
     time label took to clear it — one number, so all three follow from it. */
  .msg.has-chrome { --msg-chrome-h: 0px; }
  .msg > .msg-chrome { display: none; }
}

/* --- The saved mark, in a corner, on touch ---
   A saved star is up at full strength with nothing hovering, on every viewport,
   precisely so you can read what is saved while scrolling past — that is the one
   way it differs from copy, and hiding it with the line on the one screen you
   scroll with a thumb would take away the feature rather than a target. So touch
   draws it where it has always been drawn: the top-right corner of the bubble of
   your own message, of the row of the agent's.
   It is not the button. It is a second element (makeSavedMark) carrying the same
   `.msg-star`, so syncStars fills both in one pass and there is no second piece
   of state; it is `pointer-events: none`, so it takes no tap from the message
   underneath it — the menu that tap opens is where saving and unsaving live. And
   it is drawn nowhere else: with a pointer the star on the line is already up
   and filled, and two marks for one fact is one too many. */
.msg-star.saved-mark {
  display: none;
  pointer-events: none;
}
@media (hover: none) {
  .msg-star.saved-mark.on { display: inline-flex; }
}

/* The menu a tap opens: the session sheet's own popover and the session sheet's
   own dress (.card-menu, 04-dashboard.css), with one thing added. Every row is a
   44px target — a full-width one, so no row can be hit by mistake for its
   neighbour, which is the whole reason a resend is safe to offer here without a
   second question. */
.msg-menu .card-menu-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
}
.msg-menu .card-menu-item .icon { flex: none; }
/* Copy says so in the row, in the copy button's own accent and with the copy
   button's own mark, and the sheet goes once it has been read. */
.msg-menu .card-menu-item.copied { color: var(--accent); }

/* --- Saved ---
   A screen of the chat column, so it scrolls like the transcript does and takes
   the same padding. Full width at every size: there is nothing here a narrow
   screen has to give up. */
#saved {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}
#saved-list {
  display: flex;
  flex-direction: column;
  gap: 18px;
  /* The transcript's own width, so a saved message wraps where it wrapped in the
     chat it came from. */
  max-width: 100%;
}
/* One saved message: the caption, then the message under it. A rule down the
   left binds the two into one block — without it, a run of captions reads as a
   list with messages loose between them. */
.saved-item {
  border-left: 2px solid var(--border);
  padding-left: 10px;
}
.saved-item .msg { margin-bottom: 0; }
/* Where it came from and when it was said, together on the left — they are one
   sentence about one message and reading them apart would be reading a table.
   The two things you can do to it go to the far end. Wraps rather than
   truncating: a session name is a thing the user wrote, and the phone is where
   it will not fit. */
.saved-cap {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 2px 8px;
  margin-bottom: 6px;
  font-size: 11px;
  color: var(--text-muted);
}
.saved-where { font-weight: 500; }
.saved-when { font-variant-numeric: tabular-nums; }
/* The buttons sit at the caption's right end, pushed there by the first of
   them. */
.saved-cap .saved-copy { margin-left: auto; }
/* Both are `.msg-copy` — one is the copy button and the other is the star that
   shares its class — so both arrive here absolutely positioned in a message's
   corner, which is not what this row is. Undone for both: they are ordinary
   items of the caption, always visible, because everything on this screen is
   saved and there is no hover to reveal anything with. */
.saved-cap .msg-copy {
  position: static;
  opacity: 0.85;
  display: inline-flex;
}
.saved-cap .msg-star { opacity: 1; }
@media (hover: hover) {
  .saved-cap .msg-copy.saved-copy:hover { opacity: 1; color: var(--text); }
}
@media (pointer: coarse) {
  .saved-cap .msg-copy { min-width: 44px; min-height: 44px; }
}
/* Nothing saved yet — said in the middle of the screen it is about, with the
   mark that puts things here so the sentence has something to point at. */
.saved-empty {
  margin: 48px auto;
  max-width: 320px;
  text-align: center;
  color: var(--text-muted);
  font-size: 14px;
}
.saved-empty .icon { font-size: 28px; opacity: 0.5; }
.saved-empty p { margin: 8px 0 0; }
.saved-empty-how { font-size: 12px; }

/* --- The subagents row (above the box) ---
   The eighth switch's row. Its shape is the queue/pins/wake-ups family's and not
   the services/jobs one, and the difference is the cap: a subagent plate is one
   line of text, so 140px is about four of them, which is the same call #jobs-bar
   makes at 200px for a card two lines tall. Past four, a strip you glance at has
   become the screen — so it scrolls instead of the page, which is the whole
   argument written out over #services-bar.

   The declarations are a third copy of this shape and that is on purpose rather
   than overlooked: the two that exist differ only in `max-height`, and folding a
   third selector into either would make one of the two comments above them a lie
   about which rows it describes. */
#subagents-bar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 16px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  max-height: 140px;
  overflow-y: auto;
}

/* A PLATE, not a sidebar row — the row is buildSubagentItem's and stays
   buildSubagentItem's, and what makes it read as a plate is here. The fill,
   border and radius are .owed-chip's to the pixel, because a person looking at
   this row is usually looking at #owed-bar or #jobs-bar in the same glance and
   two chip shapes in one strip is two kinds of thing.
   `margin-bottom: 0` because the bar has its own 4px gap; left alone the row's
   own 2px stacked on top of it and these plates sat further apart than the
   neighbouring rows'. */
#subagents-bar .session-item {
  background: var(--bg-surface2);
  border: 1px solid var(--border);
  border-radius: 6px;
  margin-bottom: 0;
}
/* Which agent is behind the nick. The relay's short name (agentBadgeLabel), in
   the muted mono every other small factual aside in this app wears — an uptime,
   a wake-up's ETA, a git count. Quieter than the nick on purpose: the nick is
   what you came to find, the kind is what you check. */
#subagents-bar .sub-agent {
  /* No margin of its own: .name-row is a flex box with a 6px gap, and a margin
     on top of it stood the kind twice as far from the nick as the dot stands
     from either. */
  flex-shrink: 0;
  font-family: var(--mono);
  font-size: 10px;
  color: var(--text-muted);
}

/* The row is a DESKTOP row, exactly like #owed-bar and for the same reason: the
   switch that puts it up lives in #prompt-buttons-bar, which a phone does not
   have, so a setting made at a desk must not follow the body class across and
   leave a phone holding a row it cannot put away. A phone reaches these same
   rows through the subagents mark in #composer-sections, which opens them as a
   sheet rather than switching a row.
   Neither rule below carries !important, so `.hidden` — which renderSubagentsBar
   writes when the session has no subagents — still wins over the show rule; and
   the dashboard is deferred to by the `.in-chat` in the selector itself rather
   than by a second rule. */
@media not all and (max-width: 768px) {
  #subagents-bar { display: none; }
  body.in-chat.bar-on-subagents #subagents-bar { display: flex; }
}
@media (max-width: 768px) {
  #subagents-bar { display: none !important; }
}
