/*
 * ds/data.css — tables, queue rows, roll-ups, diffs, run logs, timelines, stats,
 * key-value lists, folds.
 *
 * Stripe's density discipline: the DATA is tight, the CHROME around it is
 * generous. Numbers are tabular-nums everywhere so columns of figures line up --
 * proportional digits in a cost column make two numbers of the same magnitude
 * look different lengths.
 */

/* =========================== 1. Table ============================ */
/* `overflow: hidden` was here to clip the corners against the radius, and it clipped
   the table too: a five-column table in a 356px wrapper simply lost its last 176px, with
   no scrollbar and nothing to say anything was missing. `overflow-x: auto` clips the
   corners just as well and hands the columns back on a phone. `-webkit-overflow-scrolling`
   for momentum on iOS. */
.ds-table-wrap {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    overflow-x: auto;
    overflow-y: hidden;
    -webkit-overflow-scrolling: touch;
    background: var(--surface);
}
.ds-table { width: 100%; border-collapse: collapse; font-size: var(--fs-dense); }
.ds-table th, .ds-table td { padding: var(--sp-2) var(--sp-3); text-align: left; border-bottom: 1px solid var(--line); min-width: 0; }
.ds-table thead th {
    background: var(--panel-bg);
    font-size: var(--fs-micro);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.05em;
    text-transform: uppercase;
    color: var(--mute);
    white-space: nowrap;
    position: sticky; top: 0; z-index: var(--z-sticky);
}
.ds-table tbody tr:last-child td { border-bottom: 0; }
.ds-table tbody tr:hover { background: var(--sunken); }
.ds-table-num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
.ds-table-mono { font-family: var(--font-mono); font-size: var(--fs-meta); }
.ds-table--fixed { table-layout: fixed; }
/* `table-layout: fixed` honours the declared widths and then lets the content run out
   of the cell regardless. A path and a symbol name are unbreakable monospace, so
   without this the Symbol column prints over Why. Breaking anywhere is right for a
   path: there is no word boundary to prefer. */
.ds-table--fixed td, .ds-table--fixed th { overflow-wrap: anywhere; }

/* =========================== 2. Queue row ======================== */
/*
 * The row the lead reads most. Fixed-width columns on the right so a column of
 * rows scans as a column rather than as ragged text.
 */
.ds-list { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.ds-list > * + * { border-top: 1px solid var(--line); }

.ds-qrow {
    display: grid;
    grid-template-columns: 18px minmax(0, 1fr) auto auto auto auto;
    align-items: center;
    gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    font-size: var(--fs-dense);
    transition: background var(--dur-fast) var(--ease-out);
    min-width: 0;
}
.ds-qrow:hover { background: var(--sunken); text-decoration: none; }
.ds-qrow-state { display: flex; justify-content: center; flex: none; }
.ds-qrow-main { min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ds-qrow-title { font-weight: var(--fw-medium); color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-qrow-meta { font-size: var(--fs-meta); color: var(--mute); display: flex; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; }
.ds-qrow-key { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--soft); flex: none; }
.ds-qrow-repos { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--mute); white-space: nowrap; flex: none; }
.ds-qrow-elapsed,
.ds-qrow-cost { font-variant-numeric: tabular-nums; font-size: var(--fs-meta); color: var(--mute); white-space: nowrap; text-align: right; flex: none; }
.ds-qrow-owner { flex: none; }

/* Unowned work on a shared queue. Your decision made the queue project-wide, so
   a row with no owner is the thing two people are about to duplicate. */
.ds-owner {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    font-size: var(--fs-micro);
    color: var(--mute);
    white-space: nowrap;
}
.ds-owner--nobody { color: var(--st-waiting-ink); }
.ds-owner--me { color: var(--accent-text); font-weight: var(--fw-medium); }
.ds-avatar {
    width: 18px; height: 18px; flex: none;
    border-radius: var(--radius-full);
    background: var(--accent-soft);
    color: var(--accent-text);
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--fs-micro);
    font-weight: var(--fw-semibold);
}

/* =========================== 3. Roll-up row ====================== */
/*
 * A container -- a Jira epic, a Linear initiative. Work happens at the leaf, so
 * this row cannot be run: it has no action, only an expand. Indented children
 * make the relationship structural rather than something the user infers.
 */
.ds-rollup { background: var(--panel-bg); }
.ds-rollup-head {
    display: grid;
    grid-template-columns: 18px minmax(0, 1fr) auto auto;
    align-items: center; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    width: 100%;
    font-size: var(--fs-dense);
    text-align: left;
}
.ds-rollup-head:hover { background: var(--sunken); }
.ds-rollup-mark { color: var(--mute); font-size: var(--fs-micro); transition: transform var(--dur-fast) var(--ease-out); }
.ds-rollup[open] .ds-rollup-mark,
.ds-rollup-head[aria-expanded="true"] .ds-rollup-mark { transform: rotate(90deg); }
.ds-rollup-count { font-size: var(--fs-meta); color: var(--mute); font-variant-numeric: tabular-nums; white-space: nowrap; }
.ds-rollup-kids > .ds-qrow { padding-left: var(--sp-10); background: var(--surface); }

/* =========================== 4. Fold ============================= */
.ds-fold { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.ds-fold + .ds-fold { margin-top: var(--sp-3); }
.ds-fold-head {
    display: flex; align-items: center; gap: var(--sp-2);
    width: 100%;
    padding: var(--sp-3) var(--sp-4);
    font-size: var(--fs-dense);
    font-weight: var(--fw-medium);
    text-align: left;
    background: var(--panel-bg);
}
.ds-fold-head:hover { background: var(--sunken); }
.ds-fold-mark { color: var(--mute); font-size: var(--fs-micro); flex: none; transition: transform var(--dur-fast) var(--ease-out); }
.ds-fold-head[aria-expanded="true"] .ds-fold-mark { transform: rotate(90deg); }
.ds-fold-head > .ds-chip { margin-left: auto; flex: none; }
.ds-fold-body { padding: var(--sp-4); border-top: 1px solid var(--line); }
.ds-fold-body[hidden] { display: none !important; }
.ds-fold--flush > .ds-fold-body { padding: 0; }

/* =========================== 5. Key / value ====================== */
.ds-kv { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: var(--sp-2) var(--sp-4); font-size: var(--fs-dense); }
.ds-kv dt { font-size: var(--fs-meta); color: var(--mute); white-space: nowrap; }
.ds-kv dd { margin: 0; min-width: 0; }
/* LABEL ABOVE, NOT BESIDE. A two-column term list is right for short facts and wrong
   for a row whose value is a card: "By the agent" in a narrow left column left the
   document itself starting a third of the way across the report, under nothing. */
.ds-kv--stacked { grid-template-columns: minmax(0, 1fr); gap: var(--sp-1); }
.ds-kv--stacked dt { margin-top: var(--sp-2); }
.ds-kv--stacked > dt:first-child { margin-top: 0; }

/* EVERY TERM LIST IS THE STACKED ONE ON A PHONE.
   The reason `--stacked` exists at all -- "a two-column term list is right for short
   facts and wrong for a row whose value is a card" -- is true of every value once the
   screen is 320px: the `max-content` label column took all but 75px on the Tracker tab
   and the sentence beside it painted out of what was left. The same declarations rather
   than new ones, so the two cannot drift apart. */
@media (max-width: 680px) {
    .ds-kv { grid-template-columns: minmax(0, 1fr); gap: var(--sp-1); }
    .ds-kv dt { margin-top: var(--sp-2); }
    .ds-kv > dt:first-child { margin-top: 0; }
}

/* =========================== 6. Stat tile ======================== */
.ds-stat { display: flex; flex-direction: column; gap: var(--sp-1); padding: var(--sp-4); background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-md); min-width: 0; }
.ds-stat-k { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.05em; color: var(--mute); }
.ds-stat-v { font-size: var(--fs-h2); font-weight: var(--fw-semibold); font-variant-numeric: tabular-nums; line-height: var(--lh-tight); }
.ds-stat-sub { font-size: var(--fs-meta); color: var(--mute); }
/* A tile with no value. Not a zero. */
.ds-stat--unknown .ds-stat-v { font-size: var(--fs-lead); font-weight: var(--fw-regular); color: var(--st-unknown-ink); font-style: italic; }

/* =========================== 7. Diff ============================= */
.ds-diff { border: 1px solid var(--line); border-radius: var(--radius-md); overflow: hidden; background: var(--surface); font-family: var(--font-mono); font-size: var(--fs-meta); }
.ds-diff-head {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--panel-bg);
    border-bottom: 1px solid var(--line);
    font-family: var(--font-ui);
    font-size: var(--fs-meta);
}
.ds-diff-path { font-family: var(--font-mono); color: var(--soft); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
.ds-diff-head > .ds-diff-stat { margin-left: auto; flex: none; font-variant-numeric: tabular-nums; }

.ds-diff-body { overflow-x: auto; }
.ds-diff-line { display: grid; grid-template-columns: 48px 1fr; line-height: var(--lh-snug); }
.ds-diff-no { padding: 0 var(--sp-2); text-align: right; color: var(--diff-gutter); user-select: none; border-right: 1px solid var(--line); font-variant-numeric: tabular-nums; }
.ds-diff-text { padding: 0 var(--sp-3); white-space: pre; }
.ds-diff-line--add { background: var(--diff-add-bg); }
.ds-diff-line--add .ds-diff-text { color: var(--diff-add-ink); }
.ds-diff-line--del { background: var(--diff-del-bg); }
.ds-diff-line--del .ds-diff-text { color: var(--diff-del-ink); }
.ds-diff-line--hunk { background: var(--sunken); }
.ds-diff-line--hunk .ds-diff-text { color: var(--mute); }

/* =========================== 8. Run log ========================== */
/*
 * GitHub's January 2026 session log, which is the state of the art: tool calls
 * grouped, output previewed inline, diffs expandable in place, bash visible.
 */
.ds-log { border: 1px solid var(--line); border-radius: var(--radius-md); background: var(--surface); overflow: hidden; }
.ds-log-step { border-bottom: 1px solid var(--line); }
.ds-log-step:last-child { border-bottom: 0; }
.ds-log-head {
    display: flex; align-items: center; gap: var(--sp-2);
    width: 100%; padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-dense); text-align: left;
}
.ds-log-head:hover { background: var(--sunken); }
.ds-log-icon { flex: none; width: 16px; text-align: center; font-size: var(--fs-meta); color: var(--mute); }
.ds-log-what { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-log-head > .ds-log-time { margin-left: auto; flex: none; font-size: var(--fs-micro); color: var(--mute); font-variant-numeric: tabular-nums; }
.ds-log-out { padding: var(--sp-2) var(--sp-3) var(--sp-3) var(--sp-8); font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--soft); background: var(--sunken); white-space: pre-wrap; overflow-x: auto; }
.ds-log-cmd { font-family: var(--font-mono); font-size: var(--fs-meta); color: var(--ink); }

.ds-log-step--pass .ds-log-icon { color: var(--pass); }
.ds-log-step--fail .ds-log-icon { color: var(--fail); }
.ds-log-step--run  .ds-log-icon { color: var(--st-running-ink); }
.ds-log-step--wait { background: var(--st-waiting-bg); }
.ds-log-step--wait .ds-log-icon { color: var(--st-waiting-ink); }

/* The steer box. GitHub's pattern: a correction typed mid-run, applied after the
   current tool call. We can stop and ask; we cannot yet correct in place. */
.ds-steer { display: flex; gap: var(--sp-2); padding: var(--sp-3); border-top: 1px solid var(--line); background: var(--panel-bg); }
.ds-steer .ds-input { flex: 1 1 auto; }
.ds-steer-note { padding: 0 var(--sp-3) var(--sp-3); font-size: var(--fs-micro); color: var(--mute); background: var(--panel-bg); }

/* =========================== 9. Timeline ========================= */
/*
 * Phase 1's rule made visible: things that HAPPEN are rows here; only things
 * that PERSIST get a URL. Runs, approvals, reviews, deployments live here.
 */
.ds-timeline { display: flex; flex-direction: column; padding-left: var(--sp-4); }
.ds-tl-item { display: grid; grid-template-columns: 20px minmax(0, 1fr); gap: var(--sp-3); position: relative; padding-bottom: var(--sp-4); }
.ds-tl-item:not(:last-child)::before {
    content: ""; position: absolute; left: 9px; top: 16px; bottom: 0;
    width: 1px; background: var(--line);
}
.ds-tl-dot {
    width: 9px; height: 9px; margin: 5px auto 0;
    border-radius: var(--radius-full);
    background: var(--line-firm);
    z-index: 1;
    flex: none;
}
.ds-tl-item--pass .ds-tl-dot { background: var(--pass); }
.ds-tl-item--fail .ds-tl-dot { background: var(--fail); }
.ds-tl-item--wait .ds-tl-dot { background: var(--st-waiting-ink); }
.ds-tl-body { min-width: 0; font-size: var(--fs-dense); }
.ds-tl-when { font-size: var(--fs-micro); color: var(--mute); font-variant-numeric: tabular-nums; }

/* =========================== 10. Basis =========================== */
/*
 * What an answer rests on, and what it does not. P3: "not examined" is as
 * prominent as "examined", and this strip is why an analysis over a nine-day-old
 * index cannot present itself as an analysis over a fresh one.
 */
.ds-basis { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); padding: var(--sp-3) var(--sp-4); background: var(--panel-bg); border: 1px solid var(--line); border-radius: var(--radius-md); font-size: var(--fs-meta); }
.ds-basis-k { color: var(--mute); }
.ds-basis-v { font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; }
.ds-basis-gap { color: var(--st-waiting-ink); }

/* =========================== 11. Work row ======================== */
/*
 * The queue row as Work uses it: a select box, the issue, its repository, its
 * state, its owner, and links out. `ds-qrow-main` is an <a> inside a row that
 * also holds checkboxes and external links, so the link is the CELL, not the
 * row -- a row-wide anchor would swallow the checkbox.
 */
.ds-qrow--work {
    /* A seventh column for what the funnel says this ticket touches, and an eighth for
       the two dates. Both sized, not `auto`: each holds a number and a word, and
       letting them size themselves made every row in the list a different shape. */
    grid-template-columns:
        18px minmax(0, 1fr) minmax(0, auto) 184px auto 76px auto auto;
}

