/* Left-rail navigation chrome — the `rail` ui_layout.
 *
 * Markup in app/web/templates/_app_rail.html; activated by
 * `html[data-ui-layout="rail"]` (stamped by base_ds.html from
 * instance.ui_layout / AGNES_UI_LAYOUT). Loaded globally but inert
 * under the default `topnav` layout — every rule is scoped to the
 * activation attribute, so topnav instances render byte-identically.
 *
 * All colors/typography come from `--ds-*` tokens so the rail works
 * under any palette theme (paper, blue, navy, dark). Widths and
 * paddings mirror the issue-#896 prototype (240px rail, 9px item
 * radius, soft active tint).
 *
 * Layout: two fixed zones with one scrolling list between them.
 *   [top]     New chat
 *   (scroll)  the conversations — as many as the free space fits,
 *             scrolling inside it; never truncated, no expand toggle
 *   [bottom]  Library · Agents ─ Admin, then the Set up Agnes card,
 *             then the profile in the foot
 * EVERY row shares one height (`--rail-row-h`) and one treatment, New chat
 * included — it is an ordinary `.rail-i`, not a control. See the "New chat"
 * section for the three tinted variants that were tried there and why all of
 * them are retired.
 *
 * COLOUR RULE — the accent marks WHERE YOU ARE, and nothing else:
 *   `--rail-active-bg`  → every "you are here" fill (accent tint)
 *   `--rail-hover-bg`   → hover, on every hoverable row (neutral wash)
 *   nothing standing    → no row carries a resting tint of its own
 *
 * The rail has held every version of this rule, so the reasoning matters more
 * than the values:
 *
 *   1. accent on active — broke once New chat took a tint of its own, because
 *      the accent then carried four meanings at once (the button, the active
 *      nav row, the active conversation, the active admin link) and the column
 *      stopped saying which of them you could act on;
 *   2. accent on New chat, neutral active — cost the accent its use in hover,
 *      since a hovered row and the resting button were the same pale blue;
 *   3. accent on hover, neutral active — worked, but spent colour on the one
 *      state the user needs no help identifying: the pointer is already there;
 *   4. accent on active (current), now that New chat is an ordinary row and
 *      nothing competes for it.
 *
 * (1) and (4) are the same rule; what makes it viable now is that New chat has
 * no standing tint. Active is the better owner than hover because it is
 * PERSISTENT wayfinding — you locate yourself in the column at a glance,
 * without reading — where hover is transient feedback that only has to be
 * perceptible. It also lands the accent on New chat for free on /chat, the
 * landing surface, where New chat IS the active row.
 *
 * The invariant to preserve whatever the values: exactly ONE thing in the
 * column may own the accent. Two owners is what broke every earlier version.
 */

/* Clearance for the fixed rail — conditional on the rail actually being there.
 * `data-ui-layout="rail"` is stamped on <html> by base_ds.html from instance
 * config, but _app_rail.html renders <nav class="rail"> only `{% if
 * session.user %}`. Applied unconditionally, the padding survived on pre-auth
 * pages that have no rail, so /login/password centred its card inside a box
 * phantom-shifted 240px right of the viewport (#1170). `:has()` ties the
 * reservation to the element it is reserving for; it is already the house
 * mechanism for rail-conditional layout (see chat.css / chats.css).
 *
 * `--rail-clearance` publishes that same number to everything else that has to
 * start where the page's content does. `position: fixed` chrome can't inherit
 * the padding — it is positioned against the viewport — so the bottom-docked
 * filter toolbar (`.fbar-dock`, `.fbar-dock__veil`) and the chats bulk bar
 * (`.ch-bulk`) each hardcoded `left: 240px` instead, and all three broke the
 * moment the rail could be collapsed: the toolbar sat 184px right of centre and
 * its frosted band started 184px in, clipped off the left of the content it was
 * supposed to be blurring. One variable, declared beside every `padding-left`
 * that encodes the same edge, is what keeps them from drifting again. Consumers
 * default it to `0px`, so a topnav-layout page needs no rail-specific rule at
 * all. */
html[data-ui-layout="rail"] body:has(.rail) {
    padding-left: 240px;
    --rail-clearance: 240px;
}

html[data-ui-layout="rail"] .rail {
    position: fixed;
    top: 0;
    left: 0;
    bottom: 0;
    width: 240px;
    background: var(--ds-surface);
    border-right: 1px solid var(--ds-border);
    display: flex;
    flex-direction: column;
    /* Stretch children to the rail's inner width. This was originally
     * pinned to neutralize a global `nav { align-items: center }` rule in
     * style-custom.css, which cascaded onto this <nav> and collapsed every
     * item to its content width. That rule is gone (#1207 — it was also
     * silently breaking the semantic layer's category sidebar), but stretch
     * stays declared explicitly rather than relying on the flex default,
     * so the rail's layout never depends on the absence of a global rule. */
    align-items: stretch;
    gap: 2px;
    padding: 16px 12px;
    z-index: 40;
    /* One row height for every rail row — nav items, conversation rows, the
       and the profile row all opt into it (min-height, so a two-line
       profile row can still grow). Scoped to the rail rather than
       :root: it is a chrome metric, not a design token. */
    --rail-row-h: 34px;
    /* The "you are here" fill, shared by every active state in the rail — nav
       destinations (.rail-i.on), the active conversation (li.is-active) and the
       active Admin flyout link. Defined once so the three cannot drift apart;
       scoped to the rail rather than :root for the same reason --rail-row-h is
       (a chrome metric, not a design token).

       ACCENT — this is the one thing in the rail that owns the tint. Being on a
       page is persistent wayfinding: you should be able to locate yourself in
       the column at a glance, without reading a single label, and colour is what
       does that. A neutral wash was tried here (see the COLOUR RULE above) and
       worked, but only while the accent was busy elsewhere.

       It also gives New chat a tinted look on /chat for free, without a standing
       tint anywhere: /chat is the landing surface and New chat is the active row
       there, so the row the accent lands on most often is the one you most
       wanted coloured. */
    --rail-active-bg: var(--ds-primary-light);
    /* The hover fill, shared by every hoverable row in the rail — nav
       destinations, conversation rows and Admin flyout links. Defined once
       beside --rail-active-bg for the same reason: three rules, one value, no
       drift.

       NEUTRAL, because active owns the accent and a rail cannot spend it twice.
       This carried `--ds-primary-light` for one pass; with active tinted the
       same value, hovering any row made it read as selected. Hover only has to
       be PERCEPTIBLE — the pointer is already on the row, so the user knows what
       they are pointing at and colour tells them nothing new.

       `--ds-surface-dim` rather than a wash mixed from the ink: the ink-mix
       trick exists to stay monotonic against a NEUTRAL sibling, and hover's
       sibling is now an accent tint, so there is no ramp-ordering problem to
       solve. (For the record: the neutral surface ramp is not monotonic across
       themes — light runs surface → dim → sunken progressively darker, but in
       dark `sunken` lands darker than `dim` — so if both states ever go neutral
       again, the stronger one must be an ink-mix, not `--ds-surface-sunken`.) */
    --rail-hover-bg: var(--ds-surface-dim);
    /* How long the hidden content of a PEEKING collapsed rail waits before it
       starts fading in — one value shared by every rule that reveals a label,
       the conversation region and the collapse toggle (see the collapsed-rail
       section at the foot of this file), so they cannot drift apart. Scoped to
       the rail rather than :root for the same reason --rail-row-h is: a chrome
       metric, not a design token.

       It TRAILS the width's own 120ms opening delay by a hair — the column is
       already moving before anything inside it starts to appear, which is the
       whole order the sequence depends on — but it does not wait for the width
       to FINISH. It doesn't have to: every label clips itself and is squeezed to
       ~zero in the narrow column (see the hide rule at the foot of this file),
       so the growing width is what unveils the text, progressively, and this
       fade only softens the moment it starts. An earlier pass had no clipping to
       lean on and had to hold the text back to 220ms — past the point the width
       was mostly done — which bought correctness with a reveal that felt like it
       was waiting for permission. At 140ms the column has just begun to open and
       the text comes up inside it. */
    --rail-peek-text-delay: .14s;
    /* …and how fast it goes on the way OUT. Shorter than the fade in and with no
       delay at all, so a collapse reads as the open sequence reversed — content
       releases, then the column closes behind it — without the close feeling
       any slower than the instant it has always been. */
    --rail-peek-text-out: .1s;
    /* `visible` (not `auto`) so popovers can escape the rail's edges instead
       of being clipped — the user-menu panel below is absolutely positioned
       and depends on this. (It also served the Studio flyout, since retired.)
       The recents region scrolls internally (.rail-history-body), so the rail
       itself does not need its own scrollbar in normal use. */
    overflow: visible;
    font-family: var(--ds-font);
}

/* 14px below the wordmark — between the 8px this carried while the rail had no
   section labels and the 18px of its first version. The 8px was tuned for a
   logo sitting directly above New chat, where a deep trough read as two
   unrelated zones when they are one surface (the logo's href is `home_route`,
   which defaults to /dashboard → /chat, exactly where `#new-chat` goes). With
   Pinned/Chats labels below, the top of the rail carries more structure and the
   wordmark needs to stop reading as part of the first row. Still asymmetric on
   purpose: the mark keeps its 6px above.
   The ≤1024px bar overrides it back to 8px — there the logo shares a row with
   the collapse toggle, and extra bottom padding sits it visually high.

   This padding/gap now lives on `.rail-logo-row`, the wrapper that also
   carries `#rail-toggle` (the one collapse/expand control, ChatGPT's
   pattern) — moved here so the row's own geometry doesn't depend on which
   of its two children happens to be visible. */
html[data-ui-layout="rail"] .rail-logo-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    padding: 6px 8px 14px;
    flex-shrink: 0;
}

html[data-ui-layout="rail"] .rail-logo {
    display: flex;
    align-items: center;
    gap: 9px;
    min-width: 0;
    flex: 1 1 auto;
    overflow: hidden;
    color: var(--ds-text-primary);
    text-decoration: none;
}

html[data-ui-layout="rail"] .rail-logo svg {
    height: 28px;
    width: auto;
    flex-shrink: 0;
}

/* The ONE rail-width control (collapse when expanded, expand when
   collapsed). Persistently visible here, in the expanded row — the
   collapsed-strip overrides below (icon mode) are where it goes quiet
   until hover/focus/touch reveals it in the logo's place instead. */
html[data-ui-layout="rail"] .rail-toggle {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 26px;
    height: 26px;
    min-width: 24px;
    min-height: 24px;
    flex-shrink: 0;
    border: none;
    border-radius: var(--ds-radius-pill, 999px);
    background: none;
    color: var(--ds-text-secondary);
    cursor: pointer;
}

