/* --- Services bar (long-lived sidecar containers), and the jobs bar under it ---
   One rule for the two, because they are one shape: the same cards in the same
   place, and a job IS a service-shaped thing (see .job-card). What differs
   between them is only when they are shown, and that is written where it is
   decided — in the reading-mode block, not here.

   #jobs-bar had no base rule at all, and therefore no cap. Measured at 1440x900
   with the card at 61px: 6 jobs stood 366px tall, 12 stood 732, and at 20 the
   row was 1220 and put the top of #input-area at y=1268 inside an #chat-area of
   852 that clips — the box was off the screen and nothing could scroll to it.

   200px is the services number and it stays the services number: the cards are
   the same height, so it means the same thing for both — three of them, and then
   the row scrolls instead of the page. A row above the box is a strip you glance
   at; past three cards it has become the screen. */
#services-bar,
#jobs-bar {
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px 16px;
  background: var(--bg-surface);
  border-top: 1px solid var(--border);
  max-height: 200px;
  overflow-y: auto;
}

/* Above the box, a job is only worth the room while it is still going. A job
   that has ENDED already said so in the chat — flushJobNotices (src/jobs.ts)
   posts its result as a message — so the card is a second copy of something you
   have already been told, and one that keeps its height forever. The full list,
   finished ones included, is a click away on the jobs mark in the prompt row.
   The exception is written into the selector, and it is the same sentence read
   backwards: a card carrying a .job-notice is one whose result was NOT posted —
   held by a Stop, or parked after failed deliveries — so the premise for hiding
   it is false. Those stay until the chat has actually been told.
   This was written for the phone's reading mode first, on exactly this argument;
   the argument was never about the width, so it is not written as one — and it
   is now a DESKTOP rule in practice, because the phone has no jobs row left at
   all (see the phone block). The row itself follows: with nothing worth showing
   there is nothing to show, and an empty strip with a seam on it is worse than
   no strip.
   ALL OF THAT HOLDS WHILE NOBODY HAS ASKED. It is the reading rule and the
   default, and both stay. A person who turns the row on with the mark HAS asked,
   and the desktop block further down answers them — see
   `body.in-chat.bar-on-jobs`. Without that the mark counted the finished jobs
   (deliberately: it is the only way to one from inside a session) and opened
   onto a row these two rules had just emptied. */
#jobs-bar .job-card:not(:has(.svc-dot.running)):not(:has(.job-notice)) {
  display: none;
}
#jobs-bar:not(:has(.svc-dot.running)):not(:has(.job-notice)) {
  display: none !important;
}

/* Dashboard: all services across sessions, grouped by owner — and the jobs
   block below it, which is the same thing about a different noun.
   They line up with the CARDS above them, which is the column this screen is.
   Services used to be centred in a 900px measure of its own: harmless while it
   sat alone above everything, and plainly wrong once the two blocks stood one
   under the other — measured at 1280px, its heading started 56px to the right of
   the jobs heading. */
#dashboard-services,
#dashboard-jobs,
#dashboard-sessions,
#dashboard-turns {
  margin: 0 0 16px;
}

/* The rail: one column holding both lists, with a scrollbar of its own so that
   twenty jobs cost twenty jobs' worth of ITS height and nothing of the page's.
   Below the breakpoint it is not a rail at all — it is the two blocks back under
   the cards, in document order, and everything in here does nothing.

   1400 is measured, and the measurement is what makes it the right place to
   stop: with the rail beside them the cards hold three columns down to 1392px
   and two below that, while the SAME screen stacked gives them four. Under
   1400 the rail costs more than it saves, so it goes back where it was. */