/* WHEN IT WAS RAISED, AND WHEN WE PULLED IT IN. Stacked rather than side by side: at
   this width two relative durations on one line read as one mangled number, and the
   pair is scanned down a column of forty rows, not across. The second is quieter
   because the first is the one a backlog is worked by. */
.ds-qrow-dates {
    display: flex; flex-direction: column; gap: 1px;
    font-size: var(--fs-meta); font-variant-numeric: tabular-nums;
    line-height: 1.25; min-width: 0;
}
.ds-qrow-date { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.ds-qrow-date--soft { color: var(--mute); font-size: var(--fs-micro); }

/* THE HEADING ROW. Not a `<th>` anywhere on this screen -- the list is a grid of divs --
   so it is one more row in the same grid, which is also what keeps it aligned when a
   breakpoint changes the columns underneath it. */
.ds-qrow--head {
    font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.04em;
    font-weight: var(--fw-semibold); color: var(--mute);
    padding-top: 0; padding-bottom: var(--sp-1);
    border-bottom: 1px solid var(--line); background: none;
}
.ds-qrow--head:hover { background: none; }
/* THE HEADINGS STACK, because the values do. Side by side they are two words over one
   76px column -- they overflowed into Owner, and "RAISED IMPORTED OWNER" ran together
   as one unreadable line. Stacked, each heading sits directly above the duration it
   names, which is the only arrangement that explains which number is which. */
.ds-qrow--head .ds-qrow-dates { white-space: nowrap; }
.ds-qrow--head .ds-qrow-dates > span:last-child { color: var(--mute); }

/* The sort control sits in the toolbar beside the search, and must not stretch. */
.ds-sortby { display: inline-flex; align-items: center; gap: var(--sp-2); flex: none; }

/* ------------------------------------------------------------------------------
   A RUN THAT HAS STARTED AND WRITTEN NOTHING YET.

   The minute between pressing Approve and the first log line is real work -- pod
   scheduled, image pulled, repository cloned -- and none of it writes a row, because
   the harness that writes rows is what is being started. The screen said "Nothing
   recorded yet" through all of it, over a header that said the agent was working.

   So this is deliberately NOT the `.ds-empty` treatment. An empty state is a shrug;
   this is a report. The four lines name the four things happening, the first one
   spinning, so the reader can see that the silence is a stage and not a stall.
   ------------------------------------------------------------------------------ */
.ds-runstarting {
    display: flex; gap: var(--sp-3); align-items: flex-start;
    padding: var(--sp-5) var(--sp-4);
}
.ds-runstarting-mark { color: var(--st-running-ink); font-size: 18px; line-height: 1.4; flex: none; }
.ds-runstarting-title { font-size: var(--fs-body); font-weight: var(--fw-semibold); margin: 0 0 var(--sp-1); }
.ds-runboot {
    list-style: none; margin: var(--sp-3) 0 0; padding: 0;
    display: flex; flex-direction: column; gap: var(--sp-2);
    font-size: var(--fs-meta); color: var(--mute);
}
.ds-runboot li { display: flex; align-items: center; gap: var(--sp-2); }
.ds-runboot li:first-child { color: var(--ink); }
.ds-runboot i { width: 12px; text-align: center; }
.ds-runboot li:first-child i { color: var(--st-running-ink); }

/* How long it has been going, inside the Running chip. Tabular so the width does not
   jitter every second as the digits change. */
.ds-runclock {
    margin-left: var(--sp-2); font-variant-numeric: tabular-nums;
    opacity: 0.85; font-weight: var(--fw-regular);
}
/* WHAT A TICKET TOUCHES, read in a list of forty. One line, numbers emphasised, the
   confidence last and quiet -- it qualifies the counts rather than competing with
   them. */
.ds-qrow-scope { font-size: var(--fs-meta); min-width: 0; }
.ds-scope { display: inline-flex; align-items: baseline; gap: var(--sp-1); flex-wrap: wrap; }
.ds-scope strong { font-variant-numeric: tabular-nums; }
.ds-scope-conf {
    font-size: var(--fs-micro);
    text-transform: uppercase;
    letter-spacing: 0.04em;
    font-weight: 600;
    color: var(--mute);
}
/* `low` is the one worth a colour, for the reason the row-level confidence is: it is
   the answer a reader would otherwise skim. `scoping.confidence_of` caps any ticket
   with an `unknown` repository at low, so this is also the marker for "something here
   nobody could decide about".

   ALL THREE ARE DECLARED, even though two of them say "stay as you were".
   `scoping.confidence_of` returns high | medium | low and the row writes
   `ds-scope--{{ confidence }}`, so leaving the other two undeclared meant two of the
   three values a real ticket can carry resolved to NO RULE -- which is not the same as
   resolving to the default, it is a class the stylesheet does not know about. Found by
   checking a live Work queue in a browser at three widths; the suite's own fixtures
   only ever produced `low`. */
.ds-scope--low .ds-scope-conf { color: var(--st-waiting-ink); }
.ds-scope--medium .ds-scope-conf,
.ds-scope--high .ds-scope-conf { color: var(--mute); }

@media (max-width: 1180px) {
    /* The scope folds under the name rather than squeezing the summary, same rule the
       analysis row follows. */
    .ds-qrow--work { grid-template-columns: 18px minmax(0, 1fr) auto auto auto; }
    .ds-qrow--work .ds-qrow-scope { grid-column: 2 / -1; }
    /* The heading row cannot describe a layout that has reflowed under it, and a
       heading over the wrong column is worse than none. */
    .ds-qrow--head { display: none; }
}
.ds-qrow--work .ds-qrow-main { color: inherit; }
.ds-qrow--work .ds-qrow-main:hover { text-decoration: none; }
.ds-qrow--work .ds-qrow-main:hover .ds-qrow-title { color: var(--accent-text); }
.ds-qrow-chip  { flex: none; }
.ds-qrow-links { display: flex; align-items: center; gap: var(--sp-1); flex: none; }
.ds-qrow[hidden] { display: none !important; }

/* A query echoed back so it can be read and copied without opening the editor.
   It is the thing you change when nothing matched, so it has to be legible. */
.ds-query-echo {
    width: 100%;
    margin: 0;
    padding: var(--sp-3);
    background: var(--sunken);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    font-size: var(--fs-meta);
    white-space: pre-wrap;
    word-break: break-word;
    text-align: left;
    color: var(--soft);
}
.ds-query-echo code { background: none; border: 0; padding: 0; white-space: pre-wrap; }

@media (max-width: 900px) {
    /* Repository and owner are the first things to go: the state and the title
       are what the row is for. The dates go with them -- sorting by them still works,
       and the result of the sort is the order of the rows, which is visible without
       the column. */
    .ds-qrow--work { grid-template-columns: 18px minmax(0, 1fr) auto auto; }
    .ds-qrow--work .ds-qrow-repos,
    .ds-qrow--work .ds-qrow-dates,
    .ds-qrow--work .ds-qrow-owner { display: none; }

    /* THE STATE CHIP WAS LANDING IN THE 18px STATE COLUMN.
       `.ds-qrow-scope` is placed by hand at `2 / -1`, so it fills the second row and
       auto-placement puts the next two items -- the chip and the links -- at the start
       of a THIRD row: the chip into column 1, which is 18px wide and holds the state
       dot, and the links into column 2 beside it. The chip is `overflow: visible` and
       its content is 75px, so "Pulled in" and "Analysed" were painted straight across
       the action links on every row of the queue, with no clipping and no scrollbar to
       show anything was wrong.

       Placed by hand instead, beside each other under the title, which is where they
       were meant to fold to. */
    .ds-qrow--work .ds-qrow-chip {
        grid-column: 2;
        justify-self: start;
    }
    .ds-qrow--work .ds-qrow-links {
        grid-column: 3 / -1;
        justify-self: start;
    }
}

/* =========================== 12. Criteria ======================== */
/*
 * An acceptance criterion with its check. Whether a criterion has a RUNNABLE check
 * is the fact that matters -- "passes review" and "returns 402 twice" are not the
 * same promise -- so the mark is a state, not a bullet.
 */
.ds-criteria { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-2); }
.ds-criterion { display: grid; grid-template-columns: 16px minmax(0, 1fr); gap: var(--sp-2); align-items: start; font-size: var(--fs-dense); }
.ds-criterion-mark { padding-top: 3px; }
.ds-criterion-text { min-width: 0; }
.ds-criterion-text code { display: inline-block; margin-top: var(--sp-1); white-space: pre-wrap; }

.ds-plain { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--sp-1); min-width: 0; }
.ds-plain > li { min-width: 0; }

/* ======================================================================
   13. The repository queue, the funnel chips, and the conversation.
       Added for the Analysis rebuild.
   ====================================================================== */

/* Column labels for a list of rows. A `<table>` would be honest about the columns and
   wrong about everything else -- the rows carry a chip, a two-line cell and a control,
   and a table cell cannot be a flex container without fighting the layout. So: one
   grid, declared twice, and the header is aria-hidden because each row already names
   its own parts. */
.ds-qhead {
    display: grid; align-items: center; gap: var(--sp-3);
    padding: 0 var(--sp-3) var(--sp-1);
    font-size: var(--fs-micro);
    text-transform: uppercase; letter-spacing: 0.05em;
    color: var(--mute);
}
/* DECLARED WIDTHS, not `max-content`, because the header and the rows are two
   separate grids and `max-content` sizes each to its own contents -- so the column
   labels drifted left of the cells they label, which is worse than no labels. Subgrid
   would say this once; it is not safe to rely on yet. */
/* Verdict · repository · branch · decision · chevron. `Review` left: it said
   `Needs review` beside a cell saying `not decided`, which is one fact in two
   vocabularies. `Report ›` left: the whole row opens the report, and a link doing
   what the row does teaches a reader that the row does not. */
.ds-qhead--repo, .ds-qrow--repo {
    grid-template-columns: 140px minmax(0, 1fr) 140px 200px 20px;
}
.ds-qrow--repo { display: grid; align-items: center; gap: var(--sp-3); }
/* Chips left-align in their column. `.ds-qrow-state` centres its chip, which is right
   for the 18px dot column of the work queue and wrong here: `Cannot tell` and `Changes
   needed` are different widths, so centring put them on different left edges and the
   one column whose whole job is to be scanned down could not be. */
.ds-qrow--repo .ds-qrow-state {
    justify-self: start;
    /* TWO LINES NOW: the verdict, and how good the evidence under it is. A column,
       because `Changes needed` plus `medium` on one line is 190px and this column is
       140 -- and the confidence is a qualifier on the verdict, so it reads under it
       rather than beside it. `align-items` keeps both on the same left edge, which is
       the whole reason this column stopped being centred. */
    flex-direction: column;
    align-items: flex-start;
    justify-content: center;
    gap: 2px;
}
/* HOW GOOD THE EVIDENCE IS. Deliberately not a `.ds-chip`: a second chip under the
   verdict chip reads as a second verdict, and this is a footnote to the first one.
   Uppercase and small, in the state colour the word earns. */
.ds-qrow-conf {
    font-size: var(--fs-micro);
    line-height: 1.2;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    font-weight: 600;
    color: var(--mute);
}
/* `low` is the one that has to carry a warning, because it is the one a reader would
   otherwise skip -- a weak case under a confident-sounding verdict is the failure this
   whole column exists to prevent. `high` stays quiet: it is the normal case and
   colouring it green invites the reader to stop reading. */
.ds-qrow-conf--low { color: var(--st-waiting-ink); }
.ds-qrow-conf--medium { color: var(--mute); }
.ds-qrow-conf--high { color: var(--mute); }
.ds-qrow--repo .ds-qrow-main {
    display: flex; flex-direction: column; gap: 1px;
    text-align: left; min-width: 0;
}
.ds-qrow--repo .ds-qrow-main:hover .ds-qrow-title { color: var(--accent-text); }
.ds-qrow--repo .ds-qrow-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
.ds-qrow-cell { font-size: var(--fs-meta); min-width: 0; }
/* A cell holding a control. The row's click handler reads this class to leave the
   control alone, and the cursor says so before the click does. */
.ds-qrow-ctl { cursor: default; display: flex; align-items: center; gap: var(--sp-2); }
/* `flex` rather than `width: 100%`. The cell now holds the select plus up to three
   markers -- stale, saved, questions -- and a select at 100% of the cell leaves them
   overflowing it. */
.ds-qrow-ctl > .ds-select { flex: 1 1 auto; width: auto; min-width: 0; }
/* THAT THERE ARE OPEN QUESTIONS, on the control they bear on. Icon only, with the count
   and the text on `title` and in `.ds-sr`: three questions of prose in a 200px cell is a
   paragraph in a table, and the questions themselves are in the report and at the gate. */
.ds-qrow-asks { flex: none; color: var(--st-waiting-ink); line-height: 1; }
.ds-qrow--repo { cursor: pointer; }
.ds-qrow-go { color: var(--mute); justify-self: end; font-size: var(--fs-lead); line-height: 1; }
.ds-qrow--repo:hover .ds-qrow-go { color: var(--accent-text); }
/* The ruling the analysis has since contradicted -- all that is left of the review
   column, on the control it is about. */
.ds-qrow-stale { flex: none; }
.ds-qrow-stale[hidden] { display: none !important; }
/* The receipt for a write nobody pressed a button for. */
.ds-qrow-saved { font-size: var(--fs-meta); color: var(--pass); flex: none; }
.ds-qrow-saved[hidden] { display: none !important; }
/* The branch button fills its column and truncates rather than widening the row: a
   release branch name is longer than any column that also has to hold five others. */
.ds-base-btn { max-width: 100%; min-width: 0; }
.ds-base-btn > .ds-trunc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
/* The row whose report is open stays marked while the drawer covers part of the list. */
.ds-qrow--repo.is-open { border-color: var(--accent-text); background: var(--accent-soft); }

/* Under 1100px the decision and the link fold under the name rather than squeezing the
   repository path, which is the one column a reader scans. */
@media (max-width: 1180px) {
    .ds-qhead--repo { display: none; }
    .ds-qrow--repo {
        grid-template-columns: max-content minmax(0, 1fr) max-content;
        row-gap: var(--sp-2);
    }
    /* The two controls drop to a line of their own under the name rather than
       squeezing the repository path, which is the column a reader scans. */
    .ds-qrow--repo .ds-qrow-ctl { grid-column: 2 / -1; }
}