html[data-ui-layout="rail"] .rail-toggle:hover {
    background: var(--rail-hover-bg);
    color: var(--ds-text-primary);
}

html[data-ui-layout="rail"] .rail-toggle:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* 17px, a touch larger than the 15px a chevron needed: this glyph's meaning
   lives in its interior (a frame plus a divider) rather than in one stroke's
   direction, and it turns to mush below ~16px. */
html[data-ui-layout="rail"] .rail-toggle svg {
    width: 17px;
    height: 17px;
}

/* No per-state transform. The chevron this replaced had to flip 180° whenever
   the rail read as 240px wide, or it pointed the wrong way about what a click
   would do; a panel glyph is symmetrical in meaning — it names the thing being
   toggled, not a direction — so both states share one drawing. The state is
   carried where it belongs: `aria-expanded` plus the accessible name
   ("Collapse navigation" / "Expand navigation" / "Pin navigation open"),
   which rail_toggle.js keeps in sync. */

/* Collapse toggle for the ≤1024px row layout (see the media query below) —
   inert and hidden above it, since the column layout has room for
   everything and nothing to collapse. */
html[data-ui-layout="rail"] .rail-collapse-toggle {
    display: none;
}

/* Both nav zones + recents + Admin are wrapped in this div so the ≤1024px
   layout has a single thing to collapse. Above that breakpoint it must be a
   completely transparent wrapper — matching .rail's own flex/gap exactly — or
   nesting it introduces new spacing between items that used to be direct flex
   children of .rail (a change to the byte-identical default column layout,
   which CLAUDE.md's design-system contract forbids).
   flex:1 1 auto + min-height:0 carries forward what .rail-history relied on
   .rail itself for: growing to fill the remaining fixed-height column so
   its own flex:1/overflow:hidden can turn IT into the scrolling region,
   instead of the whole rail growing past the viewport. No `overflow`
   here — the Studio hover flyout (rail-nav, position:absolute) still needs
   to escape rightward through this wrapper exactly as it did through .rail
   directly. */
html[data-ui-layout="rail"] .rail-collapsible {
    display: flex;
    flex-direction: column;
    gap: 2px;
    flex: 1 1 auto;
    min-height: 0;
}

/* Agnes brand orb — the raster brand mark sitting before the wordmark. */
html[data-ui-layout="rail"] .rail-orb {
    width: 30px;
    height: 30px;
    flex-shrink: 0;
}

/* Agnes wordmark beside the orb. */
html[data-ui-layout="rail"] .rail-logo-txt {
    /* 16px/650, down from 19px/700. The wordmark is an identity mark, not the
       page's headline: at 19px it outweighed every h1 in the app it sits beside,
       so the eye landed on the product name before the thing you opened. It is
       still the largest type in the column — it just stops competing with the
       content. */
    font-size: 16px;
    font-weight: 650;
    letter-spacing: -0.02em;
    line-height: 1.05;
    color: var(--ds-text-primary);
    min-width: 0;
}

html[data-ui-layout="rail"] .rail-nav {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex-shrink: 0;
}

/* The bottom zone's browse destinations (Library · Agents). `margin-top: auto`
   pins the whole zone to the foot of the scroll column — load-bearing only when
   there is no conversation list to grow above it (a caller without a chat
   grant); with the list present its flex-grow has already eaten the slack. */
html[data-ui-layout="rail"] .rail-nav-bottom {
    margin-top: auto;
    padding-top: 6px;
}

/* TYPE SCALE NOTE (the whole column, not just this rule): navigation type sits
   one step BELOW page type — 13px rows, 16px wordmark, 16px icons — so the
   content leads and the chrome recedes. It was 14px rows under a 19px wordmark,
   which read as a second page competing with the one you opened. The active row
   earns WEIGHT as well as tint (600 vs 500), so "where am I" survives a
   screenshot in greyscale and does not lean entirely on colour. */
html[data-ui-layout="rail"] .rail-i {
    display: flex;
    align-items: center;
    /* 9px, not 11: at 13px type an 11px gap reads as two columns (glyphs, then
       labels) rather than one row per destination. */
    gap: 9px;
    /* min-height, not height: the row must still grow for a wrapped label
       rather than clipping it. Vertical padding stays below the row height so
       short rows are height-driven and every rail row lines up. */
    min-height: var(--rail-row-h);
    padding: 6px 11px;
    border-radius: 9px;
    cursor: pointer;
    color: var(--ds-text-secondary);
    /* .rail-i is applied to both <a> and <button> (the Admin summary).
       Buttons don't inherit the page font by default, so pin it here or that
       row renders in the UA font, out of step with the plain nav links. */
    font-family: inherit;
    font-weight: 500;
    font-size: 13px;
    /* Explicit line-height so the label's line-box never exceeds the icon and
       every row resolves to the same height from `--rail-row-h` rather than from
       whatever its text happens to measure. */
    line-height: 1.2;
    text-decoration: none;
    border: none;
    background: none;
    width: 100%;
    text-align: left;
}

html[data-ui-layout="rail"] .rail-i svg {
    /* 16px, down from 18: the glyph identifies the row, the label names it, and
       at 13px type an 18px glyph was the loudest thing in the column. */
    width: 16px;
    height: 16px;
    flex-shrink: 0;
}

/* Hover — NEUTRAL, see `--rail-hover-bg`. The accent belongs to the active row;
   hover only has to be perceptible. */
html[data-ui-layout="rail"] .rail-i:hover {
    background: var(--rail-hover-bg);
    color: var(--ds-text-primary);
}

/* Active destination — the ACCENT tint, see `--rail-active-bg`. Ink goes with
   the fill so the row never reads as blue-behind-grey-text. Hovering the active
   row replaces the tint with the neutral wash, which is a little odd on paper
   but right in practice: the two states are mutually exclusive, and hover
   confirms the pointer is over a row you can click even when you are already on
   it. */
html[data-ui-layout="rail"] .rail-i.on {
    background: var(--rail-active-bg);
    color: var(--ds-primary-dark);
    font-weight: 600;
}

html[data-ui-layout="rail"] .rail-i:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* The label takes the free space so any trailing element in a nav item hugs
   the right edge, and truncates rather than wrapping — same treatment the
   conversation titles get, so a long label can never re-flow the ladder. */
html[data-ui-layout="rail"] .rail-i-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── Global search ─────────────────────────────────────────────────────
   Combobox over GET /api/knowledge/search, above Zone 1. Ported from the
   retired topnav chrome's `.app-header-search` when the rail became the only
   chrome (Wave 0, 2026-08) — same two ids, same endpoint, rail styling.

   Sized on the row ladder (`--rail-row-h`) so it reads as part of the column
   rather than a widget dropped on top of it, and FIXED height, so neither zone
   moves when the conversation list below grows.

   Collapse: `.rail-search` joins `.rail-history` in the `visibility: hidden`
   list further down rather than getting a compact icon-button form of its own.
   A 40px input is not a usable search box, and the peek (hover/focus-within)
   already hands back the full one — the same bargain the conversation list
   takes. The magnifier stays put in both states so the row is still legible as
   "search" while collapsed. */
html[data-ui-layout="rail"] .rail-search {
    position: relative;
    display: flex;
    align-items: center;
    margin: 0 0 8px;
}
html[data-ui-layout="rail"] .rail-search-icon {
    position: absolute;
    left: 9px;
    width: 15px;
    height: 15px;
    pointer-events: none;
    color: var(--ds-text-muted);
}
html[data-ui-layout="rail"] .rail-search-input {
    width: 100%;
    height: var(--rail-row-h);
    /* Left pad clears the absolutely-positioned magnifier. */
    padding: 0 10px 0 30px;
    font-family: inherit;
    font-size: 13px;
    color: var(--ds-text-primary);
    background: var(--ds-surface-dim);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-sm, 8px);
    transition: background var(--ds-motion-fast) var(--ds-ease-standard),
                border-color var(--ds-motion-fast) var(--ds-ease-standard);
}
html[data-ui-layout="rail"] .rail-search-input::placeholder { color: var(--ds-text-muted); }
html[data-ui-layout="rail"] .rail-search-input:focus {
    outline: none;
    background: var(--ds-surface);
    border-color: var(--ds-primary);
}
html[data-ui-layout="rail"] .rail-search-input:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* The results listbox. Anchored to the rail's inner edge and opening RIGHTWARD
   over the page — the rail is only ~240px wide, so a panel constrained to the
   column would be narrower than the titles it lists. Capped to the viewport so
   it cannot run off-screen on a narrow window. */
html[data-ui-layout="rail"] .rail-search-panel {
    position: absolute;
    top: calc(100% + 6px);
    left: 0;
    width: min(340px, calc(100vw - 32px));
    max-height: 60vh;
    overflow-y: auto;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-md, 10px);
    box-shadow: var(--ds-shadow-md);
    padding: 6px;
    /* Above the rail's own stacking, which the collapsed-state rules raise. */
    z-index: 60;
}
html[data-ui-layout="rail"] .rail-search-panel[hidden] { display: none; }
html[data-ui-layout="rail"] .rail-search-group {
    padding: 6px 10px 4px;
    font-size: 10px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    color: var(--ds-text-muted);
}
html[data-ui-layout="rail"] .rail-search-result {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    padding: 8px 10px;
    border-radius: 6px;
    font-size: 13px;
    color: var(--ds-text-primary);
    text-decoration: none;
}
html[data-ui-layout="rail"] .rail-search-result:hover,
html[data-ui-layout="rail"] .rail-search-result:focus-visible {
    background: var(--ds-border-light);
    outline: none;
}
/* Left column of a hit: the title, plus a definition line on the two types
   that have one (metric, glossary — see definitionFor() in global_search.js).
   `min-width: 0` is what lets both lines ellipsis inside a flex row. */