@media (min-width: 1400px) {
  /* The page stops scrolling and hands its height to the two columns, which
     scroll separately. Without this the rail's height would be the page's and
     "its own scroll" would mean nothing. */
  #dashboard {
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  #dashboard-body {
    flex: 1;
    min-height: 0;
    flex-direction: row;
    align-items: stretch;
    gap: 16px;
  }
  #dashboard-cards {
    flex: 1;
    min-width: 0;
    overflow-y: auto;
    /* Rows keep their own height at the top of a column that is now taller than
       they are, instead of being stretched down it. */
    align-content: start;
  }
  #dashboard-rail {
    /* 400 is measured, and the measurement went differently from how it was
       framed. The card grid is `minmax(220px, 1fr)`, so the last column is never
       CUT at any rail width — every width yields a whole number of columns, and
       what changes is how far each one is stretched past the 220 it asked for.
       At 1440 every width from 240 to 448 leaves the same three columns, so the
       rail's width costs nothing in columns across that whole range; what it
       buys is the rail's own legibility. Measured at 1440, with a URL 205px
       long: a 216px rail (the only one that keeps FOUR columns) shows 37px of
       it — four characters and an ellipsis; 320 shows 141; 400 shows all of it.
       So: 400, which leaves 3/4/5 columns at 1440/1680/1920, stretched by
       16/14/13px each. 448 would stretch them by 0/2/3 — but it puts the cards
       at exactly their 220px minimum with nothing to give, so a classic 15px
       scrollbar in the cards column takes the third column away. */
    flex: 0 0 400px;
    width: 400px;
    overflow-y: auto;
    /* The last section's own bottom margin is the rail's bottom padding. */
    padding-right: 2px;
  }
  /* Whichever section is the bottom one — renderRail marks it, because which
     one that is depends on what is running. Its margin would otherwise dangle
     below the end of the column. */
  #dashboard-rail .rail-last { margin-bottom: 0; }
}

/* The folded half of a section: what has stopped or finished. `.archive-toggle`
   is the sidebar's, worn as-is; only the border it draws above itself has to go,
   because here it separates two halves of one list rather than the list from the
   thing after it. */
.dash-fold {
  border-top: none;
  padding: 6px 2px;
  margin-top: 0;
}
.dash-fold-body { margin-bottom: 10px; }

/* The rows a mark in the desktop row switches, AWAY BY DEFAULT: a session
   opens with the box near the chat and the counts saying what is behind each
   mark, and you put up the row you actually want to read. (The prompt buttons
   are the exception and are handled at the end of this block — they start on
   screen, so for them it is hiding that needs a rule.) The body class is what
   puts one up, written per session (see shownBars in app.js).
   Hidden without !important on purpose, so the two things that already have a
   say still win: `.hidden`, which each render writes on its own row when the row
   is EMPTY, and the dashboard's own rule — and the show rule below asks for
   `.in-chat`, so it cannot fight that one either.
   Desktop only, and that is not a detail: on a phone #pins-bar and #inbox-bar
   are gone by media query and #wakeups-bar is a row a phone keeps, so a setting
   made at a desk must not reach across and take it away where there is no mark
   to bring it back.
   The jobs row is the one exception and it is written just below the list: the
   rule further up thins it to what is running or undelivered, which is right for
   the row nobody asked for — but the mark that turns this one on is also the
   only way to a FINISHED job from inside a session, so a row turned on by hand
   shows all of them. Off, it behaves exactly as before. */