/* A stat tile reporting a gap rather than a figure. Same shape, tinted, because the
   number that matters most on this screen is the one somebody would skim past. */
.ds-stat--gap { border-color: var(--st-waiting-line); background: var(--st-waiting-bg); }
.ds-stat--gap .ds-stat-k, .ds-stat--gap .ds-stat-sub { color: var(--st-waiting-ink); }
.ds-stat-of { font-size: var(--fs-lead); font-weight: var(--fw-regular); color: var(--mute); }

/* A funnel stage, on or off. `--off` is struck through AND dimmed: strike-through alone
   is invisible at 11px on a stage name of one word, and colour alone does not survive
   a greyscale print or the 8% of men who cannot tell these two apart. */
.ds-tier {
    font-size: var(--fs-micro);
    font-family: var(--font-mono);
    padding: 2px var(--sp-2);
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--soft);
    background: var(--surface);
}
.ds-tier:hover { border-color: var(--line-firm); color: var(--ink); }
.ds-tier--off { color: var(--mute); text-decoration: line-through; border-style: dashed; }

.ds-legend-row { padding: var(--sp-3) 0; border-top: 1px solid var(--line); max-width: var(--ds-prose-max); }
.ds-legend-row h4 { font-size: var(--fs-dense); display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-1); }
.ds-legend-row p { font-size: var(--fs-dense); color: var(--soft); margin-bottom: var(--sp-1); }

/* ---- the conversation, one idiom for the whole product -------------------------
   Bubbles, right for the person and left for the agent, the same shape `chat/main.html`
   uses. Two chat idioms in one product is one of them being wrong. */
/* On its own -- a log rendered into a report rather than a panel -- it needs a ceiling
   or it grows the page without bound. */
.ds-chatlog {
    display: flex; flex-direction: column; gap: var(--sp-2);
    max-height: min(60vh, 560px); overflow-y: auto;
    overscroll-behavior: contain;
    padding: var(--sp-2) 0;
}
/* INSIDE A PANEL IT FILLS THE PANEL. The 340px ceiling above used to apply here too and
   beat the panel's own 70% height -- so the conversation was three messages tall inside
   a window with room for ten, and the empty space above it belonged to the panel.
   `min-height: 0` because a flex child will not shrink below its content otherwise, and
   then nothing scrolls at all. */
.ds-chatpanel .ds-chatlog {
    flex: 1 1 auto; min-height: 0; max-height: none;
}
.ds-chat-empty { font-size: var(--fs-dense); }
.ds-msg { display: flex; flex-direction: column; gap: 2px; max-width: 85%; }
.ds-msg--user { align-self: flex-end; align-items: flex-end; }
.ds-msg--agent { align-self: flex-start; }
.ds-bubble {
    padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-md);
    font-size: var(--fs-dense);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
    background: var(--sunken);
    border: 1px solid var(--line);
}
.ds-msg--user .ds-bubble { background: var(--accent-soft); border-color: var(--accent-soft); }
.ds-msg-meta, .ds-msg-cited { font-size: var(--fs-micro); color: var(--mute); }

.ds-chatform { display: flex; align-items: flex-end; gap: var(--sp-2); }
.ds-chatform > .ds-input { flex: 1 1 auto; resize: none; min-height: 34px; }

/* A PROPOSAL IS NOT A DECISION, and one that reverses a ruling is not a suggestion.
   `is-contradiction` is why this is not a calm blue box: offering "mark this No change"
   in the same tone to somebody who ruled Change required an hour ago invites them to
   undo their own decision without noticing. */
.ds-proposal {
    align-self: flex-start;
    max-width: 85%;
    display: flex; flex-direction: column; gap: var(--sp-2);
    padding: var(--sp-3);
    border: 1px solid var(--accent-soft);
    border-left: 2px solid var(--accent-text);
    border-radius: var(--radius-md);
    background: var(--surface);
}
.ds-proposal.is-contradiction { border-color: var(--st-failed-line); border-left-color: var(--st-failed-ink); }
.ds-proposal-ask { font-size: var(--fs-dense); font-weight: var(--fw-semibold); }

/* The fallback when the sanitiser or the rich editor did not load. Readable, plain,
   and obviously not a rendered document. */
.ds-doc-plain {
    white-space: pre-wrap;
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    min-height: 420px;
    width: 100%;
}

/* ======================================================================
   14. The verdict boxes — how many repositories change.
       See templates/work/impact.html.
   ====================================================================== */
/*
 * THE THIRD SHAPE THIS HAS HAD, and the small one. It was five bordered tiles a
 * hundred pixels tall, two of them reporting that there was nothing to report. Then it
 * was a proportional bar -- which is a good picture of a distribution and a poor one of
 * "3, 0, 0": with two counts at zero the bar is a solid block of a single colour, and
 * the reader goes to the legend for the numbers anyway. That cost two rows and a card
 * to say three digits.
 *
 * Now: the digits, boxed, on one line, each box as wide as its own words. No outer
 * card -- the boxes are the frame, and a frame around a row of frames is the padding
 * this strip kept being accused of.
 */
.ds-verdicts { min-width: 0; }

.ds-verdict-boxes {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--sp-2);
}

.ds-verdict-box {
    display: inline-flex;
    align-items: baseline;
    gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-3);
    background: var(--surface);
    border: 1px solid var(--line);
    /* The verdict colour as an edge rather than a fill: three filled boxes side by
       side compete, and the one that should win is whichever has a number in it. */
    border-left-width: 3px;
    border-radius: var(--radius-sm);
    white-space: nowrap;
}
.ds-verdict-box--affected { border-left-color: var(--st-waiting-ink); }
.ds-verdict-box--none     { border-left-color: var(--st-passed-ink); }
.ds-verdict-box--unknown  { border-left-color: var(--st-unknown-ink); }

.ds-verdict-n {
    font-size: var(--fs-lead);
    font-variant-numeric: tabular-nums;
    line-height: 1.1;
}
.ds-verdict-word { font-size: var(--fs-dense); color: var(--soft); }

/* A count nobody can act on is a count nobody reads, so when it is not zero the box
   says so in colour and the sentence beside it says so in words. */
.ds-verdict-box.is-gap {
    background: var(--st-waiting-bg);
    border-color: var(--st-waiting-line);
    border-left-color: var(--st-waiting-ink);
}
.ds-verdict-box.is-gap .ds-verdict-n,
.ds-verdict-box.is-gap .ds-verdict-word { color: var(--st-waiting-ink); }

/* A zero is not news. Dimming it lets the eye land on whichever box has a number in
   it, which is the only reason this strip is read at the top of the page. */
.ds-verdict-box.is-zero { color: var(--mute); border-left-color: var(--line-firm); }
.ds-verdict-box.is-zero .ds-verdict-word,
.ds-verdict-box.is-zero .ds-verdict-n { color: var(--mute); font-weight: var(--fw-regular); }

.ds-verdict-say { font-size: var(--fs-dense); color: var(--st-waiting-ink); }

/* The one control this screen has outside the rows and the gate, on the end of the
   row rather than on a line of its own: a line holding one button is a line. */
.ds-verdict-act { margin-left: auto; }

.ds-verdict-foot { margin-top: var(--sp-2); }

/* ===========================================================================
   §15  IMPLEMENTATION — the build board and one run's log.

   The board's shape is not new and is deliberately unchanged: repository tabs across
   the top rather than a list down the left, because five sandboxes building at once are
   PEERS and a vertical list implies a reading order that does not exist. What is new is
   that it is built from tokens, so the state of a build reads the same here as it does
   on the queue, on the analysis row and in the gate.
   =========================================================================== */

.ds-buildtabs-row {
    display: flex; align-items: flex-end; gap: var(--sp-3);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--sp-4);
}
.ds-buildtabs {
    display: flex; gap: var(--sp-1); min-width: 0;
    overflow-x: auto; scrollbar-width: thin;
    /* The row scrolls; the page never does. Eleven repositories is a real fan-out and
       the alternative is tabs 40px wide. */
    flex: 1 1 auto;
}
.ds-buildtabs-count { flex: none; padding-bottom: var(--sp-2); }

.ds-buildtab {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid transparent; border-bottom: 0;
    border-radius: var(--radius-sm) var(--radius-sm) 0 0;
    background: none; color: var(--soft);
    font: inherit; cursor: pointer; text-align: left;
    max-width: 260px; min-width: 0;
    /* Sits ON the row's border, which is what makes the open one continuous with the
       panel under it rather than a button near it. */
    margin-bottom: -1px;
}
.ds-buildtab:hover { color: var(--ink); background: var(--panel-bg); }
.ds-buildtab.is-open {
    color: var(--ink);
    background: var(--surface);
    border-color: var(--line);
    border-bottom: 1px solid var(--surface);
}
.ds-buildtab-dot { flex: none; }
.ds-buildtab-id { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ds-buildtab-name {
    font-size: var(--fs-dense); font-weight: var(--fw-semibold);
    font-family: var(--font-mono);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-buildtab-state { font-size: var(--fs-micro); color: var(--mute); }
.ds-buildtab:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.ds-buildpanel.is-hidden { display: none; }
/* The repository name and its branch on one line, the branch quieter. `.ds-card-head`
   has no second slot, so the subtitle rides inside the title and stays out of the way
   of the actions on the right. */
.ds-card-mark { color: var(--mute); margin-right: var(--sp-1); }
.ds-card-sub { font-weight: var(--fw-regular); margin-left: var(--sp-2); }

/* THE BRANCH TAKES ITS OWN LINE ON A PHONE.
   The sub-label holds a `<code>`, which base.css deliberately makes
   `inline-block; max-width: 100%` with its own scrollbar so a long token cannot push
   the page sideways. `100%` resolves against `.ds-card-title` -- the sub is inline and
   has no box of its own -- so the code could be the title's full 254px and the sub's
   own 8px `margin-left` put the pair 8px past the end of the head.

   `display: block` gives the sub a box, so `max-width: 100%` means 100% OF IT, and the
   margin goes with the line it no longer shares. The repository name reads better above
   its branch at this width in any case. */
@media (max-width: 680px) {
    .ds-card-sub {
        display: block;
        margin-left: 0;
        margin-top: var(--sp-1);
    }
}

.ds-buildbody { display: flex; flex-direction: column; min-height: 0; }

/* THE STAGES IT HAS REACHED. Still not a progress bar -- a bar claims a proportion and
   "cloning" is not 40% of anything -- but no longer a row of pills either.
   SIX ROUNDED PILLS IN A ROW IS A TAB STRIP. That is what they were, and people read
   them as six things to click: same shape as a chip, same shape as `.ds-btn--sm`, laid
   out the way tabs are laid out, directly above a panel whose contents they looked like
   they would switch. Nothing happened when you clicked, which is the worst outcome --
   the screen offered six controls and honoured none of them.
   So: a track with a mark per stage and a line between them. A line that joins things
   in order is read as a sequence, not as a set of choices; a tick is read as finished
   and an open ring as not yet. It claims no percentage, because the marks are discrete
   and the connector between two of them is either travelled or not.
   `cursor: default` and no hover state anywhere below, deliberately: the one honest
   signal that there is nothing here to press. */
.ds-buildsteps {
    display: flex; align-items: flex-start;
    gap: 0; padding: var(--sp-4) var(--sp-4) var(--sp-2);
    cursor: default; overflow-x: auto;
    /* It is an `<ol>` now, and the browser's markers and indent would push the first
       mark off centre under its label. */
    list-style: none; margin: 0;
}
.ds-buildstep {
    flex: 1 1 0; min-width: 0;
    display: flex; flex-direction: column; align-items: center; gap: var(--sp-1);
    position: relative;
    font-size: var(--fs-micro); font-weight: var(--fw-medium);
    color: var(--mute); text-align: center;
    /* Not uppercase. Six shouted labels is what made the row read as chrome rather
       than as a description of what is happening. */
    text-transform: none; letter-spacing: 0;
    padding: 0 var(--sp-1);
}
/* THE CONNECTOR, drawn from each mark back to the one before it, so the first stage
   has nothing trailing off the left edge. Behind the mark (`z-index`) so the ring sits
   on the line rather than the line crossing the ring. */
.ds-buildstep::before {
    content: ""; position: absolute; top: 7px; height: 2px;
    right: 50%; left: -50%; background: var(--line); z-index: 0;
}
.ds-buildstep:first-child::before { display: none; }
.ds-buildstep-mark {
    position: relative; z-index: 1;
    width: 16px; height: 16px; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    font-size: 9px; line-height: 1;
    background: var(--surface, #fff); border: 2px solid var(--line); color: transparent;
    flex: none;
}
.ds-buildstep-label { display: block; max-width: 100%; }

/* DONE: filled, ticked, and the line INTO it filled too -- the travelled part of the
   track is the only thing that carries "how far", and it carries it without claiming
   a number. */
.ds-buildstep--done { color: var(--ink); }
.ds-buildstep--done::before { background: var(--st-passed-ink); }
.ds-buildstep--done .ds-buildstep-mark {
    background: var(--st-passed-ink); border-color: var(--st-passed-ink); color: #fff;
}
/* CURRENT: a ring with a live centre. The halo is what a spinner was for, attached to
   the stage it belongs to rather than floating over the whole screen. */
.ds-buildstep--current { color: var(--ink); font-weight: var(--fw-semibold); }
.ds-buildstep--current::before { background: var(--st-passed-ink); }
.ds-buildstep--current .ds-buildstep-mark {
    border-color: var(--st-running-ink); background: var(--st-running-bg);
    color: var(--st-running-ink);
    animation: ds-buildstep-pulse 1.8s ease-in-out infinite;
}
/* FAILED: the track INTO it is still filled, because the run did get this far. Leaving
   that segment grey would say the stage was never reached, which is the opposite of
   what happened. */
.ds-buildstep--failed { color: var(--st-failed-ink); font-weight: var(--fw-semibold); }
.ds-buildstep--failed::before { background: var(--st-passed-ink); }
.ds-buildstep--failed .ds-buildstep-mark {
    background: var(--st-failed-ink); border-color: var(--st-failed-ink); color: #fff;
}
@keyframes ds-buildstep-pulse {
    0%, 100% { box-shadow: 0 0 0 0 var(--st-running-bg); }
    50%      { box-shadow: 0 0 0 4px var(--st-running-bg); }
}
/* Somebody who has asked for less movement gets the ring without the pulse. */
@media (prefers-reduced-motion: reduce) {
    .ds-buildstep--current .ds-buildstep-mark { animation: none; }
}
@media (max-width: 700px) {
    .ds-buildstep-label { font-size: 9px; }
}
.ds-buildquiet { padding: var(--sp-3) var(--sp-4) 0; }

/* THE LOG SCROLLS INSIDE THE PANEL, which is what keeps the message box below it from
   walking off the bottom of the screen while somebody watches a live build. */
.ds-buildlog {
    padding: var(--sp-4);
    max-height: 46vh; overflow-y: auto;
    flex: 1 1 auto; min-height: 0;
    display: flex; flex-direction: column; gap: var(--sp-1);
}

.ds-buildtalk {
    border-top: 1px solid var(--line); background: var(--panel-bg);
    padding: var(--sp-3) var(--sp-4);
    /* A surface of its own, because it read as one more entry in the feed rather than
       as somewhere to write. */
}
.ds-buildtalk-head {
    display: flex; align-items: center; gap: var(--sp-2);
    font-size: var(--fs-dense); font-weight: var(--fw-semibold);
    margin-bottom: var(--sp-2);
}
.ds-buildtalk-head > .ds-meta { margin-left: auto; font-weight: var(--fw-regular); }
.ds-buildtalk-form { display: flex; gap: var(--sp-2); align-items: flex-end; }
.ds-buildtalk-form > textarea {
    flex: 1 1 auto; min-width: 0; resize: none;
    font: inherit; font-size: var(--fs-dense);
    padding: var(--sp-2) var(--sp-3);
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--ink);
}
.ds-buildtalk-form > textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: -1px; }

/* ONE RUN'S EVENTS. Time, state, text -- three columns, so a reader scanning for the
   line that failed is scanning one column and not a paragraph. */
.ds-runlog { list-style: none; margin: 0; padding: var(--sp-2) 0; }
.ds-runevent {
    display: grid; grid-template-columns: 68px 18px minmax(0, 1fr);
    gap: var(--sp-2); align-items: baseline;
    padding: var(--sp-1) var(--sp-4);
    font-size: var(--fs-dense);
}
.ds-runevent:hover { background: var(--panel-bg); }
.ds-runevent-at { font-family: var(--font-mono); font-size: var(--fs-micro); color: var(--mute); }
.ds-runevent-mark { color: var(--mute); line-height: 1.4; }
.ds-runevent.is-error .ds-runevent-mark { color: var(--st-failed-ink); }
.ds-runevent.is-running .ds-runevent-mark { color: var(--st-running-ink); }
.ds-runevent.is-completed .ds-runevent-mark { color: var(--st-passed-ink); }
.ds-runevent-text { font-family: var(--font-mono); word-break: break-word; min-width: 0; }
.ds-runevent.is-error .ds-runevent-text { color: var(--st-failed-ink); }

@media (max-width: 760px) {
    .ds-buildtab { max-width: 180px; }
    .ds-buildtab-state { display: none; }
    .ds-runevent { grid-template-columns: 56px 18px minmax(0, 1fr); }
}

/* ===========================================================================
   §16  REVIEW — the diff reader.

   PORTED, NOT REWRITTEN. Everything above in this file is new work; this is the
   `rv-*` block lifted out of `work.css`, because a review screen that no longer loads
   that stylesheet would otherwise lose the only diff reader in the product -- and
   "never delete capability" is the rule this whole redesign runs under.

   What was DROPPED rather than ported is the half a design system already does: the
   cards, the summary panel, the pass/fail marks and two hand-built modals. Those are
   `ds-card`, `ds-note`, `ds-chip` and `ds-modal` now, which is also how the merge
   dialog stopped being a third chance to rediscover the scrim bug.

   Every hex fallback is gone. Each `var(--border-color, #e9edf3)` became `var(--line)`:
   a literal beside a token is a colour with two definitions, and the one in the
   fallback does not know the theme changed.
   =========================================================================== */

/* The merge footer lays out as a row with the control last, which is what the
   `ds-card-foot` it sits in expects. */
.ds-rv-merge { display: flex; align-items: center; gap: var(--sp-3); flex-wrap: wrap; }
.ds-rv-merge > .ds-meta { flex: 1 1 14rem; min-width: 0; }
.ds-rv.is-merged { opacity: .72; }
/* Merge order, as a numbered token beside the name rather than a column of its own. */
.ds-rv-order {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: var(--radius-full);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    background: var(--panel-bg); color: var(--soft);
    margin-right: var(--sp-1);
}
/* The legend in the viewer's head, beside the close control. */
.ds-rv-legend { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--fs-micro); }
/* A wider modal, because a diff read at 560px wraps every line. */
.ds-modal--wide { width: min(100vw, max(720px, 76vw)); }
.ds-rv-diff { font-family: var(--font-mono); font-size: var(--fs-meta); }