html[data-ui-layout="rail"] .rail-search-result-main {
    display: flex;
    flex-direction: column;
    gap: 1px;
    min-width: 0;
}
html[data-ui-layout="rail"] .rail-search-result-title {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The definition itself — the answer to a lookup, so it is present but clearly
   subordinate to the term it defines. One line: it is a reminder of the
   meaning, not the full entry (that is what the row still links to). */
html[data-ui-layout="rail"] .rail-search-result-def {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: 12px;
    line-height: 1.35;
    color: var(--ds-text-secondary);
}
html[data-ui-layout="rail"] .rail-search-result-badge {
    flex: none;
    font-size: 10px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.3px;
    color: var(--ds-text-muted);
}
html[data-ui-layout="rail"] .rail-search-empty {
    padding: 12px 10px;
    font-size: 13px;
    color: var(--ds-text-muted);
}

/* ── New chat ──────────────────────────────────────────────────────────
   NO rules of its own. It is an ordinary `.rail-i` row — the block above styles
   it exactly like Library / Agents / Admin — and it highlights only via `.on`.

   It spent a while as a `.rail-compose` control: a pale-blue borderless block,
   38px instead of the 34px ladder, because as a plain row it genuinely is hard
   to tell from the conversations directly beneath it (same `--rail-row-h`, same
   `--ds-text-secondary`, 14px/500 against 13.5px/500, 9px radius against 8px).
   Three variants of that treatment were tried and all are retired: grey fill +
   `--ds-border` outline + tinted icon chip (three devices for one job, and it
   read as a disabled control), solid `--ds-primary` (far too loud for a 240px
   column), and the pale `--ds-primary-light` fill.

   Why none of them came back: a standing tint on ONE row consumed the rail's
   entire accent budget. Hover could not use the accent, because a hovered
   Library row became pixel-identical to New chat at rest; and `.on` needed an
   inset ring purely to tell two pale-blue things apart. Interaction feedback
   that fires on every row, every time the pointer moves, is worth more than
   decoration on a single row — so the accent went to hover
   (`--rail-hover-bg`) and this row went neutral.

   What distinguishes it without colour: the `+` glyph (no conversation row has
   an icon), its position at the top of the column, and the 8px gap below. If a
   future pass wants it louder, spend something OTHER than the accent — weight
   and ink are unused here (every resting row is `--ds-text-secondary`/500, so
   `--ds-text-primary`/600 is available and costs no colour). */

/* Separates New chat from the conversations below it. The rail's own 2px gap
   read as "next row in the same list"; 8px is all the separation the two need —
   and it is now the only thing between the verb and the first section label, so
   the label needs no top margin of its own (.rail-chatsec:first-child below). */
html[data-ui-layout="rail"] .rail-nav-top {
    margin-bottom: 8px;
}

/* ── Conversations ─────────────────────────────────────────────────────
   Two collapsible sections — Pinned, then Chats — inside ONE scroll box. The
   region occupies the slack between the two fixed zones, so the bottom zone
   stays pinned to the foot however long the lists get. The <ul id="chat-list">
   reuses the chat page's ids/classes so chat.js renders straight into it on
   /chat; rail_history.js fills it (and <ul id="pinned-chat-list">) on every
   other page. Nothing truncates either list — see .rail-history-body. */
html[data-ui-layout="rail"] .rail-history {
    min-height: 0;
    display: flex;
    flex-direction: column;
    /* Take the remaining vertical space so the list — not the whole rail — is
       what scrolls. min-height:0 lets it shrink below content height, and
       overflow:hidden forces the flex child (.rail-history-body) to respect
       that bound and scroll internally instead of spilling into the zone
       below. */
    flex: 1 1 auto;
    overflow: hidden;
}

/* Scroll region — ALWAYS scrolling, never truncated.
   `flex: 1 1 0` makes it grow strictly into the space .rail-history leaves (a
   `flex-basis: auto` would size to content and refuse to shrink, spilling older
   rows past the clip instead of scrolling them), and `min-height: 0` lets it
   shrink below content height so a short viewport shrinks the list rather than
   pushing the bottom zone off screen — the rail itself is `overflow: visible`
   and cannot scroll, so anything forced past the fold would be unreachable.
   That is also why there is deliberately no `min-height` floor here.

   This used to have TWO states: `flex: 0 1 auto; overflow: hidden` while
   truncated to five rows, and these values once expanded. The truncation and
   its "View all chats" / "Show less" toggle are gone — this region is already
   exactly the rail's free space, so the cap showed five rows on a screen with
   room for nine and the toggle existed only to undo a limit we imposed
   ourselves. See rail_history.js for what went with it. */
html[data-ui-layout="rail"] .rail-history-body {
    display: flex;
    flex-direction: column;
    min-height: 0;
    flex: 1 1 0;
    overflow-y: auto;
    margin-top: 2px;
    /* Room for the scrollbar so rows don't sit flush against the rail edge. */
    padding-right: 2px;

    /* ── Scroll fade ───────────────────────────────────────────────────
       This region is exactly the rail's free space, so on most viewports there
       ARE rows past one edge or the other. With a hard clip the first/last
       visible title is sliced mid-glyph and reads as a rendering fault, not as
       "keep scrolling" — the one thing the cut needs to say.

       A gradient mask says it instead: rows dissolve into the rail at whichever
       edge still has content behind it. Masking (not a gradient overlay) is
       deliberate — the fade has no colour of its own, so it needs no value that
       has to be kept in sync with `--ds-surface` across four palette themes and
       light/dark, and it cannot go wrong when a hovered/active row paints its
       own tint underneath.

       Both edges are off by default and switched on per-edge by
       rail_history.js (.is-fade-top / .is-fade-bottom) from the scroll
       position, so a list that fits entirely — the common case early on, with
       two or three conversations — is not dimmed at either end for no reason.
       At 0px the two stops collapse onto the same position and the mask is a
       no-op, so the resting state costs nothing but a stacking context. */
    --rail-fade: 20px;
    --rail-fade-top: 0px;
    --rail-fade-bottom: 0px;
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, black var(--rail-fade-top), black calc(100% - var(--rail-fade-bottom)), transparent 100%);
            mask-image: linear-gradient(to bottom, transparent 0, black var(--rail-fade-top), black calc(100% - var(--rail-fade-bottom)), transparent 100%);
}

html[data-ui-layout="rail"] .rail-history-body.is-fade-top {
    --rail-fade-top: var(--rail-fade);
}

html[data-ui-layout="rail"] .rail-history-body.is-fade-bottom {
    --rail-fade-bottom: var(--rail-fade);
}

/* Forced-colors users have asked the OS for maximum contrast; a decorative
   fade to transparent is the opposite of that. Drop it — the scrollbar carries
   the same "there is more" signal. */
@media (forced-colors: active) {
    html[data-ui-layout="rail"] .rail-history-body {
        -webkit-mask-image: none;
                mask-image: none;
    }
}

/* ── The two sections (Pinned · Chats) ─────────────────────────────────
   Both live inside the single scroll box above — deliberately NOT one fixed
   pinned block over a scrolling feed. A second scroll container in a 240px
   column means two scrollbars, two fade masks and a pinned block whose height
   has to be capped by guesswork; one scroll box with a collapsible header per
   section gets the same control into the user's hands and costs no geometry.

   Whitespace does the separating, as everywhere else in the rail: no dividers
   (see test_admin_is_the_only_divided_group — Admin owns the nav's only rule
   line) and no fills. 18px between sections — a step up from the 14px the group
   headers this replaces used, because these are now two independent things
   rather than two groups of one list, and the gap is the only thing saying so. */
html[data-ui-layout="rail"] .rail-chatsec {
    display: block;
}

html[data-ui-layout="rail"] .rail-chatsec + .rail-chatsec {
    margin-top: 18px;
}

/* The 8px under New chat is already the gap above the first label. */
html[data-ui-layout="rail"] .rail-chatsec:first-child {
    margin-top: 0;
}

/* The <h2> exists for document structure only (the button inside it is the
   control) — strip the heading's own type and spacing entirely. */
html[data-ui-layout="rail"] .rail-chatsec-hd-wrap {
    margin: 0;
    font: inherit;
}

/* Section label — a LABEL that happens to be operable, not a row. Hence no
   background in any state, no border, and the same 9px left padding as the
   conversation rows below it so the label and the titles share one left edge.
   Hover/focus spend INK (muted → secondary), which is the one channel still
   free: the accent belongs to the active conversation and the neutral wash to
   hovering an actual row, so a header that filled on hover would read as a
   selectable row (see the COLOUR RULE at the top of this file). */
html[data-ui-layout="rail"] .rail-chatsec-hd {
    display: flex;
    align-items: center;
    /* Caret sits against the label (see below), so this is the only gap. */
    gap: 3px;
    width: 100%;
    padding: 3px 9px 6px;
    border: none;
    background: none;
    border-radius: 6px;
    color: var(--ds-text-muted);
    cursor: pointer;
    text-align: left;
    font-family: inherit;
}

html[data-ui-layout="rail"] .rail-chatsec-hd:hover {
    color: var(--ds-text-secondary);
}

html[data-ui-layout="rail"] .rail-chatsec-hd:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* Same type as the group headers this promotes — the rail already reads this
   size/weight/tracking as "label for what follows". */
html[data-ui-layout="rail"] .rail-chatsec-txt {
    font-size: 10.5px;
    font-weight: 700;
    letter-spacing: 0.07em;
    text-transform: uppercase;
}

/* Disclosure caret — the rail's existing idiom, rotated the same way as
   Admin's (down = open, right = collapsed), one size quieter than Admin's 15px
   because this is a label and that is a nav row.
   Deliberately NOT pushed to the rail's right edge with `margin-left: auto`:
   out there it read as a second column of controls next to the conversation
   rows' own "⋮", and what it actually annotates is the word beside it. The
   button still spans the full width, so the hit area is the whole line. */
html[data-ui-layout="rail"] .rail-chatsec-caret {
    width: 12px;
    height: 12px;
    flex-shrink: 0;
    transform: rotate(90deg);
    transition: transform 0.18s var(--ds-ease-standard, ease);
}

html[data-ui-layout="rail"] .rail-chatsec.is-collapsed .rail-chatsec-caret {
    transform: rotate(0deg);
}

@media (prefers-reduced-motion: reduce) {
    html[data-ui-layout="rail"] .rail-chatsec-caret {
        transition: none;
    }
}

/* The list itself doesn't scroll — its parent (.rail-history-body) does — so
   neutralize the base chat.css `.cloud-chat-list { overflow-y:auto; flex:1 }`
   that would otherwise create a nested scroll container on /chat. */
html[data-ui-layout="rail"] .rail-history .cloud-chat-list {
    list-style: none;
    margin: 0;
    padding: 0;
    overflow: visible;
    flex: 0 0 auto;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id] {
    display: flex;
    align-items: center;
    gap: 6px;
    /* Same ladder as the nav rows above and below — see `--rail-row-h`. */
    min-height: var(--rail-row-h);
    padding: 4px 9px;
    border-radius: 8px;
    cursor: pointer;
    font-size: 13.5px;
    line-height: 1.3;
    color: var(--ds-text-secondary);
    border: 1px solid transparent;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id] + li[data-id] {
    margin-top: 2px;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id]:hover {
    background: var(--rail-hover-bg);
    color: var(--ds-text-primary);
}

/* The conversation you are reading — the accent `--rail-active-bg`, like every
   other active state in the rail.

   The `:hover` selectors are load-bearing, not defensive padding. The plain
   hover rule above is `… .cloud-chat-list li[data-id]:hover` — five
   specificity units against this rule's four — so WITHOUT them, moving the
   pointer over the conversation you are currently reading replaces its accent
   tint with the neutral hover wash, i.e. the open chat visibly loses its
   highlight exactly when you reach for it. (The nav rows and Admin links have
   no such problem: their hover and active selectors tie on specificity, so
   source order already keeps active on top.) Guarded by
   tests/test_ui_layout_theme.py::test_active_row_keeps_its_tint_on_hover. */