@media not all and (max-width: 768px) {
  #queue-bar, #pins-bar, #inbox-bar, #wakeups-bar, #services-bar, #jobs-bar,
  #owed-bar { display: none; }
  body.in-chat.bar-on-queue #queue-bar,
  body.in-chat.bar-on-owed #owed-bar,
  body.in-chat.bar-on-pins #pins-bar,
  body.in-chat.bar-on-notes #inbox-bar,
  body.in-chat.bar-on-wakes #wakeups-bar,
  body.in-chat.bar-on-services #services-bar { display: flex; }
  /* JOBS IS NOT IN THE LIST ABOVE, and needs two rules of its own. THE ROW
     SOMEBODY TURNED ON SHOWS ALL OF THIS SESSION'S JOBS, finished ones
     included. The hide-when-finished rules above are about the row NOBODY asked
     for; this mark is the only door to a finished job from inside a session, and
     a door that opens on an empty room is worse than no door.
     `!important` twice, and `:not(.hidden)` on the row for a reason that is not
     decoration: the rule it answers is `#jobs-bar:not(:has(…)):not(:has(…))`,
     which carries three classes to this selector's two, so with both marked
     important THAT one wins on specificity — measured, the row stayed away while
     its card came back. `.hidden` brings the count level, and it is the right
     word to add rather than a third class picked to win an arithmetic: renderJobs
     writes it when the session has no jobs at all, and a row with nothing in it
     must stay away however the switch is set. Desktop only, and that is the point
     rather than a caveat: the switch exists only in this row. On a phone the
     mark opens a list instead, and there is no row for it to switch — the phone
     block hides #jobs-bar outright now, and the note there says why. */
  body.in-chat.bar-on-jobs #jobs-bar:not(.hidden) { display: flex !important; }
  body.in-chat.bar-on-jobs #jobs-bar .job-card { display: flex !important; }
  /* The seventh switch, written the other way up because its default is the
     other way up: the prompt buttons are on screen until you say otherwise, so
     the rule that acts is the HIDE one and it asks for the class to be absent.
     The gear goes with the list — it IS the list's last button now, where it
     used to stay behind as the only door to the editor: the door lives in the
     room, and the mark that put the room away is one tap back. The selector
     speaks of the CLASS rather than the id, because the phone's #prompt-row
     holds the same list, gear and all — its own rule is in 07-prompts.css, and
     this block is desktop-only. */
  body.in-chat:not(.bar-on-prompts) .prompt-list { display: none; }
}

/* A switch that is ON. The ordinary mark is muted — that is every other mark in
   this row — so "the row is on screen" is the brighter one, and putting the row
   away returns the mark to the colour of its neighbours. The count is untouched
   in both states, which is the reason to hide a row at all: you keep knowing how
   many are in it. */
.composer-section.bar-showing { color: var(--text); }

/* A section mark asking to be looked at — today only the reports mark, when one
   of the reports behind it has stopped being delivered. --orange, not --red:
   red is reserved for something having gone wrong (see .mode-chip), and a report
   waiting for a person is not a fault, it is a thing to do. The count badge goes
   with it, so the two read as one signal rather than as a lit mark with a
   neutral number stuck to it. */
.composer-section.section-attention { color: var(--orange); }
.composer-section.section-attention[data-count]::after { background: var(--orange); }
/* Quietened: everything behind this mark is HELD — a person pressed Stop and the
   session's deliveries are paused until they write again. No colour of its own,
   because nothing is wrong and the person who silenced it is the person looking
   at it. The count goes muted instead, which is what --text-muted says
   everywhere else here: present, not active. The mark itself stays as it was, so
   this cannot be read as the disabled state. */
.composer-section.section-quiet[data-count]::after {
  background: var(--text-muted);
  color: var(--bg);
}

/* One queued thing per row — a subagent's report, a finished job's notice, a
   fired wake-up, a note — saying what it is, how long it has waited and whether
   anything is still trying. Read-only: see the dialog in index.html. */