.ds-rv-order {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; border-radius: 999px;
    font-size: var(--fs-micro); font-weight: 650;
    background: var(--panel-bg); color: var(--soft);
}
.ds-rv-branch {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--fs-micro);
    background: var(--panel-bg); padding: 2px 7px; border-radius: 5px;
}
.ds-rv-add { color: var(--diff-add-ink); font-weight: var(--fw-semibold); }
.ds-rv-del { color: var(--diff-del-ink); font-weight: var(--fw-semibold); }
.ds-rv-files { padding: 0 16px 12px; }
.ds-rv-files-loading,
.ds-rv-files-empty,
.ds-rv-files-more { font-size: var(--fs-meta); color: var(--mute); padding: 4px 0; }
.ds-rv-files-error {
    font-size: var(--fs-meta); color: var(--diff-del-ink);
    background: var(--diff-del-bg);
    padding: 8px 11px; border-radius: 7px;
}
.ds-rv-file-list { list-style: none; margin: 0; padding: 0; }
.ds-rv-file {
    display: flex; align-items: center; gap: 9px;
    padding: 4px 0;
    font-size: var(--fs-meta);
    border-bottom: 1px solid var(--line-soft, var(--line));
}
.ds-rv-file:last-child { border-bottom: 0; }
.ds-rv-file-change {
    flex: none;
    width: 17px; height: 17px; border-radius: 4px;
    display: inline-flex; align-items: center; justify-content: center;
    font-size: var(--fs-micro); font-weight: 700;
}
.ds-rv-change-added    { background: var(--diff-add-bg); color: var(--diff-add-ink); }
.ds-rv-change-removed  { background: var(--diff-del-bg);  color: var(--diff-del-ink); }
.ds-rv-change-modified { background: var(--accent-soft);  color: var(--accent-text); }
.ds-rv-change-renamed  { background: var(--panel-bg); color: var(--soft); }
.ds-rv-file-path {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-rv-file-lines { margin-left: auto; display: flex; gap: 7px; flex: none; align-items: center; }
.ds-rv-file-note { color: var(--mute); font-style: italic; }
.ds-rv-file { display: block; padding: 0; }
.ds-rv-file-head {
    width: 100%; background: none; border: 0; cursor: pointer;
    display: flex; align-items: center; gap: 9px;
    padding: 5px 0; text-align: left;
    font-size: var(--fs-meta); font-family: inherit; color: inherit;
    border-radius: 5px;
}
.ds-rv-file-head:hover { background: var(--panel-bg); }
.ds-rv-file-caret {
    flex: none; width: 9px;
    font-size: 9px; color: var(--mute);
    transition: transform .12s ease;
}
.ds-rv-file-head[aria-expanded="true"] .ds-rv-file-caret { transform: rotate(90deg); }
.ds-rv-file-body { padding: 4px 0 10px 18px; }
.ds-rv-file { display: flex; flex-wrap: wrap; align-items: center; }
.ds-rv-file-head { flex: 1 1 auto; width: auto; min-width: 0; }
.ds-rv-file-body { flex: 1 0 100%; }
.ds-rv-file-open {
    flex: none;
    margin-left: 8px; padding: 3px 9px;
    border: 1px solid var(--line); border-radius: 6px;
    background: var(--surface); color: var(--soft);
    font-size: var(--fs-micro); font-family: inherit; cursor: pointer;
    opacity: 0; transition: opacity .12s ease, border-color .12s ease;
}
.ds-rv-file:hover .ds-rv-file-open,
.ds-rv-file-open:focus { opacity: 1; }
.ds-rv-file-open:hover { border-color: var(--cp-active); color: var(--ink); }
.ds-rv-whole { border: 0; }

/* The navigator for the whole change: repositories, then files.
   Sticky rather than fixed, so it follows the reader down a long diff and stops with
   the page. Collapses to a normal block on a narrow screen, where a 260px column
   would leave nothing for the code -- which is what the reader came for. */
/* Closed review rounds, and the dialog that closes one. */
.ds-rv-rounds { display: flex; flex-direction: column; gap: 8px; margin-bottom: 12px; }
.ds-run-round { margin: 0 0 6px; }

/* Settings: a section heading with its action, used by every tab. */
.ds-sechead {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: 16px; margin: 0 0 16px;
}
.ds-sechead-text { min-width: 0; }  /* so a long sentence wraps rather than
                                       pushing the action off the right edge */
.ds-sechead-title { margin: 0; font-size: var(--fs-h2); }
.ds-sechead-say { margin: 4px 0 0; color: var(--soft); max-width: var(--ds-prose-max); }

/* The integrations table. One row per tool, the controls folded away beneath it:
   five live buttons per line turned seven tools into forty controls, with the
   destructive one sitting beside the harmless ones. */
.ds-tools td, .ds-acts td { vertical-align: middle; }
.ds-tool { display: flex; align-items: center; gap: 10px; min-width: 0; }
.ds-tool-mark {
    flex: none; display: inline-flex; align-items: center; justify-content: center;
    width: 28px; height: 28px; font-size: 18px;
}
.ds-tool-monogram {
    display: inline-flex; align-items: center; justify-content: center;
    width: 24px; height: 24px; border-radius: 6px;
    background: currentColor; color: var(--surface);
    font-size: var(--fs-micro); font-weight: 600;
}
.ds-tool-id { display: flex; flex-direction: column; min-width: 0; }
.ds-tool-name { font-weight: 600; }
.ds-tools-end { text-align: right; white-space: nowrap; }
.ds-tool-detail > td { background: var(--panel-bg); }
.ds-tool-why { margin: 0 0 10px; }
.ds-tool-controls { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.ds-tool-controls form { display: inline-flex; align-items: center; gap: 6px; margin: 0; }
/* A key field wide enough to recognise what is in it, narrow enough that Save stays
   on the same line -- a wrapped Save reads as though it belongs to the row below. */
.ds-tool-controls .ds-input { width: 15rem; max-width: 100%; }
.ds-act { display: flex; flex-direction: column; min-width: 0; }
.ds-act-title { font-weight: 500; }
@media (max-width: 860px) {
    /* The two middle columns are detail; the name and the status are the point.
       SCOPED TO THE FIVE-COLUMN TABLE. Every Settings tab uses `.ds-tools` now, and
       on a three-column one this rule hid the Status column -- the one thing the
       narrow screen was supposed to keep. */
    .ds-tools--wide th:nth-child(2), .ds-tools--wide td:nth-child(2),
    .ds-tools--wide th:nth-child(3), .ds-tools--wide td:nth-child(3) { display: none; }
}

/* The agent's question, and the way back into the build. */
.ds-askback .ds-note-body { display: flex; flex-direction: column; gap: 8px; }
.ds-askback-q { margin: 0; font-size: var(--fs-body); }
.ds-askback-ctx { margin: 0; white-space: pre-wrap; }
.ds-askback-options { list-style: none; margin: 0; padding: 0;
    display: flex; flex-wrap: wrap; gap: 6px; }
.ds-askback-input { width: 100%; }
.ds-askback-foot { display: flex; align-items: center; gap: 10px; }
.ds-askback-foot [data-role="answer-error"] { margin-right: auto; color: var(--fail); }
.ds-run-empty { padding: 12px 16px; }
.ds-rv-round-text { margin: 2px 0 10px; white-space: pre-wrap; }
.ds-rv-round-repos { margin: 2px 0 0; padding-left: 18px; }
.ds-rv-round-repos ul { margin: 2px 0 6px; padding-left: 16px; }

.ds-rework { display: flex; flex-direction: column; gap: 14px; }
/* Files on the left, the complaint on the right -- the reviewer reads the list and
   writes about what they ticked, so the two belong side by side. */
.ds-rework-pane { display: flex; gap: 16px; align-items: flex-start; }
.ds-rework-files-col { flex: 0 0 300px; min-width: 0; display: flex; flex-direction: column; gap: 4px; }
.ds-rework-notes-col { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
.ds-rework-pane.is-skipped .ds-rework-files-col,
.ds-rework-pane.is-skipped .ds-field { opacity: .45; }
.ds-rework-skip {
    display: flex; align-items: center; gap: 8px;
    min-height: 28px; cursor: pointer;
}
.ds-rework-nofiles { padding: 8px; }
/* One step per repository, and they are navigation: six repositories is too many to
   reach by pressing Next five times to correct the first one. */
.ds-rework-steps {
    display: flex; flex-wrap: wrap; gap: 6px;
    padding: 8px 16px; border-bottom: 1px solid var(--line);
}
.ds-rework-step {
    min-height: 26px; padding: 3px 10px;
    border: 1px solid var(--line); border-radius: var(--radius-full);
    background: var(--surface); color: var(--soft);
    font: inherit; font-size: var(--fs-micro); cursor: pointer;
}
.ds-rework-step:hover { color: var(--ink); }
.ds-rework-step.is-current { border-color: var(--cp-active); color: var(--ink); font-weight: 600; }
.ds-rework-step.is-filled { background: var(--st-waiting-bg); color: var(--st-waiting-ink); }
.ds-rework-step.is-skipped { opacity: .55; text-decoration: line-through; }
/* The error sits left, the controls right. */
.ds-rework-foot [data-role="rework-error"] { margin-right: auto; color: var(--fail); }
/* Bounded and scrolled: a change across six repositories is a long list, and a
   dialog that grows past the window puts its own Send button off-screen. */
.ds-rework-targets {
    max-height: 320px; overflow: auto;
    border: 1px solid var(--line); border-radius: 8px; padding: 6px;
}
.ds-rework-repo + .ds-rework-repo { margin-top: 6px; border-top: 1px solid var(--line); padding-top: 6px; }
.ds-rework-repohead {
    display: flex; align-items: center; gap: 8px;
    min-height: 28px; padding: 3px 4px; cursor: pointer;
}
.ds-rework-files { margin-left: 22px; }
.ds-rework-file {
    display: flex; align-items: center; gap: 8px;
    min-height: 24px; padding: 1px 4px; cursor: pointer;
    font-size: var(--fs-micro);
}
.ds-rework-file:hover, .ds-rework-repohead:hover { background: var(--panel-bg); }
.ds-rework-file span { font-family: var(--font-mono, monospace); }
/* The verdict sits with the change it judges, at the end of the summary note. */
.ds-note-actions { margin-left: auto; align-self: center; padding-left: 12px; }

/* THE MESSAGE IS THE POINT OF THE NOTE, so it does not get the narrow half.
   `.ds-note` is icon + body + actions on one line and does not wrap, so on the Review
   screen at 390px "2 repositories in this change, 2 with no pull request" was folded
   into 124px beside 161px of buttons -- six words a line, the buttons given more room
   than the sentence explaining them. Below 680px the actions take a line of their own.

   Here rather than beside `.ds-note` in feedback.css: `.ds-note-actions` is declared
   in THIS file, ds_base.html loads data.css after feedback.css, and a media query adds
   no specificity -- so `margin-left: auto` above would have won. */
@media (max-width: 680px) {
    .ds-note { flex-wrap: wrap; }
    .ds-note-body { flex: 1 1 100%; }
    .ds-note-actions {
        flex: 1 1 100%;
        margin-left: 0;
        padding-left: 0;
        display: flex;
        flex-wrap: wrap;
        gap: var(--sp-2);
    }
}

/* THREE PANES: which repository, which file, and the code.
   The change has three levels, so the screen does. Nesting files under repositories
   in one column means a reader with six repositories scrolls one list of everything
   to reach one file. */
.ds-rv-page { display: flex; gap: 12px; align-items: flex-start; }
.ds-rv-detail { flex: 1 1 auto; min-width: 0; }
.ds-rv-repos, .ds-rv-filepane {
    position: sticky; top: 12px;
    max-height: calc(100vh - 120px); overflow: auto;
    border: 1px solid var(--line); border-radius: 8px;
    background: var(--surface);
}
.ds-rv-repos { flex: 0 0 210px; }
.ds-rv-filepane { flex: 0 0 240px; }
.ds-rv-pane-head { padding: 8px; border-bottom: 1px solid var(--line); }
.ds-rv-pane-head .ds-meta { margin: 6px 2px 0; }
.ds-rv-pane-title {
    margin: 0 2px; font-size: var(--fs-micro); font-weight: 600;
    text-transform: uppercase; letter-spacing: .04em; color: var(--mute);
}
.ds-rv-pane-wait { list-style: none; padding: 10px 8px; }
/* A repository with nothing matching the filter recedes but stays put: moving the
   list under the pointer while somebody is typing is its own problem. */
.ds-rv-nav-repo.has-no-match > .ds-rv-nav-repohead { opacity: .4; }
.ds-rv-tree-file.is-current { background: var(--panel-bg); font-weight: 600; }
.ds-rv-tree-file.is-current .ds-rv-tree-name { color: var(--ink); }
.ds-rv-nav-list { list-style: none; margin: 0; padding: 6px; }
.ds-rv-nav-repo { margin: 0 0 4px; }
.ds-rv-nav-repo:last-child { margin-bottom: 0; }
.ds-rv-nav-repohead {
    display: flex; align-items: center; gap: 6px;
    width: 100%; min-height: 30px; padding: 5px 6px;
    border: 0; border-radius: 6px; background: none;
    color: var(--ink); font: inherit; font-weight: 600;
    text-align: left; cursor: pointer;
}
.ds-rv-nav-repohead:hover { background: var(--panel-bg); }
.ds-rv-nav-repo.is-current > .ds-rv-nav-repohead { background: var(--panel-bg); }
/* The open repository is marked on the EDGE as well as by its background: a tint
   alone is the kind of difference that disappears on a bright screen. */
.ds-rv-nav-repo.is-current { box-shadow: inset 2px 0 0 var(--cp-active); border-radius: 6px; }
/* Ellipsised from the END now that the owner prefix is gone: `twilio-go` is read
   left to right, and the left-truncation that suited a path made it `...wilio-go`. */
.ds-rv-nav-reponame {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-rv-nav-count { flex: none; font-weight: 400; }
/* Merged repositories are still listed -- the order is part of reading the change --
   but they are not what anybody is here to do. */
.ds-rv-nav-repo.is-merged > .ds-rv-nav-repohead { opacity: .6; }
.ds-rv-nav-files { list-style: none; margin: 0; padding: 6px; }
.ds-rv-tree-head { padding: 8px; border-bottom: 1px solid var(--line); }
.ds-rv-tree-head .ds-meta { margin: 6px 2px 0; }
.ds-rv-tree-list { list-style: none; margin: 0; padding: 6px; }
.ds-rv-tree-list ul { list-style: none; margin: 0; padding: 0; }
/* One directory and its files. Spacing belongs here rather than on the heading,
   so a group with no directory (files at the repository root) is spaced the same. */
.ds-rv-tree-group { margin: 0 0 6px; }
.ds-rv-tree-group:last-child { margin-bottom: 0; }
.ds-rv-tree-dir {
    margin: 8px 4px 2px;
    color: var(--mute); font-size: var(--fs-micro);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    direction: rtl; text-align: left;     /* keep the DEEPEST folder visible */
}
.ds-rv-tree-file {
    display: flex; align-items: center; gap: 6px;
    width: 100%; min-height: 26px; padding: 3px 5px;
    border: 0; border-radius: 6px; background: none;
    color: var(--ink); font: inherit; font-size: var(--fs-micro);
    text-align: left; cursor: pointer;
}
.ds-rv-tree-file:hover { background: var(--panel-bg); }
.ds-rv-tree-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-rv-tree-lines { flex: none; font-size: var(--fs-micro); }
.ds-rv-tree-none { padding: 8px; }

/* The path above each diff: directories recede, the filename does not. */
.ds-rv-crumb { color: var(--mute); }
.ds-rv-crumb-sep { color: var(--mute); margin: 0 3px; }
.ds-rv-crumb-name { color: var(--ink); font-weight: 600; }

/* Three columns do not fit a laptop beside a 208px rail, let alone a phone. Below
   1200px the repository list folds into a row above the files; below 900px both
   become ordinary blocks and the code gets the whole width. */
@media (max-width: 1200px) {
    .ds-rv-repos { flex: 0 0 170px; }
    .ds-rv-filepane { flex: 0 0 200px; }
}
@media (max-width: 900px) {
    .ds-rv-page { display: block; }
    .ds-rv-repos, .ds-rv-filepane {
        position: static; max-height: 240px; margin-bottom: 10px; flex: none;
    }
}

/* Pygments token classes, mapped onto six roles.
   Pygments emits dozens of classes; these group them, so a language nobody thought
   about still comes out readable instead of uncoloured. Anything unmapped inherits
   the line colour, which is the correct default -- never invisible. */
.ds-rv-line-text .k,  .ds-rv-line-text .kd, .ds-rv-line-text .kn,
.ds-rv-line-text .kp, .ds-rv-line-text .kr, .ds-rv-line-text .kt,
.ds-rv-line-text .ow, .ds-rv-line-text .cp { color: var(--syn-keyword); }

.ds-rv-line-text .s,   .ds-rv-line-text .s1, .ds-rv-line-text .s2,
.ds-rv-line-text .sb,  .ds-rv-line-text .sc, .ds-rv-line-text .sd,
.ds-rv-line-text .se,  .ds-rv-line-text .sh, .ds-rv-line-text .si,
.ds-rv-line-text .sr,  .ds-rv-line-text .ss, .ds-rv-line-text .sa,
.ds-rv-line-text .dl   { color: var(--syn-string); }

.ds-rv-line-text .c,  .ds-rv-line-text .c1, .ds-rv-line-text .cm,
.ds-rv-line-text .cs, .ds-rv-line-text .ch { color: var(--syn-comment); font-style: italic; }

.ds-rv-line-text .m,  .ds-rv-line-text .mi, .ds-rv-line-text .mf,
.ds-rv-line-text .mh, .ds-rv-line-text .mo, .ds-rv-line-text .il,
.ds-rv-line-text .kc  { color: var(--syn-number); }

.ds-rv-line-text .nf, .ds-rv-line-text .nd, .ds-rv-line-text .na,
.ds-rv-line-text .nv, .ds-rv-line-text .vi, .ds-rv-line-text .nt
      { color: var(--syn-name); }

.ds-rv-line-text .nc, .ds-rv-line-text .nn, .ds-rv-line-text .no,
.ds-rv-line-text .ne, .ds-rv-line-text .nb { color: var(--syn-type); }

/* A lexing error is not a reason to shout at the reader: Pygments marks a token it
   could not place with `.err`, and the default stylesheet paints it on red. On a
   per-line lex that fires constantly and legitimately -- the second half of a string
   that opened on the line above is an error to the lexer and ordinary code to a
   person. */
.ds-rv-line-text .err { color: inherit; background: none; border: 0; }

/* "Viewed" -- a reviewer's place in a long change.
   Always visible, unlike `.ds-rv-file-open`, which appears on hover: this one is a
   state as well as a control, so hiding it until the pointer arrives would hide the
   fact that the file HAS been viewed. */
.ds-rv-file-viewed {
    flex: none;
    display: inline-flex; align-items: center; gap: 5px;
    margin-left: 8px; padding: 3px 7px;
    min-height: 24px;                      /* a target, not just a label */
    border-radius: 6px;
    color: var(--soft); font-size: var(--fs-micro);
    cursor: pointer; user-select: none;
}
.ds-rv-file-viewed:hover { color: var(--ink); background: var(--panel-bg); }
.ds-rv-file-viewed input { margin: 0; cursor: pointer; }
/* Read and put away: still listed, still reopenable, visibly done. */
.ds-rv-file.is-viewed .ds-rv-file-path { text-decoration: line-through; }
.ds-rv-file.is-viewed .ds-rv-file-head { opacity: .55; }
.ds-rv-hunk {
    width: 100%; border-collapse: collapse;
    margin: 0 0 8px;
    border: 1px solid var(--line); border-radius: 7px;
    overflow: hidden;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: var(--fs-micro); line-height: 1.6;
}
.ds-rv-hunk:last-child { margin-bottom: 0; }
.ds-rv-hunk-head {
    caption-side: top; text-align: left;
    padding: 4px 9px;
    background: var(--panel-bg);
    color: var(--mute);
    font-family: inherit; font-size: var(--fs-micro);
}
.ds-rv-hunk-note {
    font-size: var(--fs-meta); color: var(--mute);
    padding: 5px 0;
}
.ds-rv-line td { padding: 0 4px; vertical-align: top; }
.ds-rv-line-no {
    width: 1%; white-space: nowrap; text-align: right;
    color: var(--mute);
    user-select: none;
    border-right: 1px solid var(--line-soft, var(--line));
}
.ds-rv-line-sign { width: 1%; user-select: none; text-align: center; }
.ds-rv-line-text { white-space: pre-wrap; word-break: break-word; }
.ds-rv-line-add    { background: var(--diff-add-bg); }
.ds-rv-line-add .ds-rv-line-sign,
.ds-rv-line-add .ds-rv-line-text { color: var(--diff-add-ink); }
.ds-rv-line-remove { background: var(--diff-del-bg); }
.ds-rv-line-remove .ds-rv-line-sign,
.ds-rv-line-remove .ds-rv-line-text { color: var(--diff-del-ink); }
.ds-rv-line-meta .ds-rv-line-text { color: var(--mute); font-style: italic; }
.ds-rv-merge {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 11px 16px;
    border-top: 1px solid var(--line);
    background: var(--panel-bg);
}
.ds-rv-merge button { margin-left: auto; }
.ds-rv-result { margin: 0 16px 12px; padding: 9px 12px; border-radius: 7px; font-size: var(--fs-meta); }
.ds-rv-result.is-good { background: var(--diff-add-bg); color: var(--diff-add-ink); }
.ds-rv-result.is-bad  { background: var(--diff-del-bg); color: var(--diff-del-ink); }
/* `.ds-rv-modal` is NOT ported. It was the hand-built overlay behind the file viewer
   and the merge confirmation, both of which are `ds-modal` now -- and a hand-built
   overlay is precisely what put the analysis screen's dialog underneath its own scrim.
   Shipping the rules for an overlay nothing renders would leave the next reader a
   second way to build one. */

/* ===========================================================================
   §17  THE AUDIT CHAIN.

   A vertical list where the GAPS matter as much as the entries, so a missing link is
   rendered in the same shape as a present one and marked, rather than left out. A chain
   that renders only what it has reads as complete.
   =========================================================================== */
.ds-chain { list-style: none; margin: 0; padding: 0; }
.ds-chain-link {
    display: grid; grid-template-columns: 26px minmax(0, 1fr);
    gap: var(--sp-3);
    padding: var(--sp-3) 0 var(--sp-4);
    position: relative;
}
/* The thread between the links, drawn behind the marks. Stops at the last one, because
   a line running off the bottom implies a link that is not there. */
.ds-chain-link:not(:last-child)::before {
    content: ""; position: absolute;
    left: 12px; top: 32px; bottom: 0;
    width: 1px; background: var(--line);
}
.ds-chain-mark {
    display: flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; border-radius: var(--radius-full);
    font-size: var(--fs-micro); flex: none;
    background: var(--panel-bg); color: var(--mute);
    border: 1px solid var(--line);
    position: relative; z-index: 1;
}
.ds-chain-link.is-present .ds-chain-mark {
    background: var(--st-passed-bg); color: var(--st-passed-ink);
    border-color: var(--st-passed-line);
}
/* A MISSING LINK IS NOT A FAILURE. `failed` is red and says somebody broke something;
   an unrecorded link says we do not know, which is the `unknown` vocabulary. */
/* `--st-unknown-bg` is `transparent` on purpose -- the verdict bar hatches it -- and a
   transparent circle on a page background reads as a hole rather than a marker. The
   surface plus a dashed edge is the "not recorded" reading. */
.ds-chain-link.is-missing .ds-chain-mark {
    background: var(--surface); color: var(--st-waiting-ink);
    border-style: dashed; border-color: var(--st-waiting-line);
}
.ds-chain-body { min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ds-chain-label {
    font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.07em;
    font-weight: var(--fw-semibold); color: var(--mute);
}
.ds-chain-say { font-size: var(--fs-dense); color: var(--ink); }
.ds-chain-link.is-missing .ds-chain-say { color: var(--soft); }
.ds-chain-when {
    display: flex; gap: var(--sp-2); font-size: var(--fs-micro); color: var(--mute);
    margin-top: 2px;
}
.ds-chain-who { font-weight: var(--fw-semibold); }
.ds-chain-line {
    font-size: var(--fs-meta); color: var(--soft);
    font-family: var(--font-mono); word-break: break-word;
}

/* ===========================================================================
   §18  OVERVIEW — the counts, the section heads, and the row shape they list.
   =========================================================================== */

/* A row of tiles that wraps rather than scrolls: five counts at phone width are five
   rows, and a horizontal scroller hides the two that matter most. */
.ds-stats {
    display: grid; gap: var(--sp-3);
    grid-template-columns: repeat(auto-fit, minmax(min(100%, 150px), 1fr));
}
/* ONE TILE PER ROW ON A PHONE. `auto-fit` with a 150px floor still fits two columns at
   390px -- 173px each, less the tile's own padding -- and `609,372/1,000,000` at
   `--fs-h2` needs 160. The number has no break opportunity a browser will take, so it
   was painted straight out of its tile and over the one beside it. */
@media (max-width: 680px) {
    .ds-stats { grid-template-columns: minmax(0, 1fr); }
}
.ds-stat.is-selected { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
/* A COUNT THAT JUST CHANGED, held for a moment. When a document finishes ingesting, the
   excerpt count moves without a reload, and a number that changes silently on a screen
   nobody is staring at is a number nobody sees change. Dropped after 2.6s by the script
   that adds it, so it is a notice rather than a state. */
.ds-stat.is-moved { border-color: var(--accent); background: var(--accent-soft); }
/* A ZERO SHOULD RECEDE RATHER THAN SHOUT, and a tile with nothing at it is not a link:
   filtering to an empty list is a worse answer than the zero already on the tile. */
.ds-stat.is-empty { background: var(--panel-bg); border-style: dashed; }
.ds-stat-zero { color: var(--mute); font-weight: var(--fw-regular); }
a.ds-stat { text-decoration: none; }
a.ds-stat:hover { border-color: var(--accent); }

/* The heading over a list, with its count. Not an `h2`: there are up to six of these on
   one screen and six h2s make six claims about what the page is about. */
.ds-sectionhead {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-4) 0 var(--sp-2);
    font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.07em;
    font-weight: var(--fw-semibold); color: var(--mute);
}
.ds-sectionhead-n {
    font-variant-numeric: tabular-nums; color: var(--soft);
    background: var(--panel-bg); border-radius: var(--radius-full);
    padding: 0 var(--sp-2); letter-spacing: 0;
}
.ds-sectionhead > .ds-iconbtn { margin-left: auto; }

/* OVERVIEW'S ROW. Key, name and detail, state on the right -- the queue's shape with no
   checkbox and no owner column, because nothing here is selectable in bulk. */
.ds-qrow--home { grid-template-columns: auto minmax(0, 1fr) auto; }

/* THE TRAILING COLUMN TAKES ITS OWN LINE ON A PHONE.
   On Overview the row is a key or a state dot, the title with its meta line, and a chip
   or a button. "Implementation" is a 106px chip and `MEILI-1` a 50px key, so with two
   12px gaps a 320px row left 74px for a title and a repository name -- which then
   painted straight out of the 74px it had. Below 680px the third column drops under the
   second, where it has the width it needs and the title has the row.

   SCOPED TO `.work-home`, and that is not fussiness. The project list uses the same row
   class with a DIFFERENT shape -- main, chip, links, and no leading key -- so written
   against `.ds-qrow--home` alone this took that page from three columns to two and left
   its "9 repositories read, 1,631 files" chip painting 89px out of a 90px column, on
   every row. Two shapes under one class: the rule has to say which it means. */
@media (max-width: 680px) {
    .work-home .ds-qrow--home {
        grid-template-columns: auto minmax(0, 1fr);
        row-gap: var(--sp-2);
    }
    .work-home .ds-qrow--home > :nth-child(3) {
        grid-column: 2 / -1;
        justify-self: start;
    }
}
.ds-qrow--home .ds-qrow-main { display: flex; flex-direction: column; gap: 1px; min-width: 0; text-align: left; }
.ds-qrow--home .ds-qrow-meta { display: flex; flex-wrap: wrap; gap: var(--sp-2); }
a.ds-qrow--home { text-decoration: none; }
a.ds-qrow--home:hover .ds-qrow-title { color: var(--accent-text); }
/* "See the rest" is a row too, so it lands in the list's own border rather than floating
   under it -- but it is not a ticket, so it says so by being centred and quiet. */
.ds-qrow--more {
    display: block; text-align: center; color: var(--accent-text);
    font-size: var(--fs-meta); text-decoration: none;
    padding: var(--sp-3);
}
.ds-qrow--more:hover { background: var(--panel-bg); }

/* THE FIRST RUN. The one card on this screen with the accent edge, because it is the
   moment the product exists for and everything else here is a list. */
.ds-card--accent { border-left: 3px solid var(--accent); }
.ds-firstrun {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-4); flex-wrap: wrap;
}
.ds-firstrun-id { flex: 1 1 18rem; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ds-firstrun .ds-btn { flex: none; }

/* The checklist's own head, inside the fold. */
.ds-setup-head { padding: var(--sp-4); border-bottom: 1px solid var(--line); }
.ds-setup-head.is-second { border-top: 1px solid var(--line); background: var(--panel-bg); }
.ds-setup-title { font-size: var(--fs-lead); font-weight: var(--fw-semibold); margin: 0 0 2px; }
.ds-fold-name { font-weight: var(--fw-semibold); }
/* A proportion that IS a proportion -- unlike the build steps, where "cloning" is not
   40% of anything. Here the denominator is a real count of boxes. */
.ds-meter {
    height: 4px; border-radius: var(--radius-full); background: var(--panel-bg);
    margin-top: var(--sp-3); overflow: hidden;
}
.ds-meter > span { display: block; height: 100%; background: var(--accent); }

/* ===========================================================================
 * 19. PROJECT SETUP — the checklist, the repositories panel, the read
 *
 * THESE RULES ARE THE FIX FOR A REGRESSION, not a new look. The Overview rebuild
 * moved `work/home.html` onto `ds_base.html`, which loads `theme-variables.css`
 * and the seven `ds/` files and nothing else. The checklist it includes
 * (`work/_setup_step.html`) was still written in the `cp-*` vocabulary, which
 * lives only in `codepixel.css` -- so 39 classes on the one screen every new
 * project starts on resolved to no rule at all.
 *
 * It passed every test: the suite asserts markup, and the markup was correct.
 * A rendered page has to be checked against the stylesheets it actually loads,
 * which is what `tests_setup_layout.py::NoOrphanClasses` now does.
 *
 * The vocabulary is `ds-step*` because that is what the step dict is -- a row
 * with a state -- and the states are the eight the palette already has.
 * ======================================================================== */

.ds-steplist { display: flex; flex-direction: column; }

.ds-step {
    display: flex; align-items: flex-start; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border-bottom: 1px solid var(--line);
}
.ds-step:last-child { border-bottom: 0; }

.ds-step-mark {
    flex: none; width: 20px; height: 20px; border-radius: var(--radius-full);
    display: flex; align-items: center; justify-content: center;
    font-size: var(--fs-micro); margin-top: 1px;
    border: 1.5px solid var(--line-firm);
    color: var(--mute);
}
.ds-step-body { min-width: 0; flex: 1; }
.ds-step-name { font-size: var(--fs-body); font-weight: var(--fw-medium); }
.ds-step-detail {
    font-size: var(--fs-dense); color: var(--soft);
    line-height: var(--lh-normal); margin-top: 2px;
}
.ds-step-action { flex: none; display: flex; flex-direction: column; align-items: flex-end; gap: var(--sp-1); }

/* THE ACTION TAKES A LINE OF ITS OWN ON A PHONE.
   The row is mark + body + action with `flex: none` on the action, so at 324px the
   action's 160px left the body 104 -- and the body holds the repository cards, which
   need about 150. They were painted across the action column.

   Wrapped, with the action forced onto its own line by a 100% basis and indented to
   line up under the body rather than under the mark. */
@media (max-width: 680px) {
    .ds-step { flex-wrap: wrap; }
    .ds-step-action {
        flex: 1 1 100%;
        align-items: flex-start;
        margin-left: calc(20px + var(--sp-3));
    }
}

.ds-step--done .ds-step-mark {
    background: var(--st-passed-bg); border-color: transparent; color: var(--st-passed-ink);
}
.ds-step--done .ds-step-name { color: var(--mute); font-weight: var(--fw-regular); }
.ds-step--done .ds-step-detail { display: none; }
/* EXCEPT THE ONE THAT STILL HAS SOMETHING TO SAY. "10 issues from MEILI are on the
   board" answers "did that import work?", and a row reporting a finished import read
   identically to one nobody had pressed -- same title, same button -- so the way to
   find out was to press it again. */
.ds-step--done.ds-step--revisit .ds-step-detail { display: block; }
.ds-step--done.ds-step--revisit .ds-step-name { color: var(--ink); }

/* The one lit row. Everything else recedes so this reads as the instruction. */
.ds-step--next { background: var(--accent-soft); }
.ds-step--next .ds-step-mark {
    background: var(--accent); border-color: transparent; color: var(--on-accent);
}
.ds-step--next .ds-step-name { color: var(--accent-text); font-weight: var(--fw-semibold); }

/* A STEP THAT IS NEITHER DONE NOR NEXT: a later one, so it recedes. `cp-step-todo` had
   no rule either, which worked only because every other state overrode the default --
   and on a project where nothing is done yet, which is every new project, this is most
   of the list. Three equally-weighted rows ask the reader to choose where to start,
   which is the decision the checklist exists to remove. */
.ds-step--todo .ds-step-name { color: var(--soft); }
.ds-step--todo .ds-step-mark { background: var(--surface); }

.ds-step--warn .ds-step-mark {
    background: var(--st-waiting-bg); border-color: transparent; color: var(--st-waiting-ink);
}
.ds-step--warn .ds-step-detail { color: var(--st-waiting-ink); }

/* The other way to do the same step. A link, not a button: one of the two is the
   recommendation and they must not look like a choice between equals. */
.ds-step-alt {
    background: none; border: 0; padding: 0;
    font-size: var(--fs-dense); color: var(--accent-text);
    cursor: pointer; text-decoration: underline; text-align: right;
}

/* --- What each repository is written in, under the stack row ------------- */

.ds-repos { margin-top: var(--sp-2); }
.ds-repos-summary {
    display: flex; align-items: center; gap: var(--sp-2);
    cursor: pointer; list-style: none;
    font-size: var(--fs-dense); font-weight: var(--fw-medium);
    color: var(--accent-text); padding: 3px 0;
}
.ds-repos-summary::-webkit-details-marker { display: none; }
.ds-repos-mark { font-size: var(--fs-micro); color: var(--mute); transition: transform var(--dur-fast) var(--ease-out); }
.ds-repos[open] .ds-repos-mark { transform: rotate(90deg); }
.ds-repos-count { font-weight: var(--fw-regular); font-size: var(--fs-dense); color: var(--soft); }
.ds-repos-body {
    margin-top: var(--sp-2);
    border-left: 2px solid var(--accent-soft);
    padding-left: var(--sp-3);
    display: flex; flex-direction: column; gap: var(--sp-3);
}
.ds-repo-head { display: flex; align-items: baseline; gap: var(--sp-2); flex-wrap: wrap; }
.ds-repo-name {
    font-family: var(--font-mono); font-size: var(--fs-dense);
    font-weight: var(--fw-medium); color: var(--ink);
}
.ds-repo-tag {
    padding: 1px 7px; border-radius: var(--radius-full);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    text-transform: uppercase; letter-spacing: 0.03em;
    background: var(--accent-soft); color: var(--accent-text);
}
.ds-repo-meta { font-size: var(--fs-micro); color: var(--mute); }
.ds-repo-langs { display: flex; gap: 6px; flex-wrap: wrap; margin-top: var(--sp-1); }
.ds-repo-lang {
    padding: 2px var(--sp-2); border-radius: var(--radius-xs);
    font-size: var(--fs-micro); background: var(--sunken); color: var(--ink);
}
.ds-repo-lang b { font-weight: var(--fw-semibold); }
.ds-repo-lang--none {
    background: var(--st-unknown-bg); color: var(--st-unknown-ink); font-style: italic;
}
.ds-repo-facts {
    display: flex; gap: var(--sp-3); flex-wrap: wrap;
    margin-top: var(--sp-1); font-size: var(--fs-micro); color: var(--soft);
}
/* The sandbox, named by what it carries rather than by a registry path. A ticket here
   used to build in the PROJECT's stack whatever the repository was -- one answer for
   six languages -- which is how a Go ticket ran in a Python container and opened a
   pull request containing one empty requirements.txt. */
.ds-repo-image b { color: var(--pass); font-weight: var(--fw-semibold); }
/* THE HONEST HALF. A symbol count with no caveat reads as completeness. */
.ds-repo-gaps { margin-top: var(--sp-1); font-size: var(--fs-micro); color: var(--st-waiting-ink); }

/* --- The read in progress ------------------------------------------------
 * Indexing is the longest wait in the product and showed almost nothing:
 * `indexed_files_count` is written once, after the last file, so the bar sat still
 * for the whole run. Silence during a long wait is indistinguishable from failure.
 * Inside the step it belongs to, not a banner at the top of the page.
 * ----------------------------------------------------------------------- */

.ds-index {
    margin-top: var(--sp-3); padding: var(--sp-3);
    border-radius: var(--radius-md); background: var(--sunken);
    border: 1px solid var(--line);
}
.ds-index-head {
    display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    letter-spacing: 0.05em; text-transform: uppercase; color: var(--mute);
    margin-bottom: var(--sp-2);
}
.ds-index-row { margin-bottom: var(--sp-3); }
.ds-index-row:last-child { margin-bottom: 0; }
.ds-index-line {
    display: flex; justify-content: space-between; align-items: baseline;
    gap: var(--sp-2); margin-bottom: 5px;
}
.ds-index-name { font-family: var(--font-mono); font-size: var(--fs-dense); color: var(--ink); }
.ds-index-branch {
    font-family: var(--font-sans); font-size: var(--fs-micro);
    padding: 1px 6px; border-radius: var(--radius-full);
    background: var(--sunken); color: var(--soft);
}
.ds-index-count { font-size: var(--fs-micro); color: var(--soft); font-variant-numeric: tabular-nums; }
/* THE MARK AT THE FRONT: done, reading, or waiting. It is what makes the list read as
   a queue being worked through rather than a set of unrelated bars. */
.ds-index-mark { font-size: var(--fs-micro); margin-right: 6px; width: 12px; }
.ds-index-say { margin-top: 5px; font-size: var(--fs-micro); color: var(--soft); }
.ds-index-row--done .ds-index-mark { color: var(--pass); }
.ds-index-row--done .ds-index-name { color: var(--soft); }
/* The finished bar's colour is `ds-bar-fill--pass`, written by the renderer. A rule
   here as well would be two mechanisms for one fact, which is how they drift. */
/* A WAITING ROW DOES NOT ANIMATE. A bar sweeping back and forth means work is
   happening, and on a repository that has not started it means the opposite. */
.ds-index-row--queued { opacity: 0.62; }
.ds-index-row--queued .ds-index-mark { color: var(--mute); }
.ds-index-fail { color: var(--fail); }
.ds-index-ok { color: var(--pass); }
/* THE WHOLE SET, ON ONE LINE, beside the heading. Per-repository counts answer "how
   far is this one"; three repositories read in sequence leave the reader adding up. */
.ds-index-total {
    font-weight: var(--fw-regular); letter-spacing: 0; text-transform: none;
    color: var(--soft); font-variant-numeric: tabular-nums;
}
/* A ROW NOBODY ASKED FOR. Dimmer than a queued one and NOT animated: a sweeping bar
   over a repository with no task behind it is the lie this state exists to end. */
.ds-index-row--idle { opacity: 0.55; }
.ds-index-row--idle .ds-index-mark { color: var(--mute); }
.ds-index-idle { color: var(--soft); }
/* SILENT FOR LONGER THAN THE PHASE CAN EXPLAIN. Stated, not animated away. */
.ds-index-stuck { color: var(--st-waiting-ink); font-weight: var(--fw-semibold); }
.ds-index-row--stuck .ds-index-mark { color: var(--st-waiting-ink); }

@media (max-width: 860px) {
    .ds-repos-count { display: none; }
    .ds-repos-body { padding-left: var(--sp-2); }
    .ds-step { padding: var(--sp-3) var(--sp-2); }
}

/* One repository's block inside the checklist's collapsed panel. The gap comes from
   `ds-repos-body`; this exists so the head, the languages, the facts and the gaps are
   one thing a reader's eye can group, rather than four lines of equal weight. */
/* `minmax(0, 1fr)`, not the implicit `auto` track: an auto track is sized by its
   content's max-content width and will not go below it, so `min-width: 0` on the grid
   itself bought nothing -- the card stayed 148px wide inside a 134px box and painted
   over what was beside it. */
.ds-repo { display: grid; grid-template-columns: minmax(0, 1fr); gap: 2px; min-width: 0; }

/* ===========================================================================
 * 20. CONVENTIONS — two columns, because they are two jobs
 *
 * Reading what the code found is scanning; answering what it could not is
 * writing. The page's own comment records why they are side by side: the counts
 * said "8 from your code, 3 need you" and left the eye to walk eleven cards to
 * find which three.
 *
 * PORTED FROM `codepixel.css`, 54 rules, with every hex resolved to a token. The
 * mapping follows what the page says about itself: "Blue is information to check;
 * amber is attention wanted. Green says 'verified' and nobody has verified any of
 * it -- it was read out of a repository ten seconds ago." So the code column is
 * `running` (blue) and the needs-you column is `waiting` (amber), which is what
 * those two states mean everywhere else in the product.
 * ======================================================================== */

.ds-conv-lede {
    display: flex; align-items: flex-start; gap: var(--sp-2);
    margin: 0 0 var(--sp-3); padding: var(--sp-3);
    border-radius: var(--radius-md);
    background: var(--accent-soft);
    font-size: var(--fs-dense); line-height: var(--lh-normal); color: var(--soft);
}
.ds-conv-lede i { margin-top: 2px; color: var(--accent-text); }
.ds-conv-lede strong { color: var(--ink); }

/* EQUAL COLUMNS. Weighting the left made the column that needs work the narrower of
   the two, which is backwards: the boxes somebody has to write in are on the right. */
.ds-conv-board {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--sp-5);
    align-items: start;
}
.ds-conv-col { display: flex; flex-direction: column; gap: var(--sp-3); min-width: 0; }
.ds-conv-col-head {
    display: flex; align-items: center; gap: var(--sp-2);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--soft);
}
.ds-conv-col-head i { color: var(--st-running-ink); }
.ds-conv-col--you .ds-conv-col-head i { color: var(--st-waiting-ink); }
.ds-conv-col-count {
    margin-left: auto;
    padding: 2px var(--sp-2); border-radius: var(--radius-full);
    font-size: var(--fs-micro); letter-spacing: 0;
    background: var(--st-running-bg); color: var(--st-running-ink);
    font-variant-numeric: tabular-nums;
}
.ds-conv-col--you .ds-conv-col-count {
    background: var(--st-waiting-bg); color: var(--st-waiting-ink);
}
/* Nothing left to answer is not "attention wanted" any more. */
.ds-conv-col--you .ds-conv-col-count.is-clear {
    background: var(--st-running-bg); color: var(--st-running-ink);
}
.ds-conv-col-lede {
    margin: calc(var(--sp-1) * -1) 0 2px;
    font-size: var(--fs-micro); line-height: var(--lh-normal); color: var(--mute);
}
.ds-conv-col-empty {
    padding: var(--sp-4); border-radius: var(--radius-md);
    border: 1px dashed var(--line); font-size: var(--fs-dense); color: var(--soft);
}
.ds-conv-col-empty--ok {
    border-style: solid; border-color: var(--st-running-line);
    background: var(--st-running-bg); color: var(--st-running-ink);
}
.ds-conv-col-empty--ok i { margin-right: var(--sp-1); }

.ds-conv-card {
    padding: var(--sp-4);
    border-radius: var(--radius-md);
    background: var(--surface);
    border: 1px solid var(--line);
    /* The left edge carries the state, so scrolling reads as a list of what is done and
       what is not without stopping to read a chip. */
    border-left: 3px solid var(--st-running-ink);
}
.ds-conv-card.needs-you {
    border-left-color: var(--st-waiting-ink);
    background: var(--st-waiting-bg);
}
.ds-conv-card-head {
    display: flex; align-items: flex-start; justify-content: space-between;
    gap: var(--sp-3);
}
.ds-conv-card-head h2 { margin: 0; font-size: var(--fs-body); font-weight: var(--fw-semibold); }
.ds-conv-card-help { margin: 3px 0 0; font-size: var(--fs-dense); color: var(--soft); }

.ds-conv-chip {
    flex: none;
    padding: 3px var(--sp-2); border-radius: var(--radius-full);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold); letter-spacing: 0.02em;
}
.ds-conv-chip--ok { background: var(--st-running-bg); color: var(--st-running-ink); }
.ds-conv-chip--gap { background: var(--st-waiting-bg); color: var(--st-waiting-ink); }
.ds-conv-count { font-size: var(--fs-micro); opacity: 0.7; margin-left: 5px; }
.ds-conv-count:empty { display: none; }

.ds-conv-more {
    background: none; border: 0; padding: var(--sp-1) 0 0;
    font-size: var(--fs-micro); color: var(--accent-text); cursor: pointer;
}
.ds-conv-more:hover { text-decoration: underline; }
.ds-conv-item.is-hidden { display: none; }

/* THE QUESTION, not a restatement. These used to be IN the document text, which
   `for_agent()` sends verbatim -- so "**Not found in the code.** How is this built?"
   reached the agent looking like a convention. */
.ds-conv-question {
    margin: 0 0 var(--sp-2); padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--st-waiting-bg);
    font-size: var(--fs-dense); line-height: var(--lh-normal);
    color: var(--st-waiting-ink);
}

.ds-conv-items { display: flex; flex-direction: column; gap: var(--sp-1-5); margin-top: var(--sp-3); }
.ds-conv-card-foot { display: flex; gap: var(--sp-4); margin-top: var(--sp-3); flex-wrap: wrap; }
.ds-conv-item { display: flex; align-items: center; gap: var(--sp-2); }
.ds-conv-item-input {
    flex: 1; min-width: 0;
    padding: var(--sp-1-5) var(--sp-3); font-size: var(--fs-dense);
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    background: var(--surface); color: var(--ink);
}
.ds-conv-item-input:focus { outline: none; border-color: var(--accent); }
/* WHERE THE LINE CAME FROM. A lead accepts "CLAUDE.md" in two seconds; verifying an
   unattributed claim means going to look, which is the work this page exists to save. */
.ds-conv-item-from {
    flex: none; max-width: 190px; overflow: hidden; text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--fs-micro); color: var(--soft); font-family: var(--font-mono);
}
.ds-conv-item-from.is-yours { opacity: 0.5; font-style: italic; }
.ds-conv-item-remove {
    flex: none; background: none; border: 0; cursor: pointer;
    color: var(--mute); font-size: var(--fs-dense); padding: var(--sp-1);
}
.ds-conv-item-remove:hover { color: var(--fail); }