html[data-ui-layout="rail"] .rail-history .cloud-chat-list li.is-active,
html[data-ui-layout="rail"] .rail-history .cloud-chat-list li.active,
html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id].is-active:hover,
html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id].active:hover {
    background: var(--rail-active-bg);
    color: var(--ds-primary-dark);
    font-weight: 600;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id]:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* Conversation label — grows to fill, truncates rather than wrapping. */
html[data-ui-layout="rail"] .rail-history .cloud-chat-list-label {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Origin pill (Slack) — small, non-interactive. */
html[data-ui-layout="rail"] .rail-history .cloud-chat-surface-badge {
    flex-shrink: 0;
    font-size: 9.5px;
    font-weight: 700;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--ds-text-muted);
    background: var(--ds-surface-dim);
    border-radius: 5px;
    padding: 1px 5px;
}

/* Paused badge reports an infra state the user can't act on — noise in the
   narrow rail; drop it (matches the retired history column's behavior). */
html[data-ui-layout="rail"] .rail-history .cloud-chat-paused-badge {
    display: none;
}

/* Pinned-state indicator — a static glyph, not a control (the pin ACTION lives
   in the row menu). Always visible and solid rather than outlined, so a pinned
   row stays identifiable once the "Pinned" header has scrolled away, by shape
   and not by color alone. */