.report-row {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 10px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
/* Which of the four this row is. Muted and small — the kind is what you scan
   past, the name is what you read — and fixed-width so four kinds down a column
   line their names up instead of stepping sideways. */
.owed-kind {
  flex: 0 0 58px;
  color: var(--text-muted);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* A line of its own under a held row: what to do about it. Full width, so it
   never competes for the first line with the facts above it, and only ever
   present on the one state that has an answer this short. */
/* The one action a row carries. At the right end, after the state, in the
   muted-until-hovered shape the other quiet ✕ buttons here wear (.svc-stop,
   .inbox-chip-remove): a queue is read far more often than it is emptied. */
.owed-drop {
  background: none;
  border: none;
  padding: 2px 4px;
  color: var(--text-muted);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  border-radius: 4px;
  transition: color 0.15s, background 0.15s;
}
@media (hover: hover) {
  .owed-drop:hover { color: var(--red); background: var(--bg-hover); }
}
/* What the relay said about the last dismissal, above the list it changed —
   including where a kept thing still is, which is the part worth copying. */
.owed-said {
  margin: 0 0 8px;
  padding: 8px 10px;
  background: var(--bg-surface2);
  border-radius: 6px;
  color: var(--text-muted);
  font-size: 12px;
  line-height: 1.45;
}
.owed-exit {
  flex: 0 0 100%;
  margin-left: 68px;
  color: var(--text-muted);
  font-size: 11px;
}
.report-row:last-child { border-bottom: none; }
/* The name of the thing: a nick, a job, a wake-up's prompt, a note's text. The
   last two have no length limit, so this one is allowed to shrink and clip
   rather than push the state and the clock off the row. */
.report-nick {
  font-weight: 600;
  color: var(--text);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.report-batch {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--text-muted);
}
/* Pushed to the right end together: the two facts you scan a queue for are how
   old the oldest is and whether anything is stuck. */
.report-when { margin-left: auto; color: var(--text-muted); font-family: var(--mono); font-size: 11px; }
.report-state { color: var(--text-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.04em; }
/* ONE vocabulary for the three things the relay can owe a session — a subagent
   report, a job's completion notice, a fired wake-up. They are drawn in three
   different shapes (a list row, a card, a chip) and the state has to read the
   same in all of them, so the colours live here once and each of the three wears
   the class.
   parked takes --orange, which in this app means look at this: nothing will
   deliver it until a person acts. held takes no colour at all — it is a mute
   somebody switched on, and the person looking at it is the person who did it —
   so it says so in the app's secondary colour and leans, which distinguishes it
   from the ordinary state without asking for anything. */
.state-parked { color: var(--orange); }
.state-held { color: var(--text-muted); font-style: italic; }
/* In the drawer it is an action you reached for, so it stands on its own line
   rather than trailing a heading. */
.dash-fold-body .dash-svc-clear { margin: 0 0 8px; }
.dash-svc-heading {
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  margin: 4px 0 8px;
}
/* Sits in the heading, reads as an action and not as a second title: the count
   beside it is the reason to press it. Muted until hovered — it removes rows,
   so it should not compete with the jobs themselves for attention. */
.dash-svc-clear {
  margin-left: 8px;
  padding: 0 6px;
  font: inherit;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 4px;
  cursor: pointer;
}
.dash-svc-clear:hover { color: var(--text); border-color: var(--text-muted); }

.dash-svc-group { margin-bottom: 10px; }
.dash-svc-session {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-muted);
  margin: 0 0 4px 2px;
}
.dash-svc-session.clickable { cursor: pointer; }
@media (hover: hover) { .dash-svc-session.clickable:hover { color: var(--accent); } }
#dashboard-services .svc-card,
#dashboard-sessions .svc-card,
#dashboard-turns .svc-card { margin-bottom: 4px; }
/* A session container's card has nothing to OPEN; the one thing the card itself
   does is go to its session, so it keeps .svc-card's hand only while that
   session is still there to go to. Its ✕ carries its own cursor either way — a
   button is pressable whether or not the row around it leads anywhere. */
.session-card:not(.clickable) { cursor: default; }
.svc-card {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  /* Lines packed, not spread. `stretch` is the default and it shares the card's
     spare height out between its lines — with a 17px name line above a 44px
     button line, that opened a hole between them. */
  align-content: center;
  gap: 8px;
  padding: 6px 10px;
  background: var(--bg-surface2);
  border: 1px solid color-mix(in srgb, var(--green) 30%, var(--border));
  border-radius: 6px;
  font-size: 13px;
  color: var(--text);
}
.svc-dot {
  flex-shrink: 0; width: 8px; height: 8px; border-radius: 50%;
  background: var(--text-muted);
}
.svc-dot.running { background: var(--green); }
.svc-dot.stopped { background: var(--red); }
/* The name gives way, and it is the only thing on the top line that can. It was
   flex-shrink: 0, which cost nothing while the line held a dot and a name and
   ran off the end; now the buttons are up here and a name has no ceiling (a
   service is whatever `agency-serve --name` was given, a session card wears the
   session's name). Measured in the 400px rail, on a 398px card: a 60-character
   name put the two buttons on a line of their own and took the card from 82.5
   to 120.5px — four lines, with the pair floating in the middle of the third.

   `flex-basis: 0` is what fixes that, and shrinking is not: a wrapping flex
   container breaks its lines on the items' HYPOTHETICAL sizes, before any
   shrinking is done, so `flex-shrink: 1` moved the wrap threshold by exactly
   0px — measured at five name lengths, the same answer to the pixel with it and
   without. A basis of 0 is never what breaks the line.

   And `max-width: max-content` is what keeps the basis of 0 from being a
   different bug. Growing freely, the name fills the line: "web" measured a 300px
   box instead of a 19.5px one, which is invisible on a service card and wrong on
   a job card, where it shoved the @nick 12px PAST the buttons it is nowhere
   near. Capped at its own content the name hugs its text exactly as it did, and
   the leftover space goes where it is wanted — .card-actions' auto margin.

   Ellipsis for what is left: past the cap the name is clipped with one, and the
   whole string is in the title, which this element already carried. */
.svc-name {
  flex: 1 1 0;
  min-width: 0;
  max-width: max-content;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  font-weight: 600;
}
/* Everything on a card that DOES something, held against the right of its top
   line — one group, the same place, on all three kinds of card: a service's
   restart and stop, a job's stop/dismiss, a session container's stop.
   They were on the foot before, where they shared a line with a reading that
   has no length limit (a URL, a command) and a disclosure mark, and the three
   card kinds each put them in a slightly different place in that queue. The top
   line holds a name and nothing else, so the right end of it is empty on every
   card — which is what makes one rule reach all three.
   `margin-left: auto` and not `justify-content`, because the card is the flex
   container here and its other lines must not be spread. */
.card-actions {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  /* The card's own gap. The two service buttons sat 8px apart on the foot and
     their coarse-pointer targets were sized against that number — see the
     ::after block below. */
  gap: 8px;
  flex-shrink: 0;
}
.svc-url {
  flex: 1; min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--accent); font-family: var(--mono); font-size: 11px;
  text-decoration: none;
}
@media (hover: hover) { .svc-url:hover { text-decoration: underline; } }
.svc-uptime {
  flex-shrink: 0; color: var(--text-muted); font-family: var(--mono); font-size: 11px;
}
.svc-restart, .svc-stop {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 13px; padding: 2px 6px; flex-shrink: 0; border-radius: 4px;
  transition: color 0.15s, background 0.15s;
  display: inline-flex; align-items: center; justify-content: center;
}
/* On a finger these were ~25x22 boxes holding a 10px mark, and the row around
   them opens the log on tap — so a miss did not do nothing, it opened the pane
   and looked like a button that does not work. 44px is the same floor every
   other touch target in this app is held to (see the coarse-pointer block by
   .saved-cap .msg-copy, 14-saved.css). */
@media (pointer: coarse) {
  /* The 44 is a TARGET, not a box. It used to be `min-height: 44px` on the button
     itself, and that height went straight into the layout: the button sat on a
     line of the card, so the line became a 44px band holding an 11px word, and
     the card went from 58 to 85 — two lines of text in the space of four, which
     is exactly what it looked like. A pointer being coarse is a fact about the
     finger, not about how big the drawing should be.
     So the box stays the size of its mark and an invisible ::after carries the
     target. -15px vertically takes the 14px box to 44, which is the dimension a
     thumb misses in and the one this is for. Horizontally only -2: the two
     service buttons sit 8px apart, and a target that reached across that gap
     would answer "restart" with "stop" — measured at -4 it already claimed the
     midpoint between them. 34 wide is what these had before any of this and
     nobody was missing them sideways.

     The buttons moved to the TOP line, so what is 15px below them is no longer
     open card: it is the right end of the foot, where the disclosure chevron
     sits. Measured on a phone (390 and 320 wide, coarse pointer): the target's
     bottom edge lands 1.0px inside the top of that 10px mark and 4.0px short of
     its centre, and elementFromPoint at the centre of every part of both card
     kinds still returns the part itself — the chevron, the URL, the uptime, each
     button. The shadow costs a pixel of a mark that is not a button anyway (the
     whole card opens the log); it does not cost the aim. */
  .svc-restart, .svc-stop { padding: 2px 10px; position: relative; }
  .svc-restart::after, .svc-stop::after {
    content: "";
    position: absolute;
    inset: -15px -2px;
  }
}
/* `logs` was a button on both cards until the cards themselves became the way
   to open one. Its rules went with it. */
@media (hover: hover) {
  .svc-restart:hover { color: var(--text); background: var(--bg-hover); }
  .svc-stop:hover { color: var(--red); background: rgba(239,83,80,0.1); }
}
/* --- Job cards ---
   The service card's shape, because a job IS that kind of thing: a container run
   that outlives the turn and has a state. Only what differs is written here. */
.job-card {
  /* Services border green because a service that exists is a service that
     serves. A job's border follows its own outcome instead — it is the row's
     loudest feature and it has four answers, not one. */
  border-color: color-mix(in srgb, var(--blue) 30%, var(--border));
}
.job-card:has(.svc-dot.ok) { border-color: color-mix(in srgb, var(--green) 30%, var(--border)); }
.job-card:has(.svc-dot.failed) { border-color: color-mix(in srgb, var(--red) 30%, var(--border)); }
.job-card:has(.svc-dot.stopped) { border-color: var(--border); }

/* A TURN's card wears the app's one word for "a turn is going" — the drifting
   orange every session dot, subagent dot and bar section already says it in (see
   .session-item .dot.running, which this joins rather than copies). It is the
   whole reason this is a block of its own and not a line under Sessions: green
   there means a container is ALIVE while nobody is talking to it, and orange
   here means somebody is being answered right now. Two different costs, and the
   panel exists to tell them apart.
   Scoped inside .turn-card, because `.svc-dot.running` is green for a service
   and that is a different sentence about a different noun.
   ONLY THE FALLBACK REACHES THIS NOW: a roster card whose session is known wears
   that session's own dot instead (.svc-card .dash-dot in 04-dashboard.css, which
   is the same orange by the same token), and .svc-dot is left for the card whose
   session is gone — where "the container is running" is all that is true. */
.turn-card .svc-dot.running {
  background: var(--dot-run);
  animation: dot-breathe 1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
  .turn-card .svc-dot.running { animation: none; }
}
/* Blue-bordered like a running job, and for the same reason: what this row is
   about is work in flight, not a service that exists. */
.turn-card { border-color: color-mix(in srgb, var(--blue) 30%, var(--border)); }
/* Scoped inside .job-card, all four of them. `.svc-dot.running` and
   `.svc-dot.stopped` already mean something for a SERVICE — up and down, green
   and red — and a job's four outcomes are not that pair. Unscoped these would
   have repainted every service card on the screen.
   Blue and steady for a running one: a job that is working is not asking for
   anything, and the elapsed time beside it is what tells a live one from a
   wedged one. Blue and NOT the green the session dots now use, because green is
   already this row's word for "finished successfully" — a job says how it ENDED,
   an agent says whether it is going, and only one of those vocabularies has a
   past tense to collide with. */
.job-card .svc-dot.running { background: var(--blue); }
.job-card .svc-dot.ok { background: var(--green); }
.job-card .svc-dot.failed { background: var(--red); }
/* Neither green nor red on purpose: somebody pressed stop, and that is neither
   a success to celebrate nor a failure to chase. */
.job-card .svc-dot.stopped { background: var(--text-muted); }
.job-owner,
.svc-owner {
  flex-shrink: 0;
  font-size: 11px;
  font-family: var(--mono);
  color: var(--text-muted);
}
/* The same mark on a session or turn card, where it names a subagent AND its
   lead — too long to share the top line with a name and the ✕, so it takes a
   line of its own under them, clipped rather than wrapped. */
.svc-owner {
  flex-basis: 100%;
  min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The image a service or job runs on, when it is not the agency image: a line
   of its own between what it is and what it is doing, in the same muted mono
   as a job's command — but always shown, because on these cards it is part of
   what the thing IS (postgres:17 is not "a service"). One line, clipped. */
.card-image {
  flex-basis: 100%;
  min-width: 0;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  color: var(--text-muted); font-family: var(--mono); font-size: 11px;
}
/* The command takes the room the service card gives its URL, and for the same
   reason: it is the only thing that says WHAT is running. */
/* The command is detail, not identity. A row says which job this is (its name)
   and how it is going (its state, its clock); the command is what you read when
   you want to know what it actually runs — and it is the one field with no
   bound on its length. Ellipsised into the row it was a third of a sentence
   taking half the width; on the jobs page, wrapped, it was three lines of the
   four in every card. So it waits for the card to be opened, where it has a
   line of its own and can break wherever it must (paths and URLs have no
   spaces to break at). */
.job-cmd { display: none; }
.job-card.expanded .job-cmd {
  display: block;
  flex-basis: 100%;
  order: 9;
  min-width: 0;
  white-space: normal;
  overflow: visible;
  overflow-wrap: anywhere;
  color: var(--text-muted); font-family: var(--mono); font-size: 11px;
}
/* The second line of a job card and of a service card: what it is doing. What
   can be DONE about it used to be here too and is not — the buttons went to the
   top line, where they are the same three pixels from the right on every kind of
   card (see .card-actions). What is left is a reading line. */
.job-foot,
.svc-foot {
  flex-basis: 100%;
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
.job-state { flex-shrink: 0; font-size: 11px; }
/* Second line: the reading at the left — the outcome, the clock, a URL — and the
   disclosure mark alone at the right. The auto margin sits on the mark and not
   on the state, because a one-line card wants its right-hand end held over there
   and the state must stay beside the name it belongs to.
   The mark now sits directly under .card-actions on the line above, which is a
   column of right-hand things and reads as one: press the top of it, open with
   the rest. Turned 90 degrees while the card is open, the same way every other
   disclosure in this app reports itself. */
.job-foot .job-more,
.svc-foot .job-more {
  margin-left: auto;
  display: inline-flex;
  align-items: center;
  color: var(--text-muted);
  transition: transform 0.15s;
}
.job-card.expanded .job-more,
.svc-card.expanded .job-more { transform: rotate(90deg); }
/* One thing to open, and the whole card opens it — same as a job's. */
.svc-card { cursor: pointer; }
/* The URL keeps its own cursor: it is a link, and it goes somewhere else. */
.svc-url { cursor: pointer; }
/* One thing to open, and the whole row opens it. */
.job-card { cursor: pointer; }
.job-state-running { color: var(--blue); }
.job-state-ok { color: var(--green); }
.job-state-failed { color: var(--red); }
.job-state-stopped { color: var(--text-muted); }
/* The completion notice's own state, beside how the job ended. Its colour comes
   from the shared .state-* pair — see them for why held and parked look the way
   they do. */
.job-notice {
  flex-shrink: 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.job-card .run-time { opacity: 1; }
.job-stop:disabled { opacity: 0.3; cursor: default; }
.job-logs {
  flex-basis: 100%;
  margin: 4px 0 0;
  /* After the command, which is order 9. What the job RUNS explains the log
     under it; the other way round the log arrived before the sentence it was
     the answer to. */
  order: 10;
}
/* Says out loud that this is a snapshot. `job.logs` answers once and never
   again, and a pane that looked live would have you watching a frozen tail
   believing the container had gone quiet. */
.job-logs-head {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 10px;
  color: var(--text-muted);
  font-style: italic;
  margin-bottom: 2px;
}
.job-logs-note { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.job-logs-refresh {
  background: none; border: none; color: var(--text-muted); cursor: pointer;
  font-size: 12px; padding: 2px 6px; border-radius: 4px; flex-shrink: 0;
}
@media (hover: hover) { .job-logs-refresh:hover { color: var(--text); background: var(--bg-hover); } }
.job-logs-body {
  margin: 0;
  padding: 8px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--mono); font-size: 11px;
  white-space: pre-wrap; word-break: break-word;
  max-height: 220px; overflow-y: auto;
  color: var(--text-muted);
}

.svc-logs {
  flex-basis: 100%;
  margin: 4px 0 0;
  padding: 8px;
  background: var(--bg-base, #0d0d0d);
  border: 1px solid var(--border);
  border-radius: 4px;
  font-family: var(--mono); font-size: 11px;
  white-space: pre-wrap; word-break: break-word;
  max-height: 220px; overflow-y: auto;
  color: var(--text-muted);
}