.ds-conv-note {
    width: 100%; min-height: 84px; max-height: 200px;
    padding: var(--sp-2) var(--sp-3); font-size: var(--fs-dense);
    line-height: var(--lh-normal);
    border: 1px solid var(--line); border-radius: var(--radius-sm);
    font-family: inherit; resize: vertical;
    background: var(--surface); color: var(--ink);
}
.ds-conv-note:focus { outline: none; border-color: var(--accent); }
/* The placeholder is the instruction. "Add a note" told somebody nothing about what a
   note is for; an example of a real convention tells them in one read. */
.ds-conv-note::placeholder { color: var(--mute); }

/* PROSE LIFTED OUT OF AN OLDER DOCUMENT, which was shown under a heading reading "In
   your words" -- so a lead was handed paragraphs they had never written and told they
   were theirs. */
.ds-conv-carried {
    margin: var(--sp-2) 0 0; padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    background: var(--st-waiting-bg);
    font-size: var(--fs-micro); line-height: var(--lh-normal);
    color: var(--st-waiting-ink);
}

@media (max-width: 1100px) {
    .ds-conv-board { grid-template-columns: minmax(0, 1fr); }
}
/* "In your words" / "Add your own" — the toggle that reveals a section's free-text box.
   A link, not a button: the lines above it are the structured answer and this is the
   escape hatch, and a second button beside "Add a convention" would read as a choice
   between equals. */
