/* A field you type in or choose from: a text input, a search box, a select. It is NOT
   .app-quiet-button, and the difference is what the box does. Both are filled at rest now
   that a grouped worded action carries the inset ground too, but a quiet button is something
   you press, so it washes on hover and scales on press; a field is a place to put something,
   so it never reacts to a pointer passing over it. Library's search and type filter, and the
   a list sort, had each written out the quiet button's box while meaning this. */
.app-field {
  box-sizing: border-box;
  max-width: 100%;
  min-height: var(--touch-min);
  padding: var(--control-pad-y) var(--control-pad-x);
  color: var(--ink);
  background-color: var(--field-fill);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-control);
  font: inherit;
  /* 16px, the scale's own --text-detail-body -- never smaller on a phone, since anything
     under 16px makes iOS Safari zoom the page on focus. */
  font-size: var(--text-input);
}

.app-field:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent);
}

select.app-field {
  appearance: none;
  -webkit-appearance: none;
  padding-right: calc(var(--control-pad-x) + 1.2rem);
  background-color: var(--field-fill);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(-45deg, transparent 50%, var(--muted) 50%);
  background-position: calc(100% - 0.85rem) 50%, calc(100% - 0.55rem) 50%;
  background-repeat: no-repeat;
  background-size: 0.4rem 0.4rem;
  text-overflow: ellipsis;
}

.app-field::placeholder {
  color: var(--muted);
  opacity: 1;
  text-overflow: ellipsis;
}

input.app-field {
  text-overflow: ellipsis;
}

/* Fields share a row while each keeps a useful minimum, then stack at full width. */
.app-field-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-r2);
  align-items: center;
}

.app-field-row > .app-field {
  flex: 1 1 9rem;
  min-width: 0;
}

/* Artwork: a podcast's cover, a library image, a candidate row's thumbnail. object-fit
   and a ground are the whole point -- without them a non-square source either distorts
   or shows the page through its own transparency. */
.app-media {
  display: block;
  flex: none;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius-control);
  object-fit: cover;
}

* {
  box-sizing: border-box;
}

html {
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--page);
}

body {
  margin: 0;
  overflow: hidden;
  /* Transparent, so .app-wallpaper (its own first child, a grid item behind everything)
     is what a person actually sees -- --page (var, above) is only the flat fallback for
     a canvas that layer itself cannot reach. */
  background: transparent;
  color: var(--ink);
  font: var(--text-detail-body)/var(--leading-detail-body) var(--font-body);
}

/* The app shell: a grid whose last track is the dock's own reserved space, so the app
   area can never occupy it or scroll under it. Portrait geometry lives here; landscape
   swaps the tracks in styles-threads-dock.css, beside the dock rules that fill them.
   isolation: isolate keeps .app-wallpaper's z-index: -1 inside the shell rather than
   behind the canvas. */
body.app-shell {
  display: grid;
  height: var(--app-height, var(--vh100));
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: minmax(0, 1fr) auto;
  isolation: isolate;
}

/* contain:layout makes this the containing block of every position: fixed descendant
   (the command bar, the toasts, the full-screen viewers), so nothing inside the app
   area can reach the dock's own track. */
.app-area {
  grid-row: 1;
  grid-column: 1;
  position: relative;
  min-width: 0;
  min-height: 0;
  contain: layout;
  overflow: clip;
}

/* The wallpaper spans every track in portrait, so the composition runs unbroken behind
   both the app area and the dock. */
.app-wallpaper {
  grid-row: 1 / -1;
  grid-column: 1 / -1;
}

/* Portrait: the dock is the shell's second row, centred on .chat's own measure. */
.dock-nav {
  grid-row: 2;
  grid-column: 1;
  justify-self: center;
  width: min(var(--shell-max), 100%);
}

/* The public pages (pair, install) are plain documents, not the app shell: body's
   overflow: hidden above would clip a long page at Large text or with the keyboard up. */
html:has(body.pair-page) {
  overflow-y: auto;
}

body.pair-page {
  overflow-y: auto;
  min-height: 100dvh;
}

.chat {
  width: 100%;
  max-width: var(--shell-max);
  height: 100%;
  margin: 0 auto;
  padding: calc(var(--safe-top) + var(--space-xs)) var(--shell-gutter) var(--shell-pad-bottom);
  display: flex;
  flex-direction: column;
  min-height: 0;
  gap: var(--shell-gap);
  /* Transparent, not --panel-subtle or --page: this is the pane frame, not a pane itself,
     and the wallpaper (body's own first child) is what shows through it and through the
     0.75rem gaps between the panes it holds. The border below stays: on desktop it is what
     still marks .chat's own column against the wallpaper running the full viewport width
     around it. */
  background: transparent;
  border: 1px solid var(--line);
  border-radius: var(--radius-overlay);
  transition: box-shadow 140ms ease, background-color 140ms ease;
}

