/*
 * ds/shell.css — the app frame: rail, page, header, tabs.
 * The frame every screen sits in. Knows nothing about what a screen contains.
 */

/* =========================== 1. Frame ============================== */
.ds-app {
    display: grid;
    grid-template-columns: var(--ds-rail-w) minmax(0, 1fr);
    min-height: 100vh;
    background: var(--ground);
}
.ds-app--rail-min { grid-template-columns: var(--ds-rail-w-min) minmax(0, 1fr); }

/* =========================== 2. Rail =============================== */
.ds-rail {
    grid-column: 1;
    display: flex;
    flex-direction: column;
    gap: var(--sp-1);
    background: var(--rail-bg);
    border-right: 1px solid var(--line);
    padding: var(--sp-3) var(--sp-2);
    position: sticky;
    top: 0;
    height: 100vh;
    overflow-y: auto;
    z-index: var(--z-rail);
}

.ds-rail-brand {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-1) var(--sp-2) var(--sp-3);
    min-width: 0;
}

/* The org / project switcher. A button, because it opens a menu. */
.ds-switch {
    display: flex; align-items: center; gap: var(--sp-2);
    width: 100%;
    padding: var(--sp-2);
    border-radius: var(--radius-sm);
    font-size: var(--fs-dense);
    font-weight: var(--fw-medium);
    color: var(--ink);
    min-width: 0;
    transition: background var(--dur-fast) var(--ease-out);
}
.ds-switch:hover { background: var(--sunken); }
.ds-switch[aria-expanded="true"] { background: var(--sunken); }
.ds-switch-id { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 0; text-align: left; }
.ds-switch-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The workspace, under the project rather than instead of it. Which project you are
   in changes every row below; which organisation owns it changes nothing you can
   see, so it is the caption and not the headline. */
.ds-switch-org { font-size: var(--fs-micro); color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-switch-caret { color: var(--mute); font-size: var(--fs-micro); flex: none; }

/* --------------------------- Menu ---------------------------------- *
 * A popover hung off a button. The product had none, so every choice between
 * named things was a page: `All projects` was a rail row that left the project
 * you were in to show you a grid of cards, and coming back meant going in again.
 * Switching is not a destination.
 */
.ds-menu {
    position: absolute;
    z-index: var(--z-popover);
    min-width: 232px; max-width: 320px;
    padding: var(--sp-1);
    background: var(--raised);
    border: 1px solid var(--line-firm);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-xl);
    max-height: min(70vh, 480px); overflow-y: auto;
    animation: ds-pop var(--dur-fast) var(--ease-out);
}
.ds-menu[hidden] { display: none !important; }
.ds-menu-group {
    padding: var(--sp-2) var(--sp-2) var(--sp-1);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    letter-spacing: 0.06em; text-transform: uppercase; color: var(--mute);
}
.ds-menu-item {
    display: flex; align-items: center; gap: var(--sp-2);
    width: 100%;
    padding: var(--sp-2);
    border-radius: var(--radius-sm);
    font-size: var(--fs-dense);
    color: var(--soft);
    min-width: 0;
}
.ds-menu-item:hover { background: var(--sunken); color: var(--ink); text-decoration: none; }
.ds-menu-item[aria-current="true"] { color: var(--ink); font-weight: var(--fw-medium); }
.ds-menu-item-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ds-menu-item-mark { flex: none; color: var(--accent-text); font-size: var(--fs-micro); }
.ds-menu-sep { height: 1px; margin: var(--sp-1) 0; background: var(--line); }

/* The rail is the menu's containing block, so a menu pinned under the switcher
   does not move when the page behind it scrolls. */
.ds-rail { position: sticky; }
.ds-rail-switch { position: relative; }
.ds-rail-switch > .ds-menu { top: calc(100% + 4px); left: 0; right: 0; }

/* Group heading inside the rail. The current product has four UNLABELLED
   groups, which is how eleven rows read as one undifferentiated list. */
.ds-rail-group {
    display: flex; align-items: center; justify-content: space-between;
    gap: var(--sp-2);
    padding: var(--sp-4) var(--sp-2) var(--sp-1);
    font-size: var(--fs-micro);
    font-weight: var(--fw-semibold);
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--mute);
}