.ds-conv-note-toggle span { margin-left: 2px; }

/* The Implementation board: the tab strip, the panel and the per-repository talk, as
   one column. It had no rule -- the children each carry their own margins, so it looked
   right and was a wrapper nothing described. Said here, because a layout that works by
   accident is one somebody will break by changing a child. */
.ds-buildboard { display: flex; flex-direction: column; gap: var(--sp-4); min-width: 0; }

/* ===========================================================================
 * 21. THE PROJECT LIST, and setting several products up at once
 *
 * The first screen an account sees. `projects.css` had its own `project-list-*`,
 * `setup-*` and `origin-*` vocabularies for shapes the design system has; what
 * is genuinely particular is the setup grid, which is a form nothing else in
 * the product looks like, so it is ported rather than mapped.
 * ======================================================================== */

/* ONE DECISION, ONE SCREEN. Naming the work and saying where the code lives used
   to be two dialogs and a page reload apart -- five times over for a lead setting
   up five services. */
.ds-setupgrid { max-width: 780px; margin: 0 auto; text-align: left; }
.ds-setuprow {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr) auto auto;
    grid-template-areas: "name field add remove" "chips chips chips chips";
    gap: var(--sp-2);
    align-items: center;
    padding: var(--sp-3) 0;
    border-bottom: 1px solid var(--line);
}
.ds-setuprow-name { grid-area: name; }
.ds-setuprow-field { grid-area: field; }
.ds-setuprow-repos { grid-area: chips; display: flex; flex-wrap: wrap; gap: var(--sp-1); }
.ds-setuprow-remove {
    grid-area: remove;
    background: none; border: 0; cursor: pointer;
    color: var(--mute); font-size: var(--fs-lead); line-height: 1;
    padding: 0 var(--sp-1);
}
.ds-setuprow-remove:hover { color: var(--fail); }
.ds-setupchip {
    display: inline-flex; align-items: center; gap: var(--sp-1);
    padding: 2px var(--sp-2); border-radius: var(--radius-full);
    background: var(--accent-soft); color: var(--accent-text);
    font-size: var(--fs-micro); font-family: var(--font-mono);
}
.ds-setupchip button {
    background: none; border: 0; cursor: pointer; color: inherit;
    font-size: var(--fs-dense); line-height: 1;
}