/* At phone widths the shell column IS the screen, so its own 16px inset plus 1px border
   would stack under every pane's own gutter (--list-gutter on a field, --page-gutter on a
   header) and put a card ~34px from the edge against the reference's 17px. Every pane
   already owns its side inset, so the frame gives its side back, up to just under
   --shell-max (a media query cannot read the token, and shell-responsive.test.ts allows
   only the token itself to spell that width). The command bar (fixed inside .app-area)
   and the dock (in its own shell track) read --side-gutter themselves, so they are
   unaffected. The block borders keep their
   1px (the reservations under .chat-lower subtract it) but paint nothing, so no pale line
   runs along the top edge of the screen. */
@media (max-width: 47.99rem) {
  .chat {
    padding-inline: 0;
    border-inline-width: 0;
    border-block-color: transparent;
  }
}

.chat.file-drag-active {
  background-color: color-mix(in srgb, var(--ink) 4%, var(--page));
  box-shadow: inset 0 0 0 2px var(--muted);
}

.chat-upper,
.chat-lower {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  gap: var(--space-r3);
  /* The page-enter slide (below) pushes a section 14px past the right edge; in a mobile
     browser that transient overflow inflates the layout viewport, and the resulting resize
     (syncChatOverlayGeometry() in chat.js) strands --chat-overlay-bottom at a stale value.
     clip, not hidden: hidden would make these scroll containers and break the nested
     scrollers and sticky elements. overflow-y stays visible. */
  overflow-x: clip;
}

.chat-upper {
  flex: none;
}

/* Page-level notices at the top of the shell. display: contents so an empty container
   adds no row gap; each notice is its own flex item. */
.app-notices {
  display: contents;
}

.app-notice {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-r2);
  padding: var(--space-r2) var(--space-r3);
  border-radius: var(--card-radius);
  background: var(--panel-subtle);
  box-shadow: var(--shadow-card);
  font-size: var(--text-detail-body);
}

.app-notice p {
  margin: 0;
  margin-right: auto;
}

.app-notices[hidden],
.app-notice[hidden] {
  display: none;
}

/* With no visible pane in it (every display but Inbox; .chat-header is display: contents and
   its h1 is out of flow) .chat-upper is a 0-height flex item, and .chat's row gap would still
   sit above .chat-lower as phantom space at the top of the page. It takes that gap back. The
   wall route keeps its own layout untouched. */
body:not(.wall) .chat-upper:not(:has(> section:not([hidden]))) {
  margin-bottom: calc(-1 * var(--shell-gap));
}

.chat-lower {
  flex: 1 1 0;
}

/* Page change: whichever pane chat.js's own markDisplayEntering() just marked fades and
   slides in from the reference's own offset. `.display-enter` is a class chat.js adds to the
   one destination section syncAgentView() just made visible and removes from whichever one
   it replaces -- not the bare `:not([hidden])` selector a from a pure-CSS version of this
   used to key on, which restarts on its own the moment an element's `display` goes from none
   back to something real, but leaves no signal a DOM test can see (jsdom runs no animations
   and lays out nothing). `> section` catches every destination uniformly regardless of which
   of them additionally carries the inner .app-scroller some (not all -- Calendar and Email
   Review scroll a nested div instead) compose. */
@keyframes app-page-enter {
  from {
    transform: translateX(14px);
    opacity: 0.75;
  }
  to {
    transform: translateX(0);
    opacity: 1;
  }
}

.chat-upper > section.display-enter,
.chat-lower > section.display-enter {
  animation: app-page-enter var(--motion-page) var(--ease-page);
}

@media (prefers-reduced-motion: reduce) {
  .chat-upper > section.display-enter,
  .chat-lower > section.display-enter {
    animation: none;
  }
}

/* Clearance under the command bar, so the last row of the agenda, the grocery list, or
   a chart's own bottom ticker strip is never underneath the composer. It is on
   .chat-lower rather than on each pane because every display surface this app has sits
   inside it, including the near-zero-height one an expanded Email Review panel leaves
   behind. Unconditional: every display has the bar, including Today and Groceries. The
   status rail lives inside form#composer, so the measured --command-bar-space already
   grows with it; the thread zone overlays rather than resizes, so an open thread never
   moves this padding. The composer's transparent top padding, .chat's own bottom padding
   and its 1px border are space the reservation already owns, and the 4px is the clear
   band above the pill. */