html[data-ui-layout="rail"] .rail-history .cloud-chat-pin-flag {
    flex-shrink: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    line-height: 1;
    color: var(--ds-primary);
    pointer-events: none;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-pin-flag svg {
    width: 11px;
    height: 11px;
    fill: currentColor;
}

/* Row overflow trigger ("⋮") — one control for Pin/Rename/Delete, replacing the
   inline icon buttons that each cost width in a ~250px column. Hidden until the
   row is hovered or the control focused, and held open while its menu is. */
html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn {
    flex-shrink: 0;
    opacity: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: none;
    background: none;
    color: var(--ds-text-muted);
    cursor: pointer;
    line-height: 1;
    padding: 2px 2px;
    border-radius: 5px;
}

html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn svg {
    width: 14px;
    height: 14px;
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list li[data-id]:hover .chat-rowmenu-btn,
html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn:focus-visible,
html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn[aria-expanded="true"] {
    opacity: 1;
}

html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn:hover,
html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn[aria-expanded="true"] {
    background: var(--ds-surface-dim);
    color: var(--ds-text-primary);
}

html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* Coarse pointers get no hover event — pin the reveal-on-hover control on.
   (The menu PANEL itself is styled once, unscoped, in chat.css: it is
   body-appended, so it is not a descendant of .rail and cannot be reached by
   this file's selectors.) */
@media (hover: none) {
    html[data-ui-layout="rail"] .rail-history .chat-rowmenu-btn {
        opacity: 1;
    }
}

/* No date-group header rules here any more, and none should come back: neither
   renderer emits one under rail. The region is now the pinned shelf plus a
   RAIL_RECENT_LIMIT-row "Recent" feed under the Chats destination row (see
   rail_history.js), and at five rows a date boundary labels a boundary the list
   is too short to have — the last one standing, "Older", sat inside a section
   already labelled "Recent". The five buckets topnav still uses are styled by
   its own sheet; `_groupSessionsByDate` in chat.js is topnav-only now. */

/* Two rules used to live here and are deliberately gone with the truncation:
   an `li[hidden] { display: none }` restatement (the row/header rules above set
   an explicit `display`, which beat the UA `[hidden]`), and a margin patch for
   the collapsed case where a `hidden` date header sat between the pinned block
   and the first recent row. Nothing marks a row `hidden` any more — the only
   `[hidden]` left in the region is the empty-state <p> below — so both were
   dead weight describing a state that no longer exists. */

/* Empty state — hidden as soon as the list has any rows. */
html[data-ui-layout="rail"] .rail-history-empty {
    margin: 4px 0 0;
    padding: 6px 9px;
    font-size: 12.5px;
    color: var(--ds-text-muted);
}

html[data-ui-layout="rail"] .rail-history .cloud-chat-list:not(:empty) + .rail-history-empty {
    display: none;
}

/* No `.rail-history-all` rules here any more, and none should come back. That
   was the "View all chats" link at the foot of the scroll box: quieter than a
   `.rail-i` on purpose (the recents are the thing to read), with no active state
   and a trailing chevron for its "this goes somewhere" mark.

   Being quiet was not the problem — being INSIDE this region was. The region is
   text-only, so the collapsed rail hides it, and the collapsed rail is the
   default on /admin; the link was also revealed only once the caller had a
   conversation. A way to a page cannot be conditional on either. /chats is a
   destination row at the top of the zone now (`.rail-i`, its own glyph — see
   _app_rail.html), which is reachable at both rail widths and on a first run,
   and the lists below it are what the collapse hides. */

/* ── Admin ────────────────────────────────────────────────────────────
   The last section of the bottom zone. The border-top is the one divider
   inside the nav zones: it marks the shift from using the instance (Library,
   Agents) to operating it. */
html[data-ui-layout="rail"] .rail-admin {
    margin-top: 8px;
    border-top: 1px solid var(--ds-border);
    padding-top: 8px;
    flex-shrink: 0;
}


/* Footer — the profile row alone (onboarding moved up to a card above the
   bottom nav, "How {brand} works" up to an icon beside the wordmark). Outside
   .rail-collapsible so the profile stays reachable in the ≤1024px bar with the
   nav collapsed. margin-top:auto is belt-and-braces: the scroll column above
   already grows into the slack. */
html[data-ui-layout="rail"] .rail-foot {
    margin-top: auto;
    padding-top: 6px;
    display: flex;
    flex-direction: column;
    gap: 6px;
    flex-shrink: 0;
}

/* Divider above the profile row — the last rule line in the column, marking
   the account off from the navigation. padding-top gives it breathing room
   from the row below. */
html[data-ui-layout="rail"] .rail-user-menu {
    position: relative;
    border-top: 1px solid var(--ds-border);
    padding-top: 10px;
}

html[data-ui-layout="rail"] .rail-user {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    min-height: var(--rail-row-h);
    padding: 7px 8px;
    border-radius: 9px;
    border: none;
    background: none;
    cursor: pointer;
    text-align: left;
}

html[data-ui-layout="rail"] .rail-user:hover {
    background: var(--ds-surface-dim);
}

/* The one rail icon whose size wasn't already pinned. Every other leading icon
   in the column declares `flex-shrink: 0` (`.rail-i svg`, the onboarding ring,
   the brand orb); `.app-avatar` is a shared component from style-custom.css that
   the topnav header uses too, so the floor belongs here rather than on it.
   It matters because the collapsed rail keeps its labels IN FLOW now (hidden,
   not `display: none` — see the collapse section at the foot of this file): the
   profile row's flex line became avatar + gap + label against a ~38px content
   box, and the only shrinkable item was the avatar, which measured 19px instead
   of 36. Expanded there is room for all three, so this changes nothing there. */
html[data-ui-layout="rail"] .rail-user .app-avatar {
    flex-shrink: 0;
}

html[data-ui-layout="rail"] .rail-user-txt {
    display: flex;
    flex-direction: column;
    line-height: 1.25;
    font-size: 13px;
    min-width: 0;
}

html[data-ui-layout="rail"] .rail-user-txt b {
    font-weight: 600;
    color: var(--ds-text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

html[data-ui-layout="rail"] .rail-user-role {
    font-size: 11px;
    color: var(--ds-text-muted);
}

/* The one <button> in the profile menu ("Start over onboarding"). The shared
   `.app-user-menu-item` skin is written for <a>, so restate the bits a button
   doesn't inherit — otherwise it renders in the UA font, centred, and only as
   wide as its text, next to links that fill the panel. */
html[data-ui-layout="rail"] .rail-user-menu .app-user-menu-btn {
    width: 100%;
    border: none;
    background: none;
    font-family: inherit;
    text-align: left;
    cursor: pointer;
}

/* User pop-up opens UP from the bottom row instead of down from the
   header — same panel classes, repositioned. */
html[data-ui-layout="rail"] .rail-user-menu .app-user-menu-panel {
    position: absolute;
    bottom: calc(100% + 8px);
    left: 0;
    right: auto;
    top: auto;
    width: 224px;
    box-shadow: 0 12px 34px rgba(0, 0, 0, 0.14);
}

/* ── Onboarding row + its popover ─────────────────────────────────────
   SAME anatomy as every other rail destination now (an icon + a label,
   `.rail-i`'s own treatment — hover wash, no standing tint, no card chrome):
   the only thing that sets it apart from Library/Agents is what the icon
   IS — a circular progress ring rather than a fixed glyph. That also means
   its height never differs between icon mode's collapsed/expanded states:
   the ring is a fixed size in both (collapsed, it's the whole row's
   content; expanded, it's the row's leading icon, same as any other row's
   svg), and only the LABEL appears/disappears — through the exact
   mechanism every other row's label does (`.rail-i-label`'s sibling in the
   shared show/hide list further down this file).

   `.rail-nav-bottom`'s auto margin (default IA) or the icon-mode flex
   spacer (admin pages) is what pins this row above the profile foot — see
   each respectively; this rule only carries its own vertical rhythm. */
html[data-ui-layout="rail"] .rail-getstarted {
    position: relative;
    flex-shrink: 0;
    margin-top: 10px;
}

/* Retired at 5/5. The row exists to move someone through five steps, so
   with none left it is a full ring reporting a finished job, permanently,
   in the scarcest column in the app. Finishing the steps and clicking
   "Skip onboarding" (which lands all five at once) are deliberately the
   same state: the skip toast tells the caller the card is gone and where
   to find it again, and it has to be true. The profile menu's "Start over
   onboarding" is the way back to 0/5 — restartJourney() clears every step,
   which drops this class and brings the row back. */
html[data-ui-layout="rail"] .rail-getstarted.is-complete {
    display: none;
}

/* Nothing beyond `.rail-i` (reused directly on the button in the markup)
   plus the popover's own position:relative anchor above — kept as a class
   hook for the ring/body children's selectors and any future onboarding-
   specific tweak, not because the row needs its own chrome. */
html[data-ui-layout="rail"] .rail-getstarted-btn {
    gap: 11px;
}

/* The row's icon — a circular progress meter, nothing inside it. Sized
   larger than the plain 18px nav icons (an arc needs diameter to be read as
   an arc) but otherwise just this row's `.rail-i` leading icon — always
   visible, collapsed or expanded, at a CONSTANT size, which is what keeps
   the row's own height from changing between icon mode's two states. */
html[data-ui-layout="rail"] .rail-getstarted-ring {
    position: relative;
    width: 28px;
    height: 28px;
    flex-shrink: 0;
    display: grid;
    place-items: center;
}

html[data-ui-layout="rail"] .rail-getstarted-ring-svg {
    grid-area: 1 / 1;
    width: 100%;
    height: 100%;
    /* Starts the arc at 12 o'clock instead of SVG's default 3 o'clock. */
    transform: rotate(-90deg);
}

/* Stroke width is in viewBox units (36 wide, rendered at 28px), so 4 here is
   ~3.1 real px — deliberately heavy relative to the 28px diameter. A hairline
   ring at this size reads as a decorative circle; a thick one reads as a
   meter, which is the whole job of this element. */
html[data-ui-layout="rail"] .rail-getstarted-ring-track {
    fill: none;
    stroke: var(--ds-border);
    stroke-width: 4;
}

/* `stroke-dasharray` is written by chat_onboarding.js's
   `updateGetStartedIndicator` — "<filled length> <circumference>". r=15 on
   the 36×36 viewBox above, so the circumference (2πr ≈ 94.2) is the
   constant that write uses. */
html[data-ui-layout="rail"] .rail-getstarted-ring-fill {
    fill: none;
    stroke: var(--ds-primary);
    stroke-width: 4;
    stroke-linecap: round;
    stroke-dasharray: 0 94.2;
    transition: stroke-dasharray var(--ds-motion-med, 0.24s) var(--ds-ease-standard, ease);
}

@media (prefers-reduced-motion: reduce) {
    html[data-ui-layout="rail"] .rail-getstarted-ring-fill {
        transition: none;
    }
}

/* No `.is-complete` styling for the ring: at 5/5 the arc is a full lap of
   `--ds-primary`, which reads as done on its own. The class is still toggled
   by chat_onboarding.js and still meaningful to other consumers — it just
   doesn't need to swap a glyph any more, because there is no glyph. */

html[data-ui-layout="rail"] .rail-getstarted-body {
    display: flex;
    flex-direction: column;
    gap: 3px;
    flex: 1 1 auto;
    min-width: 0;
}

/* "Set up Agnes" → "Continue setup" after the first step. Carries the weight of
   a heading against the row labels around it, without their size. */
html[data-ui-layout="rail"] .rail-getstarted-title {
    font-size: 13.5px;
    font-weight: 650;
    line-height: 1.2;
    color: var(--ds-text-primary);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* "N of 5 steps complete" — written whole by chat_onboarding.js, empty until
   the journey resolves (so the line simply isn't there rather than lying). */
html[data-ui-layout="rail"] .rail-getstarted-sub {
    font-size: 11.5px;
    line-height: 1.25;
    color: var(--ds-text-muted);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

html[data-ui-layout="rail"] .rail-getstarted-sub:empty {
    display: none;
}

/* Dropdown card — opens UP from the launcher on hover / focus, and stays
   pinned when clicked open (.is-open). */
html[data-ui-layout="rail"] .rail-getstarted:hover .rail-getstarted-panel,
html[data-ui-layout="rail"] .rail-getstarted:focus-within .rail-getstarted-panel,
html[data-ui-layout="rail"] .rail-getstarted.is-open .rail-getstarted-panel {
    display: block;
}

/* Explicit-close override. The toggle button lives INSIDE .rail-getstarted,
   so :focus-within stays true for as long as it (or the "×" the user just
   clicked) holds focus — and the cursor is, by definition, still over the
   launcher at the exact moment "×" or an outside-click handler fires. Both
   reveal rules above would therefore keep the panel visible regardless of
   what JS does to .is-open. `.is-closed` (added by rail_history.js's close
   paths and chat_onboarding.js's dismissJourney) forces it shut until the
   user's cursor actually leaves the launcher or they reopen it explicitly. */
html[data-ui-layout="rail"] .rail-getstarted.is-closed .rail-getstarted-panel {
    display: none !important;
}
html[data-ui-layout="rail"] .rail-getstarted-panel {
    /* Hidden by default via CSS (the `hidden` attribute was dropped so the
       hover / .is-open rules above can reveal it — a `[hidden]` reset was
       winning otherwise). */
    display: none;
    position: absolute;
    bottom: 100%;
    left: 0;
    right: 0;
    background: var(--ds-surface);
    border: 1px solid var(--ds-border);
    border-radius: 14px;
    box-shadow: 0 12px 34px rgba(0, 0, 0, 0.16);
    padding: 6px;
    z-index: 60;
    /* Never taller than the viewport leaves room for — scroll if the steps
       plus header overflow a short window. */
    max-height: min(70vh, 460px);
    overflow-y: auto;
}

/* The Journey card inside the popover.
   The .cloud-chat-journey-* styles live in chat.css, which only loads on
   /chat — so on every OTHER rail page (where the popover is mounted
   standalone) the card would render as raw UA-styled buttons. rail.css loads
   everywhere, so the full journey styling is (re)defined here, rail-scoped;
   on /chat these out-specify the chat.css base and keep the look identical. */
html[data-ui-layout="rail"] .rail-getstarted-panel .cloud-chat-journey {
    margin-top: 0;
    padding: 8px 6px;
    border-top: none;
    background: transparent;
}

html[data-ui-layout="rail"] .cloud-chat-journey-head {
    display: flex;
    align-items: center;
    /* Was a non-wrapping row: h3 + the "Complete ✓" badge + two icon buttons
       with no flex-wrap and no min-width:0 on the heading. In the completed
       state the badge appears and the row exceeds the panel's width (a fixed
       280px below 1024px, rail.css) — the panel's own `overflow-y: auto`
       computes overflow-x to auto too, so the icon buttons were clipped past
       the edge instead of just visible on their own line. */
    flex-wrap: wrap;
    justify-content: space-between;
    gap: 4px 8px;
}

html[data-ui-layout="rail"] .cloud-chat-journey-head h3 {
    margin: 0;
    font-size: 14px;
    font-weight: 650;
    color: var(--ds-text-primary);
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

html[data-ui-layout="rail"] .cloud-chat-journey-badge {
    font-size: 11px;
    color: var(--ds-accent-success-ink);
    background: var(--ds-accent-success-bg);
    border: 1px solid var(--ds-accent-success-line);
    border-radius: var(--ds-radius-pill, 999px);
    padding: 1px 8px;
}

html[data-ui-layout="rail"] .cloud-chat-journey-actions {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 4px;
}

html[data-ui-layout="rail"] .cloud-chat-journey-iconbtn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;
    font-size: 12px;
    font-weight: 600;
    line-height: 1;
    color: var(--ds-text-muted);
    background: transparent;
    border: 1px solid var(--ds-border);
    border-radius: var(--ds-radius-pill, 999px);
    cursor: pointer;
    /* The visual pill stays 20px (a 44px button would dominate this compact
       header), but the actual hit area is expanded to the 44px touch-target
       minimum via an invisible ::before, not by growing the element itself. */
    position: relative;
}

html[data-ui-layout="rail"] .cloud-chat-journey-iconbtn::before {
    content: '';
    position: absolute;
    /* Full 44px vertical touch target, but horizontal expansion capped at 2px
       (half the 4px inter-button gap) so adjacent buttons' hit areas can't
       overlap. The rail head now renders only ↻ — the "×" is inline-only
       (chat_onboarding.js) — so nothing is adjacent to steal an edge here
       today; the cap stays as the rule for whatever lands beside it next. */
    inset: -12px -2px;
}

html[data-ui-layout="rail"] .cloud-chat-journey-iconbtn:hover {
    color: var(--ds-text-primary);
    border-color: var(--ds-text-muted);
}

/* "N of 5 done", in place of the slogan this line used to hold. No bar: the
   launcher card sits directly below this popover with its own, so a second one
   would draw the same fraction twice. */
html[data-ui-layout="rail"] .cloud-chat-journey-sub {
    margin: 2px 0 8px;
    font-size: 12px;
    color: var(--ds-text-muted);
}

html[data-ui-layout="rail"] .cloud-chat-journey-list {
    display: flex;
    flex-direction: column;
    gap: 1px;
}

/* mark · (label + why). No trailing chevron: one column of glyphs down the left
   is enough, and a second down the right edge crowded labels that already wrap at
   this width. Top-aligned, not centered — with the row centered a two-line label
   floated its ✓/→/• to the middle of the text block, so the marks stopped lining
   up with each other and the list lost its left spine. */
html[data-ui-layout="rail"] .cloud-chat-journey-step {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    width: 100%;
    text-align: left;
    padding: 7px 8px;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--ds-text-secondary);
    font-size: 13.5px;
    line-height: 1.35;
    font-family: inherit;
    cursor: pointer;
}

html[data-ui-layout="rail"] .cloud-chat-journey-step:hover {
    background: var(--ds-surface-dim);
}

html[data-ui-layout="rail"] .cloud-chat-journey-step:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

html[data-ui-layout="rail"] .cloud-chat-journey-mark {
    flex-shrink: 0;
    width: 16px;
    text-align: center;
    font-weight: 600;
}

html[data-ui-layout="rail"] .cloud-chat-journey-body {
    flex: 1;
    min-width: 0;
}

html[data-ui-layout="rail"] .cloud-chat-journey-label {
    display: block;
}

/* The step's own explanation, on the suggested step only — it replaced a `title`
   tooltip that needed a hover nobody tries and opened an OS box across the rows
   below it. */
html[data-ui-layout="rail"] .cloud-chat-journey-why {
    display: block;
    margin-top: 2px;
    font-size: 12px;
    line-height: 1.4;
    color: var(--ds-text-muted);
}

/* Done: ✓ + muted label, no line-through — struck text on a label that wraps to
   two lines at this width read as damaged rather than finished, and it repeated
   what the tick already says. */
html[data-ui-layout="rail"] .cloud-chat-journey-step.done {
    color: var(--ds-text-muted);
}

html[data-ui-layout="rail"] .cloud-chat-journey-step.done .cloud-chat-journey-mark {
    color: var(--ds-accent-success-ink);
}

/* The suggested step, and the card's only focal point: tinted block, primary
   ink, its own why-line. A suggestion of where to resume — not a gate, since
   every other row is equally clickable. */
html[data-ui-layout="rail"] .cloud-chat-journey-step.active {
    color: var(--ds-text-primary);
    background: var(--ds-primary-light);
    font-weight: 600;
}

html[data-ui-layout="rail"] .cloud-chat-journey-step.active:hover {
    background: var(--ds-primary-light);
}

html[data-ui-layout="rail"] .cloud-chat-journey-step.active .cloud-chat-journey-mark {
    color: var(--ds-primary);
}

html[data-ui-layout="rail"] .cloud-chat-journey-step.active .cloud-chat-journey-why {
    font-weight: 400;
}

/* A step further down the list — still a real control (pointer cursor + hover
   wash from the base rule above): every step is clickable in any order. Held at
   the row default so all three ranks stay distinct — `.active` darkest, this one
   secondary, `.done` muted. */
html[data-ui-layout="rail"] .cloud-chat-journey-step.todo {
    color: var(--ds-text-secondary);
}

/* A step's secondary destination ("Choose where Agnes reaches you" under "Use
   Agnes outside this tab"). Restated in FULL, not just sized: the base rule lives
   in chat.css, which does not load on the other rail pages, so on every page but
   /chat this rendered as a raw browser link — blue, or purple once visited,
   underlined, in a card built entirely from `--ds-*` ink. Geometry (the indent
   that hangs it off the label rather than the ✓/→ gutter) matches the base rule;
   the size is 12px because the rail sets its step rows to 13.5px rather than the
   token scale, so --text-xs would read as fine print rather than one step down. */
html[data-ui-layout="rail"] .cloud-chat-journey-sub-action {
    display: block;
    margin: -1px 0 2px 32px;
    padding: 2px 0;
    font-size: 12px;
    line-height: 1.45;
    color: var(--ds-text-muted);
    text-decoration: none;
}

html[data-ui-layout="rail"] .cloud-chat-journey-sub-action:hover {
    color: var(--ds-primary);
    text-decoration: underline;
}

html[data-ui-layout="rail"] .cloud-chat-journey-sub-action:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

html[data-ui-layout="rail"] .cloud-chat-journey-sub-action.is-linked {
    color: var(--ds-accent-success-ink);
}

/* Panel footer: the quiet skip. Restated here for the same reason as everything
   above — chat.css doesn't load on the other rail pages, and the popover is
   mounted on all of them. */
html[data-ui-layout="rail"] .cloud-chat-journey-finish {
    display: block;
    width: 100%;
    margin-top: 10px;
    padding: 5px 12px;
    border: none;
    border-radius: 8px;
    background: transparent;
    color: var(--ds-text-muted);
    font: inherit;
    font-size: 12px;
    cursor: pointer;
}

html[data-ui-layout="rail"] .cloud-chat-journey-finish:hover {
    color: var(--ds-text-primary);
    background: var(--rail-hover-bg, var(--ds-surface));
}

html[data-ui-layout="rail"] .cloud-chat-journey-finish:focus-visible {
    outline: var(--ds-focus-outline);
    outline-offset: var(--ds-focus-outline-offset);
}

/* ── Collapsed rail — a persisted USER PREFERENCE ────────────────────
 * `.rail-icon-mode` used to be derived from the request path (only
 * /admin/* pages ever collapsed, since a second 190px admin sidebar there
 * left only ~39% of a 1100px viewport for content). It is now the
 * caller's own choice, stored once under `agnes.rail.collapsed` in
 * localStorage and honored on EVERY rail page, admin included: the inline
 * bootstrap script at the top of `<nav>` in _app_rail.html (same
 * before-first-paint technique _theme_resolve.html uses for the colour
 * scheme) adds or removes this class before the browser paints, so there
 * is no flash regardless of which page loads first. `_admin_page` in the
 * template only supplies the class a caller who has never toggled starts
 * from — collapsed on /admin, expanded everywhere else — never an override
 * once a stored value exists.
 *
 * Hovering (or focusing into, or — on touch, which has neither — tapping
 * the one `#rail-toggle` in the logo row) PEEKS it open to 240px as an
 * OVERLAY and reveals the label text beside each icon, but the ROW SET is
 * identical collapsed or peeked: nothing here may add or remove a row,
 * only change its width/label. The onboarding row's own icon IS a
 * circular progress ring (see the "Onboarding row + its popover" section
 * above) — a fixed size in both states, same as any other row's icon, so
 * it needs no special handling here beyond the shared label show/hide
 * every row gets. Clicking `#rail-toggle` (rather than merely hovering)
 * PERSISTS whichever width it lands on — js/rail_toggle.js — so the next
 * page honors it too, not just this hover; that click is also what a
 * caller who wants the rail collapsed on every OTHER page, not just
 * /admin, uses to say so.
 *
 * Every peek rule below is gated on `:not(.rail-no-peek)`. `#rail-toggle`
 * lives INSIDE the rail, so the pointer that just clicked "collapse" is
 * necessarily still hovering it — `:hover` kept the peek open and the rail
 * stayed 240px wide until the caller wandered off somewhere else on the page,
 * which read as the control having done nothing. js/rail_toggle.js parks that
 * class on the rail for a pointer-driven collapse and drops it again the
 * moment the pointer leaves (or the caller tabs in, where the peek IS how
 * they read the labels), so the click lands visibly and the next hover peeks
 * normally.
 *
 * Gated to `min-width: 1025px`, deliberately never touching anything
 * inside the `max-width: 1024px` query below: that query already turns the
 * rail into a wrapping top bar with its own collapse story, and letting
 * icon-mode's rules leak in there — even harmlessly — is exactly the kind
 * of interaction CLAUDE.md's design-system contract warns against ("don't
 * let icon mode fight the narrow-screen reflow"). Keeping the two
 * breakpoints mutually exclusive sidesteps any need to reason about which
 * one wins on specificity. */
@media (min-width: 1025px) {
    /* The width transition belongs to BOTH states, not just the collapsed one.
       It used to be declared only on `.rail.rail-icon-mode` below, and a
       transition can only run when the element it is declared on still matches
       AFTER the change: clicking the toggle to EXPAND removes that class, so
       the rule carrying the transition vanished in the same frame the width
       changed and the rail snapped 56 -> 240px with no animation at all, while
       collapsing (which ADDS the class) animated normally. Declaring it on the
       base `.rail` gives the expanded state its own transition, so the toggle
       animates in both directions; the two rules below still override the
       delay per state (0s closing, .12s peeking open).

       Desktop-gated even though `.rail` itself is not, so it cannot animate
       the ≤1024px top-bar reflow on a viewport resize — the two breakpoints
       stay mutually exclusive (see the note above). */
    html[data-ui-layout="rail"] .rail {
        transition: width var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s,
                    box-shadow var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s;
    }

    /* ...and the page's own clearance moves WITH it. The rail's width was
       animated while this reservation flipped 240px <-> 56px instantly, so
       every toggle click tore the page away from the column it belongs to —
       content landed at its new position a full 160ms before the rail arrived.
       Same duration and easing, so the two edges travel together.

       Only the PERSISTED toggle changes this value; peeking deliberately
       leaves it alone (the peeked rail is an overlay), so nothing here can
       reflow the page on a stray hover. */
    html[data-ui-layout="rail"] body:has(.rail) {
        transition: padding-left var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease);
    }

    html[data-ui-layout="rail"] body:has(.rail.rail-icon-mode) {
        /* Overrides the plain `body:has(.rail)` 240px reservation above —
           `:has(.rail.rail-icon-mode)` carries the extra class in its
           argument, so it is unconditionally more specific and wins
           regardless of source order. Constant regardless of hover: the
           peeked state below is an overlay, so the page's own clearance
           never has to change. */
        padding-left: 56px;
        /* …and the fixed chrome that starts at the same edge moves with it. */
        --rail-clearance: 56px;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode {
        width: 56px;
        padding: 16px 8px;
        /* No delay on the way CLOSED — an accidental pointer leave collapses
           it right back. The hover-expanded rule below carries the ~120ms
           OPEN delay instead, so a pass-through hover doesn't peek it.
           The one thing that does wait is the peek's z-index (see that rule):
           it steps back down only AFTER the width has finished, or the last
           160ms of the collapse would play out with the still-wide rail back
           under the bottom-docked toolbar it was just above. */
        transition: width var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s,
                    box-shadow var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s,
                    z-index 0s linear var(--ds-motion-fast, .16s);
    }

    /* Collapsed contents: brand mark, the destination rows (onboarding
       included — its icon is a progress ring rather than a fixed glyph, see
       the dedicated section below, but it is otherwise an ordinary row) and
       the profile avatar — the label TEXT beside each icon is what this
       hides, nothing that needs text width to read. It comes back the
       instant the rail is peeked open (see the `:is()` rule below).
       `.rail-getstarted-body` is this row's label (title + "N of 5 steps
       complete", two lines rather than `.rail-i-label`'s one) — same
       mechanism, its own selector because it isn't `.rail-i-label` itself.

       The CONVERSATION REGION is hidden here too, and it is the one entry in
       this list that is not merely a label: its rows are text end to end and
       have no icon form to collapse to. It is hidden only while collapsed —
       peeked or persisted-expanded, the rail is the standard rail,
       conversations included. The shift this causes in the bottom zone
       (Library, Agents, Admin, the onboarding row, the profile) on
       peek/collapse is accepted and confined: `.rail` is `position: fixed`
       (see the `:is()` rule below), so nothing outside it reflows, and
       inside it the bottom zone simply rides `.rail-nav-bottom`'s
       `margin-top: auto` up and down — the same path a caller without a
       chat grant already takes. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-i-label,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-getstarted-body,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-user-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-search,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-history {
        /* `visibility`, NOT `display` — the whole choreography depends on it.
           `display: none` took every label and the conversation region OUT OF
           FLOW while collapsed, so the peek had to put them back at t=0, before
           a single pixel of width had been gained: one un-animatable relayout of
           the entire column, and text laid out in a 40px content box, on the
           frame the pointer arrived. That is what read as broken — content
           arriving ahead of the panel that is supposed to contain it.
           `visibility: hidden` keeps the exact same boxes in flow, so hover
           changes NOTHING about layout: the only thing that moves is the rail's
           width, and the content is revealed by the column growing around it.
           It also keeps the collapsed strip inert — a `visibility: hidden` row
           takes no clicks, so the hidden conversation list under the pointer
           cannot be navigated to (plain `opacity: 0` would leave it clickable).

           Nothing can paint outside the panel either, and not by timing: every
           label in this list already clips ITSELF (`overflow: hidden` +
           `text-overflow: ellipsis` + `white-space: nowrap` on `.rail-i-label`,
           `.rail-getstarted-title`, `.rail-getstarted-sub`, `.rail-user-txt b`;
           `overflow: hidden` on `.rail-logo` and `.rail-history`). Each is a
           flex item with `min-width: 0`, so in the narrow column it is squeezed
           to ~zero and its own clip hides the text — then it is unveiled
           progressively as the width hands it room. The reveal IS the width
           animation; the fade below only keeps it from starting as a hard edge.

           Closing: the fade runs with NO delay while the width also shrinks, so
           the text is gone before the column has narrowed far enough to crop it
           — the open sequence in reverse. `visibility` flips at the END of that
           fade (the `0s` transition delayed by the same duration), which is what
           lets it animate at all: it is a discrete property, so it must be
           stepped after the opacity it is guarding, never with it. */
        opacity: 0;
        visibility: hidden;
        transition: opacity var(--rail-peek-text-out) var(--ds-ease-standard, ease) 0s,
                    visibility 0s linear var(--rail-peek-text-out);
    }

    /* ...and back, peeked open — ONE rule for the same list the hide above
       carries, because there is now one mechanism and no reason for the
       conversation region, the one-line labels and the two-line labels to be
       revealed by three different rules. That split existed only to restore
       three different `display` values (`flex`, `inline`, `flex`); with
       `visibility` there is nothing per-element left to say, and the three
       rules had already drifted once — the conversation region's got `display`
       back but not `opacity`, so a peeked rail reserved its full height and
       painted none of it: a ~500px void between New chat and Library where the
       Pinned/Chats lists belong, which is the whole reason to peek.

       Hover and keyboard `:focus-within` only — there is no separate "pinned
       open" class any more: clicking `#rail-toggle` persists the preference
       instead (removes `.rail-icon-mode` outright), which already produces this
       same expanded look without needing a transient peek class to hold it open.

       `visibility` steps to visible at the START of the fade here (delay, not
       duration — the mirror of the hide rule, where it steps at the end), so the
       content is present for every frame of its own reveal and for none before
       it. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-i-label,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-getstarted-body,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-user-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-search,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-history {
        opacity: 1;
        visibility: visible;
        transition: opacity var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) var(--rail-peek-text-delay),
                    visibility 0s linear var(--rail-peek-text-delay);
    }

    /* A KEYBOARD caller skips the reveal delay. Sequencing content behind the
       column is a courtesy to the eye; to a `visibility: hidden` element it is
       also 140ms of not being focusable, and Tab does not wait — held down, it
       would step from New chat straight past the conversation list to Library
       because those rows were not yet in the tab order. Hover has no such
       hazard, so only the focus-driven peek opts out.

       Same specificity as the rule above (`:is()` takes its most specific
       argument, one pseudo-class either way), so this wins on source order —
       keep it after. Nothing can paint outside the panel meanwhile: the labels
       are clipped by their own boxes, not by this timing. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-logo-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-i-label,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-getstarted-body,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-user-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-search,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):focus-within .rail-history {
        transition: opacity var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s,
                    visibility 0s linear 0s;
    }

    /* Centre the icon in the narrow column now that its label is hidden — as
       PADDING, not as `justify-content: center`.
       Both express the same resting position; only one of them can animate.
       `justify-content` is discrete, so the switch to `flex-start` for the
       peeked layout landed in full on the frame the pointer arrived, snapping
       every icon in the column sideways before the panel had gained a pixel —
       the other half of the "content moves before the sidebar does" problem,
       alongside the `display` relayout above. Padding interpolates, so the icons
       now travel to their expanded position over the same window the width does.

       The four values are one formula, not four guesses. The strip is 56px wide
       with a 1px right border, so its centre is 27.5px; the rail's own padding
       puts a row's content edge at 8px. Centring an icon therefore wants
       `padding-left: 19.5px - iconWidth / 2`, and the rail has four icon sizes:

         .rail-logo          30px orb     -> 4.5px
         .rail-i             18px glyph   -> 10.5px
         .rail-getstarted    28px ring    -> 5.5px
         .rail-user          36px avatar  -> 1.5px

       Verified against the measured `justify-content: center` layout this
       replaces: every icon centred at cx 27.5 before and after. If an icon's
       size changes, its value here changes with it — which is the one thing
       `justify-content` did for free, and the reason each line names its size.

       `padding-right` stays 0 while collapsed for the same reason it always
       did; the labels are clipped either way, and it animates back with the
       rest. The peek rules below restore all of it. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-i,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-user {
        padding-right: 0;
        transition: padding var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s,
                    gap var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo { padding-left: 4.5px; }
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-i { padding-left: 10.5px; }
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-user { padding-left: 1.5px; }
    /* The onboarding row's own value lives on its existing rule below rather
       than in a second `.rail-getstarted-btn` block here — it is a `.rail-i`, so
       it needs to override the 10.5px above on source order, and that rule is
       already after this one. */

    /* Hold the onboarding row at the height its two-line label gives it, in
       BOTH states. Everything else in the bottom zone is a single 34px line
       whose height can't change when its label hides, but this row's label is
       two lines (title + "N of 5 steps complete") and therefore TALLER than
       the 28px ring it sits beside — so letting the row size to its content
       made it 40px collapsed and 46px expanded, and because the bottom zone
       is pinned to the foot, those 6px slid Library / Agents / Admin upward on
       every hover. Measured, not theorised: library 639 -> 633, agents
       676 -> 670, admin 729 -> 723.

       Pinned to the taller (expanded) value rather than the shorter one so
       the label never has to compress. The cost is 6px of extra breathing
       room around the ring while collapsed, which is invisible — the ring is
       centred in it and no border or fill marks the row's bounds. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-getstarted-btn {
        min-height: 46px;
        /* 19.5px - 28/2: this row's icon is the 28px progress ring, not an 18px
           glyph, so it centres on a different padding than the `.rail-i` value
           above (see that rule for the formula). */
        padding-left: 5.5px;
    }

    /* Peek open — hover, or keyboard `:focus-within` (a caller tabbing into
       the rail needs the labels back). ~120ms delay on the way OPEN so an
       accidental pointer pass-through doesn't peek it; the collapsed rule
       above carries no delay, so closing on mouse-leave is instant. Width
       only: `.rail` is `position: fixed`, so growing it floats OVER the
       page instead of pushing it — whatever sits behind it keeps the
       position/size it got from the 56px `body` clearance above, so
       nothing reflows on an accidental hover. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) {
        width: 240px;
        /* Above the bottom-docked bars for the duration of the peek. They are
           `position: fixed` at z-index 55 (`.fbar-dock` + its veil on /library,
           `.ch-bulk` on /chats) — deliberately above the rail's own 40, which
           is right in the SETTLED states, where each starts at
           `--rail-clearance` and the two never share a pixel. A peek breaks
           that assumption: the rail floats 184px over the page WITHOUT the
           clearance changing (nothing may reflow on hover), so those 184px
           landed under the toolbar card and its frosted veil — the rail's own
           rows showing through a blur that belongs to the page behind it. The
           rail also has a z-index of its own, so its popovers (the onboarding
           panel, the Admin flyout — nominally 60) resolve INSIDE its stacking
           context and were covered by the same bars. Raised only while
           peeking, and only to 56: an upload dialog / command palette (50, and
           full-viewport) still covers the settled rail, and a tour promotes
           this context to 9011 by its own class when it needs to. */
        z-index: 56;
        /* `--ds-shadow-side`, not `-lg`: every other shadow token casts
           straight DOWN, which puts nothing along the one edge that matters
           here — the right edge, where 184px of newly-widened panel now sits
           on top of the page. With a downward shadow the peek separated from
           the content it covered by a 1px hairline, so a hero behind it read
           as sliced off rather than overlaid. */
        box-shadow: var(--ds-shadow-side);
        transition: width var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) .12s,
                    box-shadow var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) .12s,
                    /* Opening, the raise is immediate: the width's own 120ms
                       anti-pass-through delay already holds the peek back, and
                       a rail that grows over the toolbar before outranking it
                       would flash the overlap this rule exists to prevent. */
                    z-index 0s linear 0s;
    }

    /* The peeked layout — the rows' expanded padding and gaps, which the
       collapsed rules above override with the centring values and which they
       animate back to over the same window the width uses. Nothing here is
       discrete, so there is no longer a `justify-content: flex-start` to switch
       to: the rows are `flex-start` in both states and the icons travel by
       padding alone. (The label reveal is the single shared rule near the top of
       this section, not three rules down here — see the note there.) */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo {
        gap: 9px;
        padding-left: 8px;
        padding-right: 8px;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-i {
        gap: 11px;
        padding-left: 11px;
        padding-right: 11px;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-user {
        gap: 10px;
        padding-left: 8px;
        padding-right: 8px;
    }

    /* The logo row shrinks to a single ~40px slot when collapsed — position
       the anchor for the toggle below. Its padding animates with every other
       row's. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row {
        position: relative;
        padding-left: 0;
        padding-right: 0;
        transition: padding var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-toggle {
        transition: opacity var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease);
    }

    /* `#rail-toggle` is absolutely positioned rather than sitting beside the orb
       — there is no room for both at 56px — and stays invisible until
       hover/focus reveals it, so "only the orb shows" at rest and no permanently
       visible chrome sits in the collapsed strip for a caller who already has a
       free way to expand (hover on the rail itself, or Tab).

       Anchored to the row's RIGHT edge (`right`, with `left: auto`), which is
       where it belongs once the rail is open. It used to be centred in the slot
       (`inset: 0; margin: auto`) and switch to `position: static` on peek, and
       that switch was a TELEPORT: `position` is discrete, so on the frame the
       pointer arrived the control jumped from the middle of a 56px strip to the
       right end of a column that had not widened yet — and it was already fully
       opaque by then, because its reveal carried no delay. Anchoring it right in
       both states means the edge it is pinned to is the one that animates, so it
       simply rides the widening panel to its expanded position; the peeked rule
       below now only has to say `right: 8px` to match the row's own padding.

       Touch keeps the centred variant — see the `hover: none` query below, where
       the toggle is visible at rest and has no animation to be part of. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-toggle {
        position: absolute;
        top: 0;
        bottom: 0;
        right: 0;
        left: auto;
        margin: auto 0;
        opacity: 0;
        pointer-events: none;
    }

    /* Hover or keyboard focus on the LOGO ROW — not the whole rail — reveals
       the toggle. It fades in on `--rail-peek-text-delay`, the same beat as the
       labels and the conversation list: the collapse control is content too, and
       revealing it at t=0 (as this did) put a chevron on screen before the panel
       holding it had started to move. The orb no longer gives way to it — that
       swap was for a 56px slot where the two overlapped, and the toggle now
       spends the whole reveal at the right-hand end. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:hover .rail-toggle,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:focus-within .rail-toggle {
        opacity: 1;
        pointer-events: auto;
        transition: opacity var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) var(--rail-peek-text-delay);
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:hover .rail-logo,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:focus-within .rail-logo {
        opacity: 0;
        pointer-events: none;
    }

    /* ...but once the rail has actually PEEKED open, the logo row goes back to
       its expanded layout: brand on the left, toggle static at the right end.
       The orb/toggle swap above exists for a 56px slot that has room for only
       one of them; at 240px there is room for both, and the row is where the
       user was told to look for the control ("toggle on the same row as the
       logo"). Without this the peeked rail showed a lone chevron and NO brand,
       because the swap rules above still matched.

       Selectors carry `.rail-logo-row` so they match the specificity of the
       swap rules they override and win on order rather than on a `!important`.
       `:is(:hover, :focus-within)` on the rail (not the row) is deliberate:
       peeking is triggered by hovering ANY part of the rail, so the row must
       take its expanded shape even when the pointer is nowhere near it. */
    /* 8px matches the base `.rail-logo-row` padding at the top of this file —
       icon mode zeroes it to centre the orb in 56px, and peeking restores it. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-row {
        padding-left: 8px;
        padding-right: 8px;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-row .rail-logo {
        opacity: 1;
        pointer-events: auto;
    }

    /* The toggle stays absolutely positioned — see the note above for why it no
       longer switches to `position: static` here. `right` matches the row's own
       padding, which lands it exactly where the static child sat, and animates
       with it rather than jumping to it. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-row .rail-toggle {
        right: 8px;
        opacity: 1;
        pointer-events: auto;
        transition: opacity var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) var(--rail-peek-text-delay),
                    right var(--ds-motion-fast, .16s) var(--ds-ease-standard, ease) 0s;
    }

}

/* No transition, full stop — combined (not nested) with the desktop-only
   gate above so this can't accidentally apply inside the ≤1024px query. */
@media (min-width: 1025px) and (prefers-reduced-motion: reduce) {
    /* The base `.rail` + `body` pair that carries the toggle's own animation
       (declared at the top of the desktop query above), not just the collapsed
       state — otherwise expanding still glided while everything else jumped. */
    html[data-ui-layout="rail"] .rail,
    html[data-ui-layout="rail"] body:has(.rail),
    html[data-ui-layout="rail"] .rail.rail-icon-mode,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within),
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-toggle,
    /* The rows whose padding now carries the icons to their expanded position,
       and the logo row's alongside them — with motion reduced the width jumps
       straight to 240px, so an icon that still travelled would arrive after the
       column it sits in. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-i,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-user,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row,
    /* Both halves of the reveal — the hide rule holds the CLOSE timing and the
       peek rule the OPEN timing, so listing only one would leave that direction
       still fading (and still stepping `visibility` behind it). */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-i-label,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-getstarted-body,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-user-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-history,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-history,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-i-label,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-getstarted-body,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-user-txt,
    html[data-ui-layout="rail"] .rail.rail-icon-mode:not(.rail-no-peek):is(:hover, :focus-within) .rail-logo-row .rail-toggle,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:hover .rail-toggle,
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo-row:focus-within .rail-toggle {
        transition: none;
    }
}

/* Touch (`hover: none`) can never trigger the reveal-on-hover swap above —
 * there is no hover event to fire it — so the toggle defaults to visible
 * there instead of waiting for a gesture that cannot happen. This is what
 * replaces the old dedicated `#rail-icon-toggle` tap affordance: one
 * control, shown unconditionally here rather than a second one bolted on
 * for touch alone. */
@media (min-width: 1025px) and (hover: none) {
    /* Centred in the slot, not right-anchored. The desktop rule pins it to the
       row's right edge so it can ride the widening panel instead of teleporting
       — but that is a choreography concern, and there is no choreography here:
       this toggle is visible at REST in a 56px strip, with the orb hidden behind
       it, so the middle of the slot is the only place it belongs. */
    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-toggle {
        inset: 0;
        right: auto;
        left: 0;
        margin: auto;
        opacity: 1;
        pointer-events: auto;
    }

    html[data-ui-layout="rail"] .rail.rail-icon-mode .rail-logo {
        opacity: 0;
        pointer-events: none;
    }
}

/* Small + tablet screens: the fixed column becomes a normal top block so
   content isn't squeezed. Raised from the prototype's 760px to 1024px so
   tablet-landscape widths (the ~760–1024px range, where a fixed 240px rail
   left the content column as little as ~660px) also get the top-bar layout;
   above this, laptops/desktops keep the side rail with a comfortable ≥784px
   content column. */
@media (max-width: 1024px) {
    /* Selector kept in lockstep with the desktop rule above: `:has()` takes the
       specificity of its argument, so a plain `body` here would now LOSE to
       `body:has(.rail)` and the 240px reservation would survive into the
       top-bar layout. Same specificity, later in the file → this wins. */
    html[data-ui-layout="rail"] body:has(.rail) {
        padding-left: 0;
        /* The rail is a top bar here, so there is no left edge to clear — and
           the fixed chrome keyed on this had its own 240px hardcoded with a
           `max-width: 760px` escape hatch, leaving the docked toolbar inset
           240px from nothing across the whole 761–1024px band. */
        --rail-clearance: 0px;
    }

    html[data-ui-layout="rail"] .rail {
        position: static;
        width: auto;
        flex-direction: row;
        flex-wrap: wrap;
        align-items: center;
        border-right: none;
        border-bottom: 1px solid var(--ds-border);
        /* The base rule sets `overflow: visible` so the Studio hover flyout can
           escape the fixed column's right edge — harmless there since the rail
           IS the viewport-height column. In this row layout `.rail` is back in
           normal document flow, so unconstrained overflow drags the whole page
           horizontally (most visibly when the tour scrolls a nav item into
           view). Cap the bar's own width — but keep `.rail` itself
           `overflow: visible`: making the BAR the scroll box (`overflow-x:
           auto`) axis-couples `overflow-y` to `auto` per the CSS Overflow
           spec, clipping the Get-started popover and the account menu that
           open downward out of the bar (Devin Review on #1092). The scroll
           containment lives on `.rail-nav` below instead — the popover and
           user-menu panels are its SIBLINGS, so they escape freely while the
           nav list (the thing the tour scrolls into view) scrolls
           internally. */
        max-width: 100%;
    }

    html[data-ui-layout="rail"] .rail-nav {
        flex-direction: row;
        flex-wrap: wrap;
        /* See the `.rail` comment above: this is the scroll box that keeps a
           tour-driven scrollIntoView from dragging the page sideways, scoped
           to an element that contains no popovers. */
        max-width: 100%;
        overflow-x: auto;
    }

    /* `.rail-i` is `width: 100%` for the column layout, where every item
       should span the rail's fixed 240px. Left unscoped here, that same
       100% resolves against the row's own width inside a wrapping flex
       container — each item claims a full line, which is the other half of
       why the bar overflowed (independent of the overflow fix above). */
    html[data-ui-layout="rail"] .rail-i {
        width: auto;
    }

    /* New chat needs no override of its own — it is a `.rail-i`, so the rule
       above already covers it. */

    /* The 8px that separates New chat from the recents is a COLUMN metric.
       In the bar the two are laid out side by side, where it reads as a stray
       right-hand gap. */
    html[data-ui-layout="rail"] .rail-nav-top {
        margin-bottom: 0;
    }

    /* The column's 14px under the wordmark is air above the first section
       label. Here the logo shares a row with the collapse toggle, where the
       same value reads as the mark sitting high in its own row. */
    html[data-ui-layout="rail"] .rail-logo-row {
        padding-bottom: 8px;
    }

    /* The collapsed/expanded rail-width preference has no meaning below
       1025px — the ≤1024px query above already turns the rail into a
       wrapping top bar with its own (unrelated) collapse toggle
       (`.rail-collapse-toggle`) — so the width control has nothing to do
       here and would just be a second, confusing button beside it. */
    html[data-ui-layout="rail"] .rail-toggle {
        display: none;
    }

    /* The conversations region is sized by the COLUMN layout's free space:
       `.rail-history: flex: 1 1 auto` over a body with `flex-basis: 0`, which
       resolves to a real height only inside the rail's fixed viewport height.
       Here `.rail` is an auto-height bar, so there is no free space to grow
       into and the whole region collapsed to 0 — Pinned and Chats were rendered
       and then clipped to nothing, i.e. the expanded bar showed every nav row
       and no conversations at all (which the collapse toggle's own contract
       says it should).

       Content-driven height instead, capped so a long history can't push the
       nav rows and the profile off a phone screen: the lists still scroll
       inside the same box, with the same fade. */
    html[data-ui-layout="rail"] .rail-history {
        flex: 0 0 auto;
    }

    html[data-ui-layout="rail"] .rail-history-body {
        flex: 0 1 auto;
        max-height: 45vh;
    }

    /* The row layout has nothing to collapse to — every nav item, the Chats
       history, and Admin stayed permanently on screen, eating vertical
       space, and at narrow widths flex-wrap grew the bar to several rows.
       The toggle (hidden entirely above this breakpoint) shows/hides this
       group; the foot below (the profile row) always stays reachable
       regardless of its state. Collapsed by default. */
    html[data-ui-layout="rail"] .rail-collapse-toggle {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        width: 40px;
        height: 40px;
        margin-left: auto;
        border: 1px solid var(--ds-border);
        border-radius: 9px;
        background: var(--ds-surface);
        color: var(--ds-text-secondary);
        cursor: pointer;
        flex-shrink: 0;
    }

    html[data-ui-layout="rail"] .rail-collapse-toggle svg {
        width: 20px;
        height: 20px;
    }

    html[data-ui-layout="rail"] .rail-collapsible {
        display: none;
        /* Always its own full-width line below the logo+toggle row, rather
           than squeezing in beside them (.rail is flex-wrap: wrap here). */
        flex: 1 1 100%;
        flex-direction: column;
    }

    html[data-ui-layout="rail"] .rail.is-nav-open .rail-collapsible {
        display: flex;
    }

    html[data-ui-layout="rail"] .rail-foot {
        margin: 0;
        border: none;
        padding: 0;
        flex-direction: row;
    }

    /* The user-row divider is a bottom-of-column separator; in the row
       layout it would read as a stray top border, so drop it. */
    html[data-ui-layout="rail"] .rail-user-menu {
        border-top: none;
        padding-top: 0;
    }


    html[data-ui-layout="rail"] .rail-user-menu .app-user-menu-panel {
        bottom: auto;
        top: calc(100% + 8px);
    }

    /* The onboarding card rides inside .rail-collapsible now (it closes the
       bottom nav rather than sitting in the foot), so it appears with the rest
       of the nav when the bar is expanded and needs no compact form of its
       own — the 40×40 icon-with-badge treatment it used to get in the foot row
       is gone with the row. Only the popover has to move: it opens DOWNWARD
       here, since the rail is at the top of the page. */


    /* Popover opens downward too when the rail is a top bar, and anchors to the
       launcher's RIGHT edge (opening leftward) since the compact icon button
       sits at the right end of the bar — a left-anchored 280px panel would run
       off the viewport. */
    html[data-ui-layout="rail"] .rail-getstarted-panel {
        bottom: auto;
        top: calc(100% + 8px);
        left: auto;
        right: 0;
        width: 280px;
    }
}