/* THE ONE QUESTION WORTH ASKING AT CREATION. Not because the project needs it
   stored, but because every setup step after it reads differently depending on
   the answer: a repository to link or one to make, a stack detected or decided,
   conventions read from code or written before there is any, a board to import
   from or a product to describe. */
.ds-origin { display: grid; gap: var(--sp-2); margin-top: var(--sp-1); }
.ds-origin-option {
    display: flex; gap: var(--sp-2); align-items: flex-start;
    padding: var(--sp-3);
    border: 1px solid var(--line-firm); border-radius: var(--radius-md);
    cursor: pointer;
}
.ds-origin-option:hover { border-color: var(--accent); }
.ds-origin-option:has(input:checked) {
    border-color: var(--accent); background: var(--accent-soft);
}
.ds-origin-option input { margin-top: 3px; flex: none; }
.ds-origin-body { display: flex; flex-direction: column; gap: 2px; }
.ds-origin-title { font-size: var(--fs-dense); font-weight: var(--fw-semibold); color: var(--ink); }
.ds-origin-detail { font-size: var(--fs-dense); line-height: var(--lh-normal); color: var(--soft); }
@media (max-width: 720px) {
    .ds-setuprow {
        grid-template-columns: minmax(0, 1fr) auto;
        grid-template-areas: "name remove" "field add" "chips chips";
    }
}