.chat .chat-lower {
  padding-bottom: max(0px, calc(var(--command-bar-space) - var(--composer-pad-top) - var(--shell-pad-bottom) - 1px + 4px));
}

/* The soft landing below every scroller in .chat-lower (the shared .app-scroller: every
   phone destination, Calendar's day list and Email Review's detail included). The end
   padding keeps the last row clear of the bar; the scroll-padding band keeps focus and
   scrollIntoView targets clear of it too. There is deliberately no alpha mask on that
   band: a pale card faded over the wallpaper reads as a milky silver bar flush above the
   pill, so a card is either fully opaque or cut cleanly by the scroller's edge. Email
   Review's detail is the one scroller in .chat-upper. The selector outranks route rules
   that reset a scroller's padding (.podcasts, .agenda-days). */
body:not(.wall) .chat .app-scroller {
  padding-bottom: var(--space-r3);
  scroll-padding-bottom: 1.5rem;
}

/* The one command bar: the composer's own fixed positioning container, anchored to the
   bottom of .app-area -- contain:layout there is what this bar is fixed against, so the
   bar can never reach past the app area's own edge. The bar holds the composer only (the
   dock sits in its own shell track) and draws no ground of its own: each part keeps
   whatever surface it needs for its own legibility (the composer pill's --panel-subtle
   fill, a chip row's, the thread zone's own opaque --panel-subtle section), but the bar
   itself is background: transparent, border: none, no radius, no shadow. The thread zone
   and the status rail are part of the composer itself: both sit inside form#composer,
   attached to the top of the pill (see .thread-zone and .status-rail below), so their
   heights are the composer's height and all growth goes upward while the pill never
   moves for anything but the keyboard. --chat-overlay-bottom is the one geometry sync
   kept: chat.js rides the bar up over the iOS software keyboard, and this is the same
   custom property it already writes for that. Centred with auto inline margins against
   left: 0/right: 0 rather than a translateX, reusing exactly the reasoning the old
   fixed chat sheet solved already: a fixed element cannot inherit the centred shell
   column's width, and a transform would make this bar the containing block for its own
   fixed-position descendants, which nothing here needs. --shell-max is .chat's own
   max-width, repeated here for the same reason. */
.command-bar {
  position: fixed;
  z-index: 40;
  left: 0;
  right: 0;
  margin-inline: auto;
  width: min(var(--shell-max), 100%);
  bottom: var(--chat-overlay-bottom, 0px);
  display: flex;
  flex-direction: column;
  min-width: 0;
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
}

/* Keeps the command bar visible while the keyboard is open. */
.chat.keyboard-open .command-bar {
  background: var(--panel);
  box-shadow: var(--shadow-keyboard-bar);
  pointer-events: auto;
  padding-bottom: var(--space-md);
}

/* The keyboard-open bar already carries its own bottom padding; no second gap. */
.chat.keyboard-open .composer {
  padding-bottom: 0;
}

/* Fills the strip between the bar and the keyboard (iOS draws its form accessory row there, translucent over the page) so no page content shows through it. */
.chat.keyboard-open .command-bar::after {
  content: '';
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 8rem;
  background: var(--panel);
}

