/*
 * 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 ============================ */
.ds-table-wrap { border: 1px solid var(--line); border-radius: var(--radius-md); overflow: hidden; 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; }

/* =========================== 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. Sized, not
       `auto`: the cell holds a number, a word and a confidence, and letting it size
       itself made every row in the list a different shape. */
    grid-template-columns: 18px minmax(0, 1fr) minmax(0, auto) 184px auto auto auto;
}
/* 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; }
    .ds-qrow--work .ds-qrow-scope { grid-column: 2 / -1; }
}
.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. */
    .ds-qrow--work { grid-template-columns: 18px minmax(0, 1fr) auto auto; }
    .ds-qrow--work .ds-qrow-repos,
    .ds-qrow--work .ds-qrow-owner { display: none; }
}

/* =========================== 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); }

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

/* THE STAGES IT HAS REACHED. A row of words, not a progress bar: a bar claims a
   proportion, and "cloning" is not 40% of anything. */
.ds-buildsteps { display: flex; flex-wrap: wrap; gap: var(--sp-2); padding: var(--sp-4) var(--sp-4) 0; }
.ds-buildstep {
    font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.05em;
    font-weight: var(--fw-semibold);
    padding: 2px var(--sp-2); border-radius: var(--radius-full);
    background: var(--panel-bg); color: var(--mute);
}
.ds-buildstep--done { background: var(--st-passed-bg); color: var(--st-passed-ink); }
.ds-buildstep--current { background: var(--st-running-bg); color: var(--st-running-ink); }
.ds-buildstep--failed { background: var(--st-failed-bg); color: var(--st-failed-ink); }
.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; }
.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));
}
.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; }
.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); }

.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. */
.ds-repo { display: grid; 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); }