/* ===========================================================================
 * 22. KNOWLEDGE — the documents a requirement retrieves from
 *
 * Replaces the half of static/css/context_engine.css that the two Knowledge screens
 * used. That file was 645 lines and most of it was a second set of names for things
 * the design system already had: `.ctx-head` for a page head, `.ctx-stat` for a stat
 * tile, `.ctx-chip` for a chip, `.ctx-tab` for a tab, `.ctx-overlay-sheet` for a
 * modal, `.ctx-empty` for an empty state. Those are gone; what is here is the four
 * things the system genuinely did not have.
 *
 * context_engine.css is still loaded by prd_view.html, skill_list.html and the
 * coverage badge in the chat header, so it stays on disk.
 * ======================================================================== */

/* --- 22.1 The drop zone ---------------------------------------------------
 * Choosing files, saying what kind they are, and what happens to them after they land.
 * The product had no file control at all, and this is the only screen that takes an
 * upload from a customer. */

.ds-drop {
    padding: var(--sp-4);
    background: var(--surface);
    border: 1px dashed var(--line-firm);
    border-radius: var(--radius-md);
    transition: border-color var(--dur-fast) var(--ease-out),
                background var(--dur-fast) var(--ease-out);
}

/* Dragging over it. The whole form is the target, not an inner rectangle: a drop zone
   you can miss by eight pixels is a drop zone that drops the file on the browser, which
   navigates away from the page. */
.ds-drop--over {
    border-color: var(--accent);
    border-style: solid;
    background: var(--accent-soft);
}

.ds-drop-row {
    display: flex;
    align-items: center;
    gap: var(--sp-3);
    flex-wrap: wrap;
}

.ds-drop-kind { width: auto; min-width: 11rem; }

.ds-drop-say {
    font-size: var(--fs-dense);
    color: var(--mute);
}

/* --- 22.2 The format badge ----------------------------------------------- */

/* WHY THE FORMAT IS A BADGE AND NOT A WORD. It decides whether the document landed
   verbatim or went through a converter -- which for a PDF read by OCR is the difference
   between a figure being right and being plausible. Six colours, from the state palette
   rather than six new ones: markdown passes through (pass), a spreadsheet is the one
   that needs checking (warn), the rest are simply converted (info). */
.ds-fmts { display: inline-flex; gap: var(--sp-1); flex-wrap: wrap; }

.ds-fmt {
    display: inline-flex; align-items: center;
    padding: 1px var(--sp-1-5);
    border: 1px solid var(--st-unknown-line);
    border-radius: var(--radius-xs);
    background: var(--st-unknown-bg);
    color: var(--st-unknown-ink);
    font-family: var(--font-mono);
    font-size: var(--fs-micro);
    font-weight: var(--fw-medium);
    letter-spacing: 0.02em;
    white-space: nowrap;
    flex: none;
}

.ds-fmt--md,
.ds-fmt--markdown,
.ds-fmt--txt {
    border-color: var(--st-passed-line);
    background: var(--st-passed-bg);
    color: var(--st-passed-ink);
}

.ds-fmt--xlsx,
.ds-fmt--csv {
    border-color: var(--st-waiting-line);
    background: var(--st-waiting-bg);
    color: var(--st-waiting-ink);
}

.ds-fmt--pdf,
.ds-fmt--docx,
.ds-fmt--pptx,
.ds-fmt--html {
    border-color: var(--st-running-line);
    background: var(--st-running-bg);
    color: var(--st-running-ink);
}

/* --- 22.3 The corpus ----------------------------------------------------- */

.ds-docs {
    display: grid;
    gap: var(--sp-4);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr));
}

/* THE LIST LAYOUT IS ONE COLUMN, not a table. The two views answer different questions
   -- "what is in here" wants the cards, "where is that one document" wants the column --
   and a card that reflows to full width answers the second without a second markup. */
.ds-docs--list { grid-template-columns: minmax(0, 1fr); }

/* A card holding an empty state must not be squeezed into one grid cell. */
.ds-docs > .ds-empty { grid-column: 1 / -1; }

.ds-doc { display: flex; flex-direction: column; }

.ds-doc .ds-card-title { min-width: 0; }

.ds-doc-facts {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1) var(--sp-3);
    font-size: var(--fs-meta);
    color: var(--mute);
}

/* A fact with nothing in it -- a document with no page count -- leaves no gap. Without
   this the row reads with a hole in it where the pages would be. */
.ds-doc-facts > span:empty { display: none; }

/* The ingest pipeline inside a card: five stages across about 18rem, so the labels wrap
   to the line under the dot rather than squeezing the rail. */
.ds-doc-stages {
    list-style: none;
    margin: 0;
    padding: 0;
    font-size: var(--fs-micro);
}

.ds-doc-systems:empty { display: none; }

/* --- 22.4 One excerpt ---------------------------------------------------- */

/* THE EXACT TEXT RETRIEVAL SEARCHES, which is a different thing from the document.
   A repository that shows the upload but not the index leaves a reader unable to tell a
   broken search from a section that chunked badly, so this is deliberately verbatim and
   deliberately monospace. */
.ds-excerpt {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
}

.ds-excerpt-head {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-2) var(--sp-3);
    background: var(--panel-bg);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-meta);
}

.ds-excerpt-path { color: var(--soft); min-width: 0; }

.ds-excerpt-ord {
    margin-left: auto;
    flex: none;
    font-family: var(--font-mono);
    color: var(--mute);
}

.ds-excerpt-body {
    padding: var(--sp-3);
    font-family: var(--font-mono);
    font-size: var(--fs-meta);
    line-height: var(--lh-normal);
    color: var(--ink);
    white-space: pre-wrap;
    /* An excerpt can be one 4,000-character paragraph with no spaces in a URL. */
    overflow-wrap: anywhere;
}

/* --- 22.5 Which systems, and where each is named ------------------------- */

.ds-sysindex { display: flex; flex-direction: column; gap: var(--sp-2); }

.ds-sysrow {
    display: grid;
    grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr);
    gap: var(--sp-2) var(--sp-3);
    align-items: baseline;
    padding-bottom: var(--sp-2);
    border-bottom: 1px solid var(--line-soft);
}

.ds-sysrow:last-child { border-bottom: none; padding-bottom: 0; }

.ds-sysrow-name {
    font-size: var(--fs-dense);
    font-weight: var(--fw-medium);
    color: var(--ink);
}

.ds-sysrow-where {
    display: flex;
    flex-wrap: wrap;
    gap: var(--sp-1);
    min-width: 0;
}

@media (max-width: 40rem) {
    .ds-sysrow { grid-template-columns: minmax(0, 1fr); }
}


/* ===========================================================================
 * 23. ONE SEARCH HIT — a passage retrieval found, and how well it matched
 *
 * The repository analytics screen's code search, and the only thing its 1,050-line
 * rebuild had to add to the system: everything else on that page was a head, a stat
 * tile, a card, a row, a drawer or a note that already existed. A hit is genuinely
 * different from all of them because it carries a verbatim fragment of the customer's
 * source and a number saying how confident the match is, and both have to be readable
 * at a glance in a list of ten.
 *
 * `ds-excerpt` (§22) is its sibling and deliberately not reused: that one is a passage
 * of a DOCUMENT, in order, with a heading path. This one is a passage of CODE, ranked,
 * with a file path and a line range.
 * ======================================================================== */

.ds-hit {
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--surface);
    overflow: hidden;
}

.ds-hit-head {
    display: flex;
    align-items: baseline;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    background: var(--panel-bg);
    border-bottom: 1px solid var(--line);
    font-size: var(--fs-meta);
}

.ds-hit-path {
    font-family: var(--font-mono);
    color: var(--ink);
    min-width: 0;
    /* The END of a path identifies the file; the start is twelve directories everything
       shares. `ds-trunc` clips the tail, so the direction is reversed here. */
    direction: rtl;
    text-align: left;
}

.ds-hit-lines {
    font-family: var(--font-mono);
    color: var(--mute);
    flex: none;
}

/* HOW CONFIDENT, not how relevant. The number is the backend's own exponential decay
   over the embedding distance, and it is at the right-hand end of the row rather than
   beside the path because a reader scans the paths first and only then asks which of
   them to trust. */
.ds-hit-score {
    margin-left: auto;
    flex: none;
    font-variant-numeric: tabular-nums;
    font-weight: var(--fw-medium);
    color: var(--accent-text);
}

/* The symbol the chunker named, when it named one. Absent for a whole-file chunk,
   which is what a file no parser understood becomes. */
.ds-hit-sym {
    padding: var(--sp-1-5) var(--sp-3) 0;
    font-family: var(--font-mono);
    font-size: var(--fs-micro);
    color: var(--soft);
}

.ds-hit-code {
    margin: 0;
    padding: var(--sp-2) var(--sp-3);
    font-family: var(--font-mono);
    font-size: var(--fs-micro);
    line-height: var(--lh-normal);
    color: var(--soft);
    white-space: pre;
    overflow-x: auto;
    /* A fragment of a minified file is one 400-character line. Scroll it rather than
       wrap it: wrapped code is unreadable, and the row would be eight lines tall. */
    max-height: 14rem;
    overflow-y: auto;
}

.ds-hit-foot {
    display: flex;
    align-items: center;
    gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    border-top: 1px solid var(--line-soft);
}

.ds-hit-foot > .ds-meta { margin-left: auto; font-family: var(--font-mono); }

/* SETUP IS NOT FINISHED, ON EVERY TAB.
   A band rather than a `ds-note`: the notes are page content and scroll away with it,
   and this has to be the first thing read on a screen somebody opened to do work the
   project cannot do yet. Removed entirely once setup is complete -- see ds_base.html. */
.ds-setupbar {
    display: flex; align-items: center; gap: var(--sp-3);
    padding: var(--sp-3) var(--sp-4);
    border: 1px solid var(--st-waiting-line);
    border-left: 3px solid var(--st-waiting-ink);
    border-radius: var(--radius-md);
    background: var(--st-waiting-bg);
}
.ds-setupbar-icon { flex: none; color: var(--st-waiting-ink); font-size: var(--fs-h3); }
.ds-setupbar-body { flex: 1 1 auto; min-width: 0; display: grid; gap: 2px; }
.ds-setupbar-title { font-size: var(--fs-dense); }
.ds-setupbar-say { font-size: var(--fs-meta); color: var(--soft); }
.ds-setupbar-go { flex: none; }
@media (max-width: 640px) {
    .ds-setupbar { flex-wrap: wrap; }
    .ds-setupbar-go { width: 100%; justify-content: center; }
}

/* THE WHOLE ROW IS THE LINK.
   The anchor covered its own two lines of text, so clicking a project card anywhere
   else -- the obvious thing to do with a list of cards -- did nothing at all. The
   stretched pseudo-element makes the row the hit area without nesting anything inside
   the anchor, which would be invalid where the row also holds a button and a menu.

   Everything after it needs a stacking context of its own or the overlay would sit on
   top of the controls and swallow their clicks. */
.ds-qrow:has(.ds-qrow-hit) { position: relative; }
.ds-qrow-hit::after {
    content: ''; position: absolute; inset: 0; z-index: 0;
}
/* ONLY THE SIBLINGS THAT HOLD A CONTROL. Lifting every one of them put the status
   chip -- which is a label, not a button -- on top of the overlay, so `elementFromPoint`
   over the middle of the row returned `.ds-qrow-chip` and the click did nothing. Found
   in a browser; the markup was right and no test could see it. */
.ds-qrow:has(.ds-qrow-hit) > *:not(.ds-qrow-hit):has(a, button, select, input, textarea) {
    position: relative; z-index: 1;
}
.ds-qrow:has(.ds-qrow-hit):hover { background: var(--raised); }
.ds-qrow-hit:focus-visible::after {
    outline: 2px solid var(--accent); outline-offset: -2px;
    border-radius: var(--radius-md);
}

/* ONE PANEL PER OTHER TAB, on Overview.
   A fixed minimum wider than `ds-grid--auto`'s 200px: these carry a sentence and up to
   three titles, and at 200px every one of them wrapped to four lines. */
.ds-glimpse { display: flex; flex-direction: column; }
.ds-glimpse .ds-card-body { flex: 1 1 auto; }
.ds-glimpse-say { margin: 0; }
.ds-glimpse-list { margin-top: var(--sp-2); }
.ds-glimpse-item {
    display: flex; align-items: baseline; gap: var(--sp-2);
    font-size: var(--fs-meta); min-width: 0;
}
.ds-glimpse-item .ds-meta { flex: none; }

/* WHERE THE ANALYSIS HAS GOT TO.
   A spinner for a run that takes minutes tells somebody only that the page has not
   crashed. The bar is a share of the five stages the funnel actually walks, the list
   names them, and the quiet line says when it has stopped moving -- which is the one
   thing a spinner can never say and the only thing worth knowing at minute four. */
/* The empty state while a run is going. `ds-empty` centres its children; the progress
   parts below need the same treatment and a little room from the sentence above. */
.ds-analysing { gap: var(--sp-2); }
.ds-analysing-bar {
    width: min(420px, 100%); height: 4px; margin-top: var(--sp-3);
    border-radius: var(--radius-full); background: var(--line); overflow: hidden;
}
.ds-analysing-fill {
    display: block; height: 100%; width: 0;
    background: var(--accent); border-radius: inherit;
    transition: width var(--dur-slow) var(--ease-out);
}
.ds-analysing-stages {
    display: flex; flex-wrap: wrap; justify-content: center;
    gap: var(--sp-1) var(--sp-3);
    margin: var(--sp-3) 0 0; padding: 0; list-style: none;
}
.ds-analysing-stage { font-size: var(--fs-micro); color: var(--mute); }
.ds-analysing-stage[data-state="done"] { color: var(--soft); }
.ds-analysing-stage[data-state="done"]::before { content: '✓ '; }
.ds-analysing-stage[data-state="now"] { color: var(--ink); font-weight: var(--fw-medium); }
.ds-analysing-quiet { color: var(--st-waiting-ink); }