.chat.keyboard-open .composer-row {
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

/* The bar's pass-through boxes (the bar itself, the composer's 17px top padding) let
   pointer events through, so a tap on the scroller's fade band above the pill reaches
   the page content that is visibly there. The thread zone and the status rail are not
   among them: each paints an opaque part of the connected surface, so each takes its
   own taps -- a pass-through rail would let a tap on its 1px seam above the pill (or in
   the gap between two of its rows) land on the page underneath it. Empty, the rail is
   display: none, so it never blocks anything. Every box that takes a tap -- a thread
   card, a chip row, a status line, the player surface, the folded player chip, the pill
   -- takes them back, so nothing in the bar stops being clickable. */
.command-bar,
.composer,
.composer-status {
  pointer-events: none;
}

.thread-zone > *,
.status-rail > *,
.composer-status > *,
.composer-row,
.podcast-mini-surface,
.podcast-mini-chip {
  pointer-events: auto;
}

/* Empty -> zero height, the normal state for both zones: neither reserves space or shows a
   surface of its own when it has nothing to say -- the rail attached to the pill included,
   which is what lets the pill keep its own fully rounded shape whenever no chip is
   showing. */
.thread-zone:empty,
.status-rail:empty {
  display: none;
}

/* Collapsed or expanded, the thread zone is the TOP of the one attached surface the rail
   and the input pill finish (the same pattern .status-rail below already states): it sits
   inside form#composer directly above the rail, on the --panel-subtle fill with the inset
   highlight (not the card shadow -- no lift at the seam), owning the surface's top
   --radius-overlay corners while the sections below it take square joins. Like the rail it
   pads nothing horizontally and sets no margin of its own: the composer's own side padding
   wraps zone, rail and pill alike, so all three sections' edges line up exactly. Collapsed
   it is a short stack of one-line summary rows (up to three, then "+N more"); expanded it
   grows upward, never resizing .chat-lower's reservation above (INV-1), its long body
   scrolling inside the panel's own viewport-bounded scroller (below). The zone's own inner
   padding is the small 0.25rem, so the fill runs unbroken around every row it holds. The
   "expanded" class name matches this file's existing convention for a state word added
   alongside a component's own class (compare .chat-open, .review-expanded). */
.thread-zone {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0;
  padding: var(--space-r1);
  background: var(--panel-subtle);
  border-radius: var(--radius-overlay) var(--radius-overlay) 0 0;
  box-shadow: var(--shadow-inset-highlight);
  pointer-events: auto;
}

/* The first max-height is the fallback for engines without dvh; the second bounds the panel
   to the viewport actually left above it once the keyboard has ridden the bar up
   (--chat-overlay-bottom, which chat.js writes) and the composer has taken its
   share (--command-bar-base: the composer height WITHOUT the thread zone, which
   chat.js writes alongside --command-bar-space precisely so this bound does not include the
   panel it is bounding -- with the space token here the panel's own height would eat its
   own allowance), so a long answer scrolls inside the panel instead of running off the top
   of the screen. overscroll-behavior keeps that scroll from chaining into the page behind. */
.thread-zone.expanded {
  max-height: 45vh;
  max-height: min(45dvh, calc(100dvh - var(--chat-overlay-bottom, 0px) - var(--command-bar-base, var(--command-bar-space)) - var(--safe-top) - 2rem));
  overflow-y: auto;
  overscroll-behavior: contain;
}

/* The rail is attached to the composer, not floating over the pane: it sits inside
   form#composer directly above .composer-row and draws the top half of the ONE dark
   surface the pill is the bottom half of -- same --panel-subtle fill, same inset
   highlight, the pill's own top corners squared off underneath it (the seam rules below)
   -- so a receipt reads as part of the thing it is a receipt for, not a pill hovering
   above it. Side padding is the composer's own (it wraps pill and rail alike), so the rail
   pads nothing horizontally; overflow: hidden keeps a collapsing row (chat.js retires with
   an animation) inside the surface's rounded top. And because it is inside the composer,
   its height is the composer's height: syncCommandBarSpace() measures them together, so
   the page's bottom reservation grows with the rail and the pane above is pushed up,
   never overlaid. Like the thread zone, the rail is an opaque part of the connected surface
   and takes its own taps (pointer-events: auto), its seam hairline included; empty, it is
   display: none, so it never blocks anything. */
.status-rail {
  display: flex;
  flex-direction: column;
  padding: 0;
  background: var(--panel-subtle);
  border-radius: var(--radius-overlay) var(--radius-overlay) 0 0;
  box-shadow: var(--shadow-inset-highlight);
  overflow: hidden;
  pointer-events: auto;
}

/* With content, the surface ends in a hairline seam above the pill -- --line cut mostly
   transparent, because the full-strength token reads as a hard edge on this fill -- and
   the pill gives up its top corners and inset highlight so nothing cuts across the join.
   A row collapsing out (chat.js's retire animation) keeps the rail :not(:empty) until it
   finishes, so the surface holds its shape for the whole 180ms rather than flashing
   between attached and rounded. */
.status-rail:not(:empty) {
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}

/* The rail now sits ABOVE the activity drawer (confirmations at the top edge of the attached
   area, the drawer, then the pill), so the first non-empty of the two owns the surface's top
   corners, the joint between them is square, and the pill loses its top corners whenever
   either sits above it. When both are empty (display: none via the :empty rule above) the
   pill keeps its full rounding. */
.thread-zone:not(:empty) {
  border-bottom: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}

.status-rail:not(:empty) + .thread-zone {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  box-shadow: none;
}

.status-rail:not(:empty) ~ .composer-row {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  box-shadow: none;
}

.thread-zone:not(:empty) + .composer-row {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  box-shadow: none;
}

/* The rail's one row component, two states replacing IN PLACE (pending -> receipt).
   Newest nearest the composer: chat.js appends after existing rows, and the rail's column
   flow puts a later sibling closer to the composer below it. data-state follows this
   file's existing convention for a control's own variant (compare
   .dictate[data-state="listening"]) rather than a second class.

   A row IN the attached surface, not a pill of its own: transparent over the rail's own
   fill, no border, radius or shadow (INV-6: the bar's elevation is the only one, and the
   connected surface is the point), left padding that lines the label up beside the pill's
   own contents, right room for the dismiss control's 44px target (.thread-dismiss, the
   one dismiss every dismissible thing in the bar shares), and a min-height that keeps
   every row a comfortable touch height. Always a <div> (chat.js's renderRail), never a
   <button>: a receipt state hosts its own nested .thread-dismiss, and one button cannot
   contain another. border: none is written explicitly rather than omitted, purely for
   clarity alongside the other resets here, not to defeat the generic `button` rule
   (below) the way it would if this were still a bare button. Growth is upward and
   unbounded here on purpose -- a wrapped multi-line .chip-label (see below) grows the row,
   which grows .status-rail, which grows the composer it is measured with, so the
   reservation above follows while the pill and dock stay put. */
.chip {
  display: flex;
  align-items: center;
  gap: var(--space-r2);
  min-height: var(--touch-min);
  max-width: 100%;
  padding: 0.35rem var(--space-r1) 0.35rem var(--space-r4);
  color: var(--ink);
  background: transparent;
  border: none;
  border-radius: 0;
  box-shadow: none;
  font-size: var(--text-detail-body);
}

/* Consecutive rows share one surface, so a hairline -- the same weakened --line the seam
   above the pill uses -- is all that tells two chips apart. */
.chip + .chip {
  border-top: 1px solid color-mix(in srgb, var(--line) 60%, transparent);
}

/* Wraps IN FULL -- never truncated, never hides text behind an ellipsis. The rail's one
   length boundary (chat.js's enterRail(), bounded by RAIL_CHIP_MAX_CHARS -- the receipt
   agent's own output.maxChars) keeps anything that would need more than a short line or two
   out of a chip altogether and into a reply thread instead, so nothing that reaches this
   label ever has content with nowhere else to be read. flex: 1 1 auto takes the row's width
   and pushes the dismiss control to the right edge; overflow-wrap keeps one unbroken long
   token (a number, a run-on word) from forcing the row wider than the rail rather than
   wrapping it. */
.chip-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow-wrap: anywhere;
}