.ds-navlink {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2);
    border-radius: var(--radius-sm);
    font-size: var(--fs-dense);
    color: var(--soft);
    min-width: 0;
    transition: background var(--dur-fast) var(--ease-out),
                color var(--dur-fast) var(--ease-out);
}
.ds-navlink:hover { background: var(--sunken); color: var(--ink); text-decoration: none; }
.ds-navlink.is-active { background: var(--accent-soft); color: var(--accent-text); font-weight: var(--fw-medium); }
.ds-navlink-icon { flex: none; width: 16px; text-align: center; font-size: var(--fs-meta); }
.ds-navlink-text { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* A count on a rail row. Never zero: a zero badge is noise that teaches people
   to stop reading badges. Hide it at the source rather than rendering "0". */
.ds-navlink-count {
    flex: none;
    font-size: var(--fs-micro);
    font-weight: var(--fw-semibold);
    font-variant-numeric: tabular-nums;
    color: var(--mute);
    background: var(--sunken);
    border-radius: var(--radius-full);
    padding: 0 var(--sp-1-5);
    min-width: 18px;
    text-align: center;
}
.ds-navlink-count--attention { color: var(--st-waiting-ink); background: var(--st-waiting-bg); }
.ds-navlink-count--urgent    { color: var(--st-blocked-ink); background: var(--st-blocked-bg); }

.ds-rail-foot { margin-top: auto; padding-top: var(--sp-3); }

/* THE RAIL IS NARROW UNTIL YOU POINT AT IT.
   ---------------------------------------------------------------------------
   Icons only by default, labels on hover or on keyboard focus. No control and no
   remembered preference: a toggle is a decision to make and a thing to find, and
   the rail is navigation, not a destination -- it should be out of the way until
   it is wanted and then be fully readable.

   `.ds-app--rail-min` had been in this stylesheet all along with nothing applying
   it, so the narrow rail was styling for a state the product could not reach. It
   is applied on the shell now and is the only state.

   HIDING IS SCOPED TO `:not(:hover):not(:focus-within)`, so opening the rail is a
   matter of ceasing to hide. Re-showing each label explicitly would mean naming a
   `display` value for every one of them, and getting a single one wrong turns an
   inline label into a block that pushes its own row apart.

   `:focus-within` is not decoration: a rail that opens only to a pointer is a rail
   a keyboard cannot read, and the labels are the only thing telling seven icons
   apart. */
.ds-rail:not(:hover):not(:focus-within) .ds-navlink-text,
.ds-rail:not(:hover):not(:focus-within) .ds-rail-group,
.ds-rail:not(:hover):not(:focus-within) .ds-switch-id,
.ds-rail:not(:hover):not(:focus-within) .ds-switch-caret { display: none; }
/* A menu anchored to a 56px rail would be 56px wide. Let it out. */
.ds-app--rail-min .ds-rail-switch > .ds-menu { right: auto; left: 0; min-width: 232px; }
.ds-rail:not(:hover):not(:focus-within) .ds-navlink { justify-content: center; }

/* NOTHING IN THE NARROW RAIL THAT NEEDS WORDS TO MEAN ANYTHING.
   Hiding only `.ds-navlink-text` left three things behind that are text and nothing
   else -- the "New work" button clipped to "New w", the "Recent" heading cut to
   "RECE", and "No conversations yet." wrapped over three lines. A 56px column cannot
   carry a sentence, and a clipped one reads as a rendering fault.

   The two controls keep their `＋`, which is the whole of what they mean. The
   conversation list goes: a list of titles has no icon form, and the rail opens on
   hover anyway. */
.ds-rail:not(:hover):not(:focus-within) .ds-rail-new {
    padding-left: 0; padding-right: 0; justify-content: center;
}
.ds-rail:not(:hover):not(:focus-within) .ds-convlist,
.ds-rail:not(:hover):not(:focus-within) .ds-railconv-none { display: none; }
.ds-rail:not(:hover):not(:focus-within) .ds-railconv-head { justify-content: center; }
.ds-rail:not(:hover):not(:focus-within) .ds-navlink-count {
    position: absolute; transform: translate(10px, -10px);
    min-width: 8px; height: 8px; padding: 0; overflow: hidden; color: transparent;
}
.ds-app--rail-min .ds-navlink { position: relative; }

/* OPENS OVER THE PAGE, NOT BY PUSHING IT. The grid column stays at 56px while the
   rail itself widens, so the content behind it does not reflow -- a sidebar that
   re-wraps the paragraph you are reading every time the pointer crosses it is worse
   than one that never opens at all. */
.ds-app--rail-min .ds-rail {
    width: var(--ds-rail-w-min);
    transition: width .14s ease;
}
.ds-app--rail-min .ds-rail:hover,
.ds-app--rail-min .ds-rail:focus-within {
    width: var(--ds-rail-w);
    box-shadow: var(--shadow-2, 0 8px 24px rgba(0, 0, 0, .18));
}
@media (prefers-reduced-motion: reduce) {
    .ds-app--rail-min .ds-rail { transition: none; }
}
/* Touch has no hover. A rail that can only be opened by pointing is unusable where
   there is no pointer, so there it stays wide. */
@media (hover: none) {
    .ds-app--rail-min { grid-template-columns: var(--ds-rail-w) minmax(0, 1fr); }
    .ds-app--rail-min .ds-rail { width: var(--ds-rail-w); }
    .ds-rail .ds-navlink-text, .ds-rail .ds-rail-group,
    .ds-rail .ds-switch-id, .ds-rail .ds-switch-caret { display: revert; }
}

/* =========================== 3. Page =============================== */
.ds-main { grid-column: 2; min-width: 0; display: flex; flex-direction: column; }

.ds-page {
    width: 100%;
    max-width: var(--ds-page-max);
    margin: 0 auto;
    padding: var(--sp-6) var(--sp-6) var(--sp-12);
    min-width: 0;
}
.ds-page--wide  { max-width: none; }
.ds-page--flush { padding: 0; max-width: none; }

/* =========================== 4. Header ============================= */
.ds-head {
    display: flex;
    flex-direction: column;
    gap: var(--sp-2);
    padding-bottom: var(--sp-4);
    margin-bottom: var(--sp-5);
    border-bottom: 1px solid var(--line);
}

.ds-crumbs {
    display: flex; align-items: center; flex-wrap: wrap;
    gap: var(--sp-1);
    font-size: var(--fs-meta);
    color: var(--mute);
    min-width: 0;
}
.ds-crumbs a { color: var(--mute); }
.ds-crumbs a:hover { color: var(--ink); }
.ds-crumb-sep { color: var(--mute); }
.ds-crumbs [aria-current="page"] { color: var(--soft); }

.ds-head-main { display: flex; align-items: flex-start; gap: var(--sp-3); min-width: 0; }
.ds-head-main > .ds-head-actions { margin-left: auto; }
.ds-title { font-size: var(--fs-h2); min-width: 0; }
/*
 * ONE LINE. A Jira summary is a sentence, and this one ran to 108 characters: two
 * lines of 28px type across the full width, above a strip that says the key again
 * and a tab bar that says what the screen is. The whole first screenful was the
 * title of the thing rather than anything about it.
 *
 * Clamped rather than shortened in Python, so the full sentence is still selectable,
 * still searchable in the page, and still read out in full by a screen reader.
 */
.ds-title--line {
    font-size: var(--fs-h3);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    display: block; width: 100%;
}
.ds-head-id { display: flex; align-items: center; gap: var(--sp-2); flex-wrap: wrap; min-width: 0; }
.ds-head-key {
    font-family: var(--font-mono);
    font-size: var(--fs-dense);
    font-weight: var(--fw-medium);
    color: var(--soft);
}
.ds-head-actions { display: flex; align-items: center; gap: var(--sp-2); flex: none; }
.ds-head-sub { font-size: var(--fs-body); color: var(--soft); max-width: var(--ds-prose-max); }

/* The facts strip: a few labelled numbers under the title. */
.ds-head-facts { display: flex; flex-wrap: wrap; gap: var(--sp-2) var(--sp-5); }
.ds-fact { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.ds-fact-k { font-size: var(--fs-micro); text-transform: uppercase; letter-spacing: 0.05em; color: var(--mute); }
.ds-fact-v { font-size: var(--fs-dense); font-weight: var(--fw-medium); font-variant-numeric: tabular-nums; }

/* =========================== 5. Tabs ============================== */
.ds-tabs {
    display: flex; align-items: center; gap: var(--sp-1);
    border-bottom: 1px solid var(--line);
    margin-bottom: var(--sp-5);
    overflow-x: auto;
    scrollbar-width: none;
}
.ds-tabs::-webkit-scrollbar { display: none; }

.ds-tab {
    display: inline-flex; align-items: center; gap: var(--sp-2);
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--fs-dense);
    font-weight: var(--fw-medium);
    color: var(--mute);
    border-bottom: 2px solid transparent;
    margin-bottom: -1px;
    white-space: nowrap;
    transition: color var(--dur-fast) var(--ease-out),
                border-color var(--dur-fast) var(--ease-out);
}
.ds-tab:hover { color: var(--ink); text-decoration: none; }
.ds-tab[aria-selected="true"] { color: var(--accent-text); border-bottom-color: var(--accent-text); }
.ds-tab-count {
    font-size: var(--fs-micro);
    font-variant-numeric: tabular-nums;
    color: var(--mute);
    background: var(--sunken);
    border-radius: var(--radius-full);
    padding: 0 6px;
}

/* A tab panel must lose to [hidden]. The last design system shipped
   `.panel { display: flex }`, which beat the user-agent rule, so every tab
   selected correctly and no panel ever switched. */
.ds-tabpanel[hidden] { display: none !important; }

/* Stage dots — the lifecycle rail, as a tablist rather than five URLs. */
.ds-stages { display: flex; align-items: center; gap: 0; min-width: 0; }
.ds-stage {
    display: flex; flex-direction: column; align-items: center; gap: var(--sp-1);
    flex: 1 1 0; min-width: 0; position: relative;
    font-size: var(--fs-micro);
    color: var(--mute);
    padding-top: var(--sp-1);
}
.ds-stage-dot {
    width: 11px; height: 11px; border-radius: var(--radius-full);
    border: 2px solid var(--line-firm);
    background: var(--surface);
    z-index: 1;
}
/* The connector sits behind the dots, drawn from each stage except the first. */
.ds-stage:not(:first-child)::before {
    content: ""; position: absolute; top: calc(var(--sp-1) + 5px); right: 50%; left: -50%;
    height: 2px; background: var(--line); z-index: 0;
}
.ds-stage--done .ds-stage-dot { background: var(--pass); border-color: var(--pass); }
.ds-stage--done::before { background: var(--pass); }
.ds-stage--current .ds-stage-dot { border-color: var(--accent-text); background: var(--accent-fill); box-shadow: 0 0 0 3px var(--accent-soft); }
.ds-stage--current { color: var(--ink); font-weight: var(--fw-semibold); }
.ds-stage--blocked .ds-stage-dot { background: var(--st-blocked-ink); border-color: var(--st-blocked-ink); }
.ds-stage--waiting .ds-stage-dot { background: var(--st-waiting-ink); border-color: var(--st-waiting-ink); }
/* A STEP THAT DIED. The set had done, current, waiting and blocked -- four states, none
   of which is "this one failed", so a pipeline that stopped could only be drawn as one
   that had not started. Knowledge's ingest has five steps and any of them can throw. */
.ds-stage--failed .ds-stage-dot { background: var(--fail); border-color: var(--fail); }
.ds-stage--failed { color: var(--fail); font-weight: var(--fw-semibold); }
.ds-stage--failed::before { background: var(--line); }

/* =========================== 6. Narrow =========================== */
@media (max-width: 1024px) {
    .ds-app { grid-template-columns: var(--ds-rail-w-min) minmax(0, 1fr); }
    .ds-app .ds-navlink-text,
    .ds-app .ds-rail-group,
    .ds-app .ds-switch-id,
    .ds-app .ds-switch-caret { display: none; }
    .ds-app .ds-navlink { justify-content: center; }
    .ds-app .ds-rail-switch > .ds-menu { right: auto; left: 0; min-width: 232px; }
    .ds-page { padding: var(--sp-5) var(--sp-4) var(--sp-10); }
}
@media (max-width: 680px) {
    .ds-app { grid-template-columns: minmax(0, 1fr); }
    .ds-main { grid-column: 1; }
    .ds-page { padding: var(--sp-4) var(--sp-4) var(--sp-10); }
    .ds-head-main { flex-wrap: wrap; }
    /* `width: 100%` gave the row its own line; `flex-wrap` lets the buttons IN it take
       a second one. The Review and Analysis headers carry three -- Tracker, Trace and
       "Everything on one page" -- which is 312px, so on a 320px screen the last one hung
       8px off the right edge and took the whole page sideways with it. */
    .ds-head-main > .ds-head-actions { margin-left: 0; width: 100%; flex-wrap: wrap; }
    /* The rail becomes a drawer. Its rules are at the foot of this file -- see
       "10. The phone" -- because they have to beat the icons-only block at 1024px,
       and a media query adds no specificity, only source order. */
}

/* =========================== 7. Rail extras ======================= */
/* The primary action sits above the nav rows, the way Linear puts New issue
   above Inbox: creating work is not a destination, so it is not a nav row. */
.ds-rail-new { margin: var(--sp-1) 0 var(--sp-3); }
.ds-rail-new:hover { text-decoration: none; }

/* Three-way appearance control. Icon-only, because the rail cannot afford three
   words -- each button keeps a visually-hidden label for screen readers. */
.ds-rail-theme { padding: var(--sp-2) var(--sp-1) var(--sp-1); }
.ds-rail-theme .ds-seg { width: 100%; }
.ds-rail-theme .ds-seg > button { flex: 1 1 0; text-align: center; padding: var(--sp-1); }

.ds-app--rail-min .ds-rail-theme,
.ds-app--rail-min .ds-rail-new .ds-navlink-text { display: none; }

@media (max-width: 1024px) {
    .ds-rail-theme { display: none; }
}

/* =========================== 8. Split ============================ */
/*
 * Content and its context, side by side. The aside is a fixed share rather than a
 * fraction so a long monospace branch name in it cannot steal width from the
 * reading column -- which is the failure the three-column review pane had.
 */
.ds-split-main { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: var(--sp-5); align-items: start; min-width: 0; }
@media (max-width: 1100px) {
    .ds-split-main { grid-template-columns: minmax(0, 1fr); }
}

/* ======================================================================
   9. The issue frame — added when the five stages became five URLs
      sharing one header. See templates/includes/ds_issue_frame.html.
   ====================================================================== */

/* The title and the sentence under it stack. `.ds-head-id` was a row holding a
   title and an optional key; it now holds a title and WHAT IS BEING WAITED ON,
   which is a sentence and belongs on its own line. */
.ds-head-id { flex-direction: column; align-items: flex-start; gap: var(--sp-1); flex: 1 1 auto; }

/* `lifecycle` writes this line for every screen. On the analysis page it was
   rendering inside the information dialog, so the one sentence that says what
   happens next was behind an icon. Coloured by health, because "the agent is
   working" and "stopped for an approval" are not the same news. */
.ds-head-say {
    font-size: var(--fs-dense);
    color: var(--soft);
    display: flex; align-items: baseline; gap: var(--sp-2);
    max-width: var(--ds-prose-max);
}
/* `ds_issue_frame` always writes a modifier -- `ds-head-say--{{ stage.health }}` -- so
   the HEALTHY case needs a rule as much as the other two. It worked by being the
   absence of one, which is fine until somebody reads the stylesheet to find out what
   the three states look like and finds two. */
.ds-head-say--healthy { color: var(--soft); }
.ds-head-say--blocked { color: var(--st-waiting-ink); }
.ds-head-say--failed { color: var(--st-failed-ink); }

/* A tab that is a LINK marks itself with aria-current, not aria-selected:
   aria-selected means something only inside a tablist, and five URLs are not
   one. Same paint, correct semantics. */
.ds-tab[aria-current="page"] { color: var(--accent-text); border-bottom-color: var(--accent-text); }
.ds-tab[aria-disabled="true"] { color: var(--mute); opacity: 0.55; cursor: default; }
.ds-tab[aria-disabled="true"]:hover { color: var(--mute); }

/* One gap, named, instead of a style attribute per call site. */
.ds-gap-below { margin-bottom: var(--sp-5); }

/* A heading for a view that is NOT a stage -- the audit chain, a whole-object page --
   where the frame above already carries the issue's own `h1`. Smaller than `.ds-title`
   on purpose: two headings at the same size on one screen is two things claiming to be
   what the page is about. */
.ds-title--sub { font-size: var(--fs-h3); }

/* NINE TABS, NOT FIVE. The issue frame's five stages fit any window; Project setup has
   nine and they are words rather than short stage names, so on a narrow window the
   strip scrolls -- which `.ds-tabs` already allows -- and the icons go, because an icon
   beside a word is decoration and the word is the navigation. */
.ds-tabs--setup { gap: var(--sp-0-5); }
@media (max-width: 1024px) {
    .ds-tabs--setup .ds-tab > i { display: none; }
    .ds-tabs--setup .ds-tab { padding-left: var(--sp-2); padding-right: var(--sp-2); }
}

/* ===========================================================================
 * THE CONVERSATIONS PANE
 *
 * Chat is a two-column screen inside the shell: the conversations in this
 * project, and the one you are in. The list used to come from
 * `sidebar.html with show_conversations=True` -- the same include that drew the
 * app's navigation -- so a screen could not have the list without the whole
 * sidebar, and the chat page was the only one in the product with its own
 * `<body>` partly for that reason.
 *
 * `ds-page--flush` removes the page padding, so this fills the main column and
 * the message list scrolls inside it rather than the window scrolling.
 * ======================================================================== */
/* ONE COLUMN. The grid's first track was a 260px conversation pane, which put a
   second vertical list immediately beside the app's rail -- two left-hand navigations
   on the one screen, neither explaining the other. The list lives under the rail's
   Conversations row now (`ds_rail.html`), so this is the conversation and nothing
   else. */
.ds-page--flush.app-container {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    height: 100vh;
    min-height: 0;
}
.ds-convlist { flex: 1; min-height: 0; overflow-y: auto; padding: var(--sp-2); }
/* The rows `chat.js` writes. It owns the markup, so the selectors follow what it
   emits rather than the other way round -- renaming them here would be renaming
   something a script that is not in this file agrees about. */
.ds-convlist .conversation-item {
    display: block; padding: var(--sp-2) var(--sp-3);
    border-radius: var(--radius-sm);
    font-size: var(--fs-dense); color: var(--soft);
    cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.ds-convlist .conversation-item:hover { background: var(--sunken); color: var(--ink); }
.ds-convlist .conversation-item.active {
    background: var(--accent-soft); color: var(--accent-text);
    font-weight: var(--fw-medium);
}
.ds-convlist .empty-message { padding: var(--sp-3); font-size: var(--fs-dense); color: var(--mute); }

/* The chat column itself: header, banner, message list, composer. Only the list
   scrolls. */
.app-container > .chat-container {
    display: flex; flex-direction: column; min-height: 0; min-width: 0;
}

/* THE CONVERSATIONS, IN THE RAIL.
   Under the row that starts a new one, which is the row they are about. Capped and
   scrollable: this sits beneath seven navigation rows and a switcher, and a rail that
   scrolls as a whole is a rail people stop reading the bottom of. */
/* FENCED ON BOTH SIDES. With a rule only above it, the row below -- Knowledge -- read
   as the last item of the conversation group rather than the next navigation row. */
.ds-railconv {
    display: flex; flex-direction: column; min-height: 0;
    margin: var(--sp-2) 0;
    padding: var(--sp-2) 0;
    border-top: 1px solid var(--line);
    border-bottom: 1px solid var(--line);
}
.ds-railconv-head {
    display: flex; align-items: center; gap: var(--sp-2);
    padding: 0 var(--sp-3) var(--sp-1);
    font-size: var(--fs-micro); font-weight: var(--fw-semibold);
    letter-spacing: 0.05em; text-transform: uppercase; color: var(--mute);
}
.ds-railconv-head > span { margin-right: auto; }
.ds-railconv-new {
    border: 0; background: none; cursor: pointer;
    color: var(--mute); font-size: var(--fs-dense); line-height: 1;
    padding: 2px 4px; border-radius: var(--radius-xs);
}
.ds-railconv-new:hover { background: var(--sunken); color: var(--ink); }
.ds-railconv-list { max-height: 22vh; padding: 0; }
/* TWO MARKUPS, ONE ROW. The server renders an `<a>`; `chat.js` renders a `<div>`
   holding a title and a delete button, and it owns that markup -- the selectors here
   follow what it emits rather than the other way round. A `display: block` row made
   the delete button wrap onto a second line, which in a 180px rail reads as a broken
   list item. */
.ds-railconv-item, .ds-convlist .conversation-item {
    display: flex; align-items: center; gap: var(--sp-1);
    padding: var(--sp-1) var(--sp-3);
    border-radius: var(--radius-sm);
    font-size: var(--fs-meta); color: var(--soft);
    cursor: pointer; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    text-decoration: none;
}
.ds-convlist .conversation-title {
    flex: 1 1 auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* Shown on hover and on keyboard focus: a delete button beside every row is a
   mis-click waiting to happen, and one that never appears is unreachable. */
.ds-convlist .delete-conversation {
    flex: none; border: 0; background: none; cursor: pointer;
    color: var(--mute); font-size: var(--fs-micro); line-height: 1;
    padding: 2px; border-radius: var(--radius-xs);
    opacity: 0; transition: opacity var(--dur-fast) var(--ease-out);
}
.ds-convlist .conversation-item:hover .delete-conversation,
.ds-convlist .delete-conversation:focus-visible { opacity: 1; }
.ds-convlist .delete-conversation:hover { color: var(--st-failed-ink); }
.ds-railconv-item:hover { background: var(--sunken); color: var(--ink); }
.ds-railconv-item.is-active {
    background: var(--accent-soft); color: var(--accent-text);
    font-weight: var(--fw-medium);
}
.ds-railconv-none {
    margin: 0; padding: var(--sp-1) var(--sp-3);
    font-size: var(--fs-meta); color: var(--mute);
}


/* ======================================================================
   10. The phone
   ======================================================================
 * Below 680px the rail leaves the grid and becomes a drawer over the page. That much
 * was already written. What was not written anywhere was the way back IN: the rail was
 * parked at `translateX(-100%)` and `.ds-rail.is-open` was the only thing that brought
 * it back, and `is-open` appeared in no template and no script. So on a phone the
 * project switcher, every project row, Settings, the account and the appearance control
 * were off the left edge of the screen with nothing on any page able to reach them --
 * the whole of the product's navigation, on every signed-in screen.
 *
 * `.ds-topbar` in ds_base.html carries the button, `ds-ui.js` carries the class, and a
 * scrim closes it. The block sits at the end of the file ON PURPOSE: section 6 hides
 * every rail label at 1024px and section 7 hides the theme control there, and an open
 * 208px drawer has to undo both. Equal specificity, so later wins.
 */
@media (max-width: 680px) {
    .ds-rail,
    .ds-app--rail-min .ds-rail {
        position: fixed;
        inset: 0 auto 0 0;
        width: var(--ds-rail-w);
        height: 100%;
        transform: translateX(-100%);
        transition: transform var(--dur-base) var(--ease-out);
        box-shadow: none;
        z-index: calc(var(--z-drawer) + 1);
    }
    .ds-rail.is-open,
    .ds-app--rail-min .ds-rail.is-open {
        transform: none;
        box-shadow: var(--shadow-lg);
    }
    @media (prefers-reduced-motion: reduce) {
        .ds-rail, .ds-app--rail-min .ds-rail { transition: none; }
    }

    /* An open drawer is 208px wide and is read with words. `display: revert` rather than
       a named value for each: section 2 says why -- guessing `inline` for something the
       UA makes a block turns a label into a row of its own. */
    .ds-app .ds-navlink-text,
    .ds-app .ds-rail-group,
    .ds-app .ds-switch-id,
    .ds-app .ds-switch-caret,
    .ds-rail:not(:hover):not(:focus-within) .ds-navlink-text,
    .ds-rail:not(:hover):not(:focus-within) .ds-rail-group,
    .ds-rail:not(:hover):not(:focus-within) .ds-switch-id,
    .ds-rail:not(:hover):not(:focus-within) .ds-switch-caret { display: revert; }

    .ds-app .ds-navlink,
    .ds-rail:not(:hover):not(:focus-within) .ds-navlink { justify-content: flex-start; }
    .ds-rail:not(:hover):not(:focus-within) .ds-rail-new {
        padding-left: var(--sp-2); padding-right: var(--sp-2);
    }
    .ds-rail:not(:hover):not(:focus-within) .ds-convlist,
    .ds-rail:not(:hover):not(:focus-within) .ds-railconv-none { display: revert; }
    .ds-rail:not(:hover):not(:focus-within) .ds-railconv-head { justify-content: space-between; }
    /* Back to a readable count. The narrow rail shrinks it to an 8px dot with
       `color: transparent`, which in a 208px drawer is a badge with no number in it. */
    .ds-rail:not(:hover):not(:focus-within) .ds-navlink-count {
        position: static; transform: none;
        min-width: 18px; height: auto; padding: 0 var(--sp-1-5);
        overflow: visible; color: var(--mute);
    }
    .ds-rail:not(:hover):not(:focus-within) .ds-navlink-count--attention { color: var(--st-waiting-ink); }
    .ds-rail:not(:hover):not(:focus-within) .ds-navlink-count--urgent { color: var(--st-blocked-ink); }
    /* Section 7 hides this at 1024px, where the rail is 56px of icons. In the drawer it
       is the only appearance control a phone has. Written with `.ds-app--rail-min` in
       front because section 7 also hides it at that specificity, and no amount of
       source order beats one extra class. */
    .ds-app--rail-min .ds-rail-theme,
    .ds-rail-theme { display: revert; }
    .ds-app--rail-min .ds-rail-new .ds-navlink-text { display: revert; }

    .ds-rail-scrim { z-index: var(--z-drawer); }
}

/* The bar that carries the drawer's button. Not rendered at all above 680px, where the
   rail is in the grid and needs no opening. */
.ds-topbar { display: none; }

@media (max-width: 680px) {
    .ds-topbar {
        display: flex;
        align-items: center;
        gap: var(--sp-2);
        position: sticky;
        top: 0;
        z-index: var(--z-sticky);
        padding: var(--sp-2) var(--sp-3);
        background: var(--rail-bg);
        border-bottom: 1px solid var(--line);
    }
    .ds-topbar-menu {
        display: inline-flex; align-items: center; justify-content: center;
        flex: none;
        width: 40px; height: 40px;
        padding: 0;
        background: none;
        border: 1px solid transparent;
        border-radius: var(--radius-sm);
        color: var(--ink);
        font-size: var(--fs-lead);
        cursor: pointer;
    }
    .ds-topbar-menu:hover { background: var(--sunken); }
    .ds-topbar-menu:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
    .ds-topbar-brand {
        display: inline-flex; align-items: center; gap: var(--sp-2);
        min-width: 0;
        /* The bar is 40px and so is the link in it: a 24px line of text next to a
           40px button is a target that misses as often as it lands. */
        min-height: 40px;
        padding-right: var(--sp-2);
        color: var(--ink);
        font-weight: var(--fw-semibold);
        text-decoration: none;
    }
    .ds-topbar-brand:hover { text-decoration: none; }
    .ds-topbar-mark {
        display: inline-flex; align-items: center; justify-content: center;
        flex: none;
        width: 1.5rem; height: 1.5rem;
        border-radius: var(--radius-md);
        background: var(--accent-fill);
        color: var(--on-accent);
        font-size: var(--fs-micro);
    }
    .ds-topbar-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
}