.chip-spinner {
  flex: none;
  width: 14px;
  height: 14px;
  border: 2px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: chip-spin 0.8s linear infinite;
}

/* aria-busy is the single busy look, applied at the tap (appParts.busy). */
button[aria-busy="true"] {
  opacity: 0.6;
  cursor: progress;
}

/* A receipt is a navigation target (tap it -> the affected object); a pending chip is not.
   Not a real <button> (see .chip's own comment above), so both affordances a real button
   gives for free are restated here: the pointer cursor, and (below) a focus ring. */
.chip[data-state="receipt"] {
  cursor: pointer;
}

/* Inset, not the outset ring `button:focus-visible` uses: the rail clips overflow (a
   collapsing row has to stay inside it), so anything drawn outside the row's own box
   would be cut off at the surface's edge. */
.chip[data-state="receipt"]:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--accent) 45%, transparent);
}

@keyframes chip-spin {
  to {
    transform: rotate(360deg);
  }
}

/* chat.js adds .chip-enter to a row as it is created and removes it on animationend. The
   enter animation is only safe at all because renderRail reconciles by key: an existing
   row is updated in place, never rebuilt, so the slide happens exactly once per chip. */
.chip-enter {
  animation: chip-enter 180ms var(--ease-page);
}

@keyframes chip-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* A row chat.js is animating out is already gone for input purposes: no tap can land on
   something that is about to disappear. */
.chip-leaving {
  pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .chip-enter {
    animation: none;
  }
}

/* The thread zone's collapsed one-line row, up to three shown then "+N more" below. Tapping it
   (or its own chevron) expands that thread into the full thread body.

   Flat (INV-6): no background, no border, no shadow of its own -- it reads as a row IN
   the thread zone's one opaque panel, not a card floating on it. Separation from a
   sibling row comes from .thread-zone's own gap, not a second elevation. background/border
   are written explicitly rather than omitted because .thread-summary is a <button>, and
   the generic `button` rule (below) would otherwise fill it with --accent and an accent
   border. */
.thread-summary {
  display: flex;
  align-items: center;
  width: 100%;
  gap: var(--space-r2);
  padding: 0.55rem var(--space-r3);
  color: var(--ink);
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--radius-control);
  font-size: var(--text-detail-body);
}

.thread-summary:hover,
.thread-summary:focus-visible {
  background: var(--code);
}

.thread-summary-text {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* At Large the one-line summary would drop most of a long thread title, so it gets two lines. */
:root[data-text-size="large"] .thread-summary-text {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  line-height: 1.35;
  overflow-wrap: break-word;
  white-space: normal;
}

/* The activity drawer: one compact row (the most urgent task) with a "+N" control, or the
   whole list, or one opened detail (.thread). Part of the zone's one opaque surface, so none
   of it draws a background, border or shadow of its own beyond a row's hover fill. */
.activity-collapsed,
.activity-item {
  display: flex;
  align-items: stretch;
  gap: var(--space-r1);
  min-width: 0;
}

/* The row sits inside its .activity-item wrapper, so the wrapper is what must fill the bar
   for the "+N" control to land at the right edge. */
.activity-collapsed > .activity-item {
  flex: 1 1 auto;
}

.activity-row {
  flex: 1 1 auto;
  width: auto;
  min-width: 0;
  min-height: var(--touch-min);
  display: flex;
  align-items: center;
  gap: var(--space-r2);
  padding: 0.55rem var(--space-r3);
  color: var(--ink);
  text-align: start;
  background: transparent;
  border: none;
  border-radius: var(--radius-control);
  font-size: var(--text-detail-body);
}

button.activity-row:hover,
button.activity-row:focus-visible {
  background: var(--code);
}

.app-run {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-r2);
}

.app-run[hidden],
.app-run-title[hidden] { display: none; }

.app-run-dot {
  flex: none;
  margin-inline: calc((1rem - 9px) / 2);
  width: 9px;
  height: 9px;
  background: var(--muted);
  border-radius: 50%;
}

.app-run[data-state="working"] .app-run-status { color: var(--accent); }
.app-run[data-state="needs-you"] .app-run-status { color: var(--icon-gold); }
.app-run[data-state="ready"] .app-run-status { color: var(--ok); }
.app-run[data-state="failed"] .app-run-status { color: var(--error); }
.app-run[data-state="working"] .app-run-dot { background: var(--accent); }
.app-run[data-state="needs-you"] .app-run-dot { background: var(--icon-gold); }
.app-run[data-state="ready"] .app-run-dot { background: var(--ok); }
.app-run[data-state="failed"] .app-run-dot { background: var(--error); }

.app-run .chip-spinner {
  flex: none;
  box-sizing: border-box;
  width: 1rem;
  height: 1rem;
}

.app-run-body {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: center;
  gap: var(--space-r2);
}

.app-run-body[hidden],
.app-run-line[hidden] { display: none; }

.app-run-title {
  min-width: 0;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  text-overflow: ellipsis;
  white-space: normal;
  overflow-wrap: normal;
  overflow: hidden;
}

.app-run-line {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  font-size: var(--text-caption);
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
}

.app-run-status {
  flex: none;
  font-weight: var(--w-title);
  white-space: nowrap;
}

.app-run-detail {
  flex: 0 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--muted);
  font-weight: 400;
  white-space: nowrap;
}

.activity-collapsed .app-run-body {
  display: flex;
  align-items: center;
  gap: var(--space-r2);
}

.activity-collapsed .app-run-detail {
  display: none;
}

.activity-collapsed .app-run-line {
  order: -1;
  flex: none;
  font-size: inherit;
  color: var(--ink);
}

.activity-collapsed .app-run-title {
  flex: 1 1 auto;
  -webkit-line-clamp: 1;
  line-clamp: 1;
}

.activity-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-left: var(--space-r3);
}

.activity-title {
  font-weight: var(--w-title);
  font-size: var(--text-detail-body);
}

.activity-list {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  max-height: 24rem;
  max-height: min(50vh, 24rem);
  overflow-y: auto;
  overscroll-behavior: contain;
}
