/* =====================================================================
 * Design tokens (Phase 1 - CSS variables)
 * ---------------------------------------------------------------------
 * All color / shadow / radius / font values used throughout this file
 * are sourced from semantic tokens defined here. A downstream theme
 * system can override any of these via `[data-theme="X"] { ... }`
 * selectors WITHOUT touching component rules below.
 *
 * Naming rule: tokens describe ROLE, not literal value. Defaults below
 * preserve the original Claude-coral dark palette - pixel-identical to
 * pre-refactor when no `data-theme` attribute is set on <html>.
 * ===================================================================== */
:root {
    /* ---- Fonts -------------------------------------------------- */
    /* This used to be `'SF Mono', monospace`, which silently degrades on
     * phones. The literal family name "SF Mono" is NOT exposed to web
     * content on iOS, so every iOS browser (all of them are WebKit,
     * including Brave and Chrome) skipped straight past it to the generic
     * `monospace`, landing on Courier - a different metric from what the
     * desktop measures. `ui-monospace` is the correct way to ask for the
     * platform monospace face and DOES resolve to SF Mono on iOS and
     * macOS. The rest of the chain is an explicit, metric-compatible
     * fallback per platform so that a miss degrades to a known face
     * instead of whatever `monospace` happens to mean. */
    --font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco,
                 'Cascadia Mono', 'Roboto Mono', 'Courier New', monospace;

    /* ---- Backgrounds -------------------------------------------- */
    --color-bg:               #1e1e1e;            /* body / terminal / modal */
    --color-bg-page:          #0a0a0a;            /* header / tooltip surface */
    --color-bg-elevated:      #2d2d30;            /* panels / inputs / cards */
    --color-bg-card:          #252526;            /* elevated card background */
    --color-bg-hover:         #323235;            /* row hover state */
    --color-bg-overlay:       rgba(0, 0, 0, 0.85); /* modal scrim */
    --color-header-bg: var(--color-bg);
    --color-bg-overlay-soft:  rgba(0, 0, 0, 0.5);  /* float-button shadow */
    --color-bg-overlay-faint: rgba(0, 0, 0, 0.3);  /* inline <code> bg */

    /* ---- Foregrounds -------------------------------------------- */
    --color-fg:        #d4d4d4;   /* primary text */
    --color-fg-muted:  #858585;   /* secondary / descriptive text */
    --color-fg-subtle: #666;      /* placeholder / disabled */
    --color-fg-faint:  #888;      /* version label / age text */

    /* ---- Borders ------------------------------------------------ */
    --color-border:        #3e3e42;
    --color-border-subtle: #2d2d30;   /* row separators inside lists */

    /* ---- Accent (Claude coral - primary brand) ------------------ */
    --color-accent:                #d77757;
    --color-accent-strong:         #e88768;                 /* hover variant */
    --color-accent-bg-soft:        rgba(215, 119, 87, 0.08);
    --color-accent-bg-faint:       rgba(215, 119, 87, 0.10);
    --color-accent-bg:             rgba(215, 119, 87, 0.15);
    --color-accent-bg-hover:       rgba(215, 119, 87, 0.25);
    --color-accent-bg-active:      rgba(215, 119, 87, 0.35);
    --color-accent-border-soft:    rgba(215, 119, 87, 0.20);
    --color-accent-border:         rgba(215, 119, 87, 0.30);
    --color-accent-border-strong:  rgba(215, 119, 87, 0.60);
    --color-accent-border-mid:     rgba(215, 119, 87, 0.50);
    --color-accent-glow:           rgba(215, 119, 87, 0.40);
    --color-accent-glow-strong:    rgba(215, 119, 87, 0.50);
    --color-accent-shadow-soft:    rgba(215, 119, 87, 0.30);
    --color-on-accent:             #1e1e1e;                 /* text on accent fill */

    /* ---- Status: success / warning / danger / info -------------- */
    --color-success:        #4ade80;
    --color-warning:        #fbbf24;
    --color-danger:         #ff4444;
    --color-info:           #4fc1ff;

    --color-danger-bg:       rgba(255, 68, 68, 0.10);
    --color-danger-bg-hover: rgba(255, 68, 68, 0.20);
    --color-danger-border:   rgba(255, 68, 68, 0.30);
    --color-danger-glow:     rgba(255, 68, 68, 0.40);

    --color-info-bg:     rgba(79, 193, 255, 0.10);
    --color-info-border: rgba(79, 193, 255, 0.30);

    --color-warning-bg:     rgba(251, 191, 36, 0.18);
    --color-warning-border: rgba(251, 191, 36, 0.35);

    /* Soft / strong danger variants used by the kill control */
    --color-danger-soft:        #e88;
    --color-danger-strong:      #ff6b6b;
    --color-danger-strong-bg:   rgba(255, 107, 107, 0.12);

    /* ---- Connection status indicator dot ------------------------ */
    --color-status-connected: #00ff00;
    --color-status-error:     #ff0000;
    --color-status-pending:   #ffa500;
    --color-status-connected-glow: rgba(0, 255, 0, 0.8);
    --color-status-error-glow:     rgba(255, 0, 0, 0.8);
    --color-status-pending-glow:   rgba(255, 165, 0, 0.6);

    /* ---- Session badges (running list) -------------------------- */
    --color-badge-owned-bg:    #2a3a2a;
    --color-badge-owned-fg:    #4ade80;
    --color-badge-tmux-bg:     #2a2a3a;
    --color-badge-tmux-fg:     #8899ff;
    --color-badge-external-bg: #4a3a2a;
    --color-badge-external-fg: #fbbf24;

    /* feat/agent-family-pills - a FACT (explicit wrapper/family) and a
       GUESS (fingerprint/derived_deepest) must not look alike; unknown
       is a third, visibly neutral state - see .family-pill below. */
    --color-family-fact-bg:    #2a3a3a;
    --color-family-fact-fg:    #5eead4;
    --color-family-guess-bg:   #3a2a3a;
    --color-family-guess-fg:   #e879f9;
    --color-family-unknown-bg: #333333;
    --color-family-unknown-fg: #999999;

    /* ---- Agent badges (Phase 8 will consume these) -------------- */
    --badge-color-claude:    #d77757;
    --badge-color-codex:     #4ade80;
    --badge-color-hermes:    #8899ff;
    --badge-color-openclaw:  #fbbf24;

    /* ---- Syntax-ish accents (local-servers link / heading) ------ */
    --color-syntax-cyan: #4ec9b0;
    --color-syntax-blue: #569cd6;

    /* ---- Shadows ------------------------------------------------ */
    --shadow-modal: 0 4px 12px rgba(0, 0, 0, 0.5);

    /* ---- Radii (sm, md, lg: the rule after :root, theme-contract.json legacy radii) */
    --radius-pill: 50px;
    --radius-full: 50%;

    /* The sidebar group header's own gutter - same convention as the retired
     * project tree gutter, applied to the
     * conversations panel's section headers (pinned / other / a user's
     * own groups): a fixed-width column for the row count so a group
     * name starts at the same x whether it holds "1" conversation or
     * "120". Narrower than the project tree's because this gutter holds
     * only the count (the chevron is a separate element after it, not
     * sharing the column). See `.session-sidebar-group__gutter` in
     * client/css/session-sidebar-groups.css. */
    --sidebar-gutter: 22px;

    /* NO FLOATING BUTTON ROW, AND NO `--fab-*` TOKENS. The terminal
     * screen's round bottom-row buttons (slash, d-pad, session editor,
     * terminal tools) are all deleted; the last, terminal tools, went on
     * 2026-09-22 when its menu moved onto the key bar's clipboard key
     * (client/js/terminal-key-bar.js). A token nothing reads is how a
     * dead layout gets revived by accident, so `--fab-size`,
     * `--fab-edge`, `--fab-gap` and `--fab-slot-0` went with it. */

    /* THERE IS NO TOP-RIGHT RAIL, AND NO `--fab-top-edge` TOKEN.
     *
     * A wide three-icon strip used to cover the terminal's top edge
     * here, and after it was removed the session editor was pinned back
     * over that corner alone, offset downward from the header by a
     * token that resolved to one --fab-gap below --header-h. Written as
     * prose, not as a declaration, because a stale example in a comment
     * is enough to satisfy a test that greps for one. The session
     * editor became a header control and then a header kebab row, so
     * nothing is left to offset and the token is deleted rather than
     * left defined and unread - the same rule that killed
     * `--fab-slot-2` above. Wanting
     * a floating control over the terminal's top corner again means
     * writing this back deliberately, which is the point.
     *
     * --header-h below stays, and is still derived rather than
     * measured: the fixed layers that must clear the header
     * (.fab-menu-notice, the config-editor drawer) read it, and its
     * parts are the same tokens .header and .btn-icon use at each
     * breakpoint, so the clearance cannot drift when the header is
     * restyled. The standalone safe-area inset is added in
     * ios-chrome.css, where the header takes it too. */
    --header-pad-y: 12px;
    /* The header's side padding, and therefore the gap between the
     * viewport edge and the first control in the header. Tokenised
     * because the docked-sidebar rule in session-sidebar.css has to ADD
     * this gap back on top of the sidebar's width - the toggle sat flush
     * against the docked bar's border when that rule paid the sidebar
     * width alone. Restated per breakpoint below, same as --header-pad-y. */
    --header-pad-x: 20px;
    /* Horizontal padding inside #launchpad-screen. A token rather than a
     * literal because the docked-sidebar rule in session-sidebar.css has
     * to ADD to it: `#launchpad-screen` is an ID selector and outranks
     * `body.session-sidebar-pinned .screen`, so the generic docked rule
     * lost to it and the home screen paid no layout for a pinned bar at
     * all - the bar simply covered the content. Measured, not guessed:
     * padding-left read back as 20px with the bar docked at 320px wide. */
    --screen-pad-x: 20px;
    --header-border: 2px;
    /* HOME-HEADER-CONSOLIDATION: the launchpad's second header row
     * (`.home-subheader`) adds real height that fixed-position consumers
     * of --header-h (e.g. .fab-menu-notice) must clear too, or they land
     * under the subheader text instead of below the header. Zero on every
     * screen except the launchpad, which stamps `.header--home` on
     * <body> (app.js setHeaderIdentity) alongside the header itself. */
    --home-subheader-extra: 0px;
    --header-h: calc(var(--control-size) + var(--header-pad-y) * 2
                     + var(--header-border) + var(--home-subheader-extra));
    /* THE TOP EDGE OF EVERY COMPILED OVERLAY (catalog, ledger, themes):
     * the height of whatever chrome sits above it. Off the home screen that
     * is the shared `.header` above; client/css/launchpad-host.css restates
     * it as the opscenter top bar while the dashboard is up. */
    --overlay-top: var(--header-h);

    /* The app's default round control, and therefore what sets the
     * header's height. Bumped to a thumb target on touch widths. */
    --control-size: 36px;
}

/* a session theme paints its cssVars on #terminal-container, so the legacy radii resolve there as well as on :root */
:root, #terminal-container { --radius-sm: var(--legacy-radius-sm, 3px); --radius-md: var(--legacy-radius-md, 4px); --radius-lg: var(--legacy-radius-lg, 8px); }

@media (max-width: 768px) {
    :root {
        --header-pad-y: 10px;
        --header-pad-x: 15px;
        --control-size: 44px;
    }
}

@media (max-width: 480px) {
    :root {
        --header-pad-y: 8px;
        --header-pad-x: 12px;
        --control-size: 40px;
    }
}

/* Base styles */
@layer reset {
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
}

/* APP SHELL HEIGHT - the terminal depends on this being DEFINITE.
 *
 * This used to be `min-height: 100vh` with `height: auto`. A flex column
 * whose own height is auto has no free space to distribute, so
 * `.screen { flex: 1 }` and `.terminal-container { flex: 1 }` degraded to
 * content sizing: the terminal's height was set by the xterm element,
 * whose height was set by `rows`, which FitAddon derived from the
 * terminal's height. That loop can only ratchet UP. Measured on a
 * 390x844 phone shell: rotate to landscape (390 tall) and rows stayed at
 * 45 with the terminal 720px tall inside a 390px window - the prompt sat
 * below the fold and tmux kept the taller geometry. Same failure for the
 * on-screen keyboard.
 *
 * A DEFINITE height breaks the loop: the shell is exactly one viewport,
 * `.screen` gets the leftover, and the terminal shrinks as well as grows.
 * `100dvh` tracks the iOS URL bar collapsing; `100vh` is the fallback for
 * browsers without dynamic viewport units. `overflow: hidden` on the
 * shell is the consequence of a fixed height - each `.screen` is its own
 * scroll container (see `.screen` below), so nothing loses scrollability.
 */
/* PAGE BACKGROUND LIVES ON `html`, NEVER ON `body`.
 *
 * Theme effects mount a `position: fixed; z-index: -1` canvas as a child of
 * body. CSS 2.1 Appendix E paints, within the root stacking context, in this
 * order: (1) the root element's own background, (2) child stacking contexts
 * with NEGATIVE z-index, (3) the backgrounds of in-flow non-positioned
 * block-level descendants. The effects canvas is step 2; `body`'s background
 * box is step 3. So ANY opaque background on body paints straight over the
 * canvas and the effect becomes invisible while still running perfectly.
 *
 * This is not hypothetical - it is the defect this rule was written to fix.
 * Every one of the 23 themes measured a maximum composited RGB delta of 0
 * against a canvas-hidden plate, including matrix, whose canvas contained
 * pixels spanning the full 0-255 green range.
 *
 * `body` background propagation would normally rescue this (a transparent
 * root adopts body's background and paints it at step 1, below the canvas),
 * but ios-chrome.css gives `html` its own background to paint the notch and
 * home-indicator insets, and a non-transparent root cancels propagation.
 *
 * Invariant, enforced by tests/test_theme_effects_visibility.node.mjs:
 *   html paints the page colour; body's background stays transparent.
 */
html {
    background: var(--color-bg);
}

body {
    font-family: var(--font-mono);
    background: transparent;
    color: var(--color-fg);
    height: 100vh;
    height: 100dvh;
    display: flex;
    flex-direction: column;
    margin: 0;
    overflow: hidden;
}

/* Header */
.header {
    position: sticky;
    top: 0;
    z-index: 1000;
    background: var(--color-bg-page);
    /* Vertical padding and the border are two of the three parts of
     * --header-h, which every fixed layer that has to sit BELOW the
     * header derives its own top offset from. Write the number here
     * instead and those layers slide under the header. */
    padding: var(--header-pad-y) var(--header-pad-x);
    border-bottom: var(--header-border) solid var(--color-accent);
}

/* HOME-HEADER-CONSOLIDATION: the actual flex row (toggle / title /
 * controls) used to be `.header` itself. It is now `.header-row`, a
 * dedicated wrapper, so `#home-subheader` (launchpad only, second row of
 * `.header`) is never a SIBLING of `h1` inside the same flex/grid parent
 * - header-title-fit.js's slotWidth() computes the title's shrink budget
 * from `h1`'s parent's children on the documented assumption that they
 * are all fixed-size, single-row content. A full-width second row in
 * that same parent broke that assumption and starved the budget to
 * negative, which its own fallback silently read as "always fits" and
 * left CSS `text-overflow: ellipsis` to truncate mid-word instead. See
 * `.header--home` below for why the wrapper's own layout still changes
 * on the launchpad. */
.header-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    gap: 15px;
}

/* HEADER TITLE MUST SHRINK, CONTROLS MUST NOT.
 *
 * This rule used to be `white-space: nowrap` and nothing else. As a flex
 * item of `.header` that is a layout bug with a very specific shape: a
 * flex item's `min-width` defaults to `auto`, which is its MIN-CONTENT
 * size, and min-content for a nowrap single line is the whole string. So
 * the title refused to shrink below its own text and pushed everything
 * after it out of the box instead. Measured on a 390px viewport with a
 * real session name: h1 ran x=63..413 (23px past the edge), `.controls`
 * sat at x=428..488 entirely off screen, `#header-menu-toggle` was 58px
 * of fully invisible kebab, and documentElement.scrollWidth was 488
 * against a 390 innerWidth - 98px of horizontal overflow.
 *
 * `min-width: 0` is the required half of the fix, not an optimisation.
 * This is the third time the min-width:auto flex trap has bitten this
 * codebase; see `.session-row` and `.cloude-session-theme__head`.
 *
 * h1 becomes a flex CONTAINER too, so the icon, the version chip and the
 * rename pencil can each opt out of shrinking (`flex-shrink: 0`) and all
 * the shrinking is concentrated in `#header-title-text`, which is the
 * only part that is safe to truncate.
 */
/* THE ICON AND THE TITLE MUST SHARE ONE LINE BOX.
 *
 * `line-height` here is an ALIGNMENT declaration, not typography. h1 is a
 * flex row with `align-items: center`, so the icon BOX and the title BOX
 * are centred against each other correctly and always were - measured
 * delta 0.01px at 1280 and 0.00px at 390. The glyphs inside them were
 * not. `#header-icon` holds a bare emoji, so its content is an ANONYMOUS
 * flex item whose line box is sized by the colour-emoji fallback font,
 * whose ascent is far larger than the monospace strut's: with
 * `line-height: normal` that line box measured 23px inside a 19.19px icon
 * box at 390px (scrollHeight 23 vs clientHeight 19). Centring an oversized
 * line box centres the BOX, not the baseline inside it, so the cloud sat
 * 1.5px below the title's centre at 390px and 0.52px at 1280 - the report
 * was "top header cloud icon and title are not aligned vertically".
 *
 * A fixed unitless line-height on the SHARED parent gives both children
 * the same line box height, so neither child's font metrics can move its
 * own baseline relative to the other's. Measured after: 0.00px delta at
 * BOTH 1280 and 390. 1.2 specifically, because it equals `.header-icon`'s
 * own 1.2em box, so the line box never exceeds the h1 content height and
 * `overflow: hidden` above cannot clip a descender out of a session name.
 * Header height is unchanged at both widths (58px at 390px).
 *
 * SCOPED TO THE HEADER (`.header h1`, the only h1 in client/index.html): a bare `h1` is unlayered, so it beat every
 * @layer rule and restyled every heading rendered in the app (the file preview's h1 came out 16px, orange, nowrap).
 */
.header h1 {
    font-size: 1.3rem;
    line-height: 1.2;
    margin: 0;
    color: var(--color-accent);
    white-space: nowrap;
    cursor: pointer;
    transition: opacity 0.2s ease;
    display: flex;
    align-items: center;
    /* Take the leftover space, give it back before .controls does. */
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
}

.header h1:hover {
    opacity: 0.8;
}

/* The only shrinkable, truncatable part of the header identity. The CSS
 * end-ellipsis here is the FLOOR, not the finished behaviour: it is what
 * renders if header-title-fit.js never runs. When that module is alive it
 * replaces the text with a MIDDLE-elided string, because session names
 * share long project prefixes (`cloude_claude-config-sync-2`) and an
 * end-ellipsis would truncate away the only part that distinguishes one
 * session from another. See client/js/header-title-fit.js. */
#header-title-text {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Version chip. TYPE AND COLOUR ONLY - this rule no longer places anything.
 *
 * NO `margin-left` HERE, AND IT MUST NOT COME BACK. This chip used to be a
 * flex child of the header h1, which is a gapless flex row, so it sat flush
 * against the title (measured 0px at both 1527px and 390px) and a 10px
 * margin-left was the fix for that. The chip has since MOVED OUT of the
 * header entirely: it is the home screen's bottom bar now, rendered by
 * launchpad.js as `.version.home-bar__version` and placed by
 * client/css/home-bar.css. There is no longer a title to be flush against,
 * so the margin has nothing to separate - it would just be 10px of dead
 * space inside a flex row that already sets its own gap. Placement belongs
 * to whoever owns the box the chip sits in, and that is home-bar.css. */
.version {
    font-size: 0.7rem;
    color: var(--color-fg-faint);
    font-weight: normal;
    flex-shrink: 0;
}

/* HOME-HEADER-CONSOLIDATION - launchpad-only header-row layout.
 *
 * Toggled on/off by setHeaderIdentity() in app.js (only showLaunchpad()
 * passes a `subheader`), never applied on auth or terminal. Applies to
 * `.header-row` (the wrapper around spacer/toggle/title/controls - see
 * its own comment above for why it exists as a separate element from
 * `.header`).
 *
 * STAYS FLEXBOX. A first version switched `.header-row` to a 3-column
 * CSS Grid (`minmax(flank, 1fr) auto minmax(flank, 1fr)`) to reserve
 * equal space on both sides of the title. It centred correctly
 * (0.008px off at 1280px, 0px at 390px) but broke the title's own text
 * fit: header-title-fit.js's slotWidth() (client/js/header-title-fit.js)
 * computes the title's shrink budget by walking `.header-row`'s actual
 * DOM children and summing their RENDERED widths - an empty grid track
 * with no element in it (the reserved-but-content-less left column,
 * since `#session-sidebar-toggle` is `display: none` here) is invisible
 * to that walk. The JS concluded the full "Cloude Code Launcher" fit
 * (its math never subtracted the reserved column it couldn't see) and
 * left the untruncated string in the DOM, while the grid actually
 * rendered the title only 160px of room - so plain CSS
 * `text-overflow: ellipsis` silently clipped it to "Cloude Code L…" with
 * nothing in the DOM saying so. Confirmed by comparing the DOM
 * `textContent` (full) against the rendered box in a screenshot
 * (truncated) - a genuine "the two disagree" case, not a JS bug.
 *
 * `#header-home-spacer` (a real, `visibility: hidden` element, not a grid
 * track) fixes this at the root: it is an actual DOM sibling of `h1`
 * with a real rendered width, so it is counted correctly by both the
 * BROWSER's flex layout and header-title-fit.js's DOM walk - the two can
 * no longer disagree, because they are now reading the same geometry.
 * `#appTitle` gets `flex: 1 1 auto` (grows AND shrinks) with
 * `justify-content: center` on itself so the icon+text pair centres
 * within whatever room is actually left, and shrinks through
 * header-title-fit.js's normal, correct middle-elision path if that room
 * ever gets tighter than the full string - the SAME mechanism session
 * titles already use, not a new one.
 *
 * `--home-header-flank-w` mirrors `.controls`' own real width (TWO
 * buttons, not three - header-menu.js permanently re-parents logoutBtn
 * and settingsBtn into its overflow panel at every width, so only
 * `#configEditorBtn` and its kebab `#header-menu-toggle` stay inline).
 * Verified after the fix, both widths, full "Cloude Code Launcher" text
 * renders untruncated, 0px / 0.008px off-centre.
 */
/* Hidden by default - never touches the terminal/auth header layout.
 * Only `.header--home` (below) turns it into a real, invisible, but
 * SIZED sibling. */
.header-home-spacer {
    display: none;
}

.header--home {
    --home-header-flank-w: calc(var(--control-size) * 2 + 8px);
}

/* AND ONE CONTROL WHEN THE ROW HAS FOLDED, which is the phone. The rule
 * above counts the icons that are inline, and below 769px there are
 * none: `HeaderMenu._fold()` re-parents the file browser, the notepad
 * and the session rows into the dropdown, leaving `#header-menu-toggle`
 * alone in `.controls`. Left uncorrected, the spacer kept reserving two
 * controls' worth of width on the side that no longer has them and
 * pushed the home title off-centre by a control and a half.
 *
 * KEYED ON `.controls--folded`, NOT ON A SECOND COPY OF 768px. That
 * class is written by `_fold()` itself, so the centring follows the
 * fold by construction; a media query here would be a second statement
 * of the same boundary, free to drift from `FOLD_QUERY` the moment one
 * of the two moved. */
.header--home:has(.controls--folded) {
    --home-header-flank-w: var(--control-size);
}

.header--home .header-home-spacer {
    display: block;
    visibility: hidden;
    flex-shrink: 0;
    width: var(--home-header-flank-w);
}

/* Stamped on <body> as `.home-header-active` - DELIBERATELY NOT the same
 * class as `.header--home`. `.header--home` is a bare class selector, so
 * reusing it on <body> would make it match here too and put the WHOLE
 * PAGE in `display: grid` (caught in the browser: bodyDisplay flipped to
 * 'grid', header collapsed to grid-content width instead of the
 * viewport). --home-subheader-extra has to reach .fab-menu-notice, which
 * is not a descendant of .header, hence the separate body class, toggled
 * in lockstep with `.header--home` by app.js (setHeaderIdentity's doc
 * comment). Value = subheader line box (0.8rem * 1.2 line-height =
 * 15.36px) + its 4px top margin, rounded up. */
body.home-header-active {
    --home-subheader-extra: 20px;
}

@media (max-width: 480px) {
    body.home-header-active {
        /* .home-subheader drops to 0.72rem under 480px (see media query
         * below) - its line box shrinks with it. */
        --home-subheader-extra: 18px;
    }
}

/* THE SIDEBAR TOGGLE IS VISIBLE ON THE HOME SCREEN, AND THIS COMMENT
 * USED TO SAY THE OPPOSITE.
 *
 * It read: "#session-sidebar-toggle stays `display: none` here (app.js
 * never shows it on the launchpad) - no rule needed, it contributes
 * nothing to the flex row either way." That stopped being true when the
 * conversation bar became available (and pinnable) on the home screen:
 * app.js's showLaunchpad() calls SessionSidebar.show(), which strips the
 * `.hidden` class off the toggle. Its own doc comment says so - "Called
 * on entering the terminal screen AND the home screen". So the flank
 * arithmetic below was reasoning about an element it believed was not
 * rendered, and the element was rendered.
 *
 * Two things went wrong as a result, both measured at 1280px before the
 * fix:
 *
 * 1. THE TOGGLE WAS NOT AT THE TOP LEFT. `#header-home-spacer` precedes
 *    it in DOM order, and on the home screen the spacer becomes a real,
 *    80px, invisible-but-SIZED flex item. So the hamburger was pushed
 *    80px inboard of the header's content edge and floated in dead space
 *    between nothing and the title - measured left edge 100px against a
 *    20px content edge. On the terminal screen the spacer is
 *    `display: none`, so there the toggle is genuinely first and sits
 *    where it belongs, which is why this only ever looked wrong at home.
 *
 * 2. THE TITLE WAS NOT CENTRED. The spacer exists for exactly one job:
 *    be as wide as `.controls` so the title lands on the true centre.
 *    The toggle added a further 36px (32px box + 4px margin) to the LEFT
 *    flank with no counterpart on the right, so the left flank was
 *    116px against a right flank of 80px and the title sat 18px right of
 *    centre.
 *
 * The fix keeps both flanks equal by making them SHARE one budget rather
 * than by adding a second invisible element: the toggle is ordered to
 * the content edge, and the spacer gives back exactly the room the
 * toggle takes. `order` is safe for the title-fit code, which walks DOM
 * children and sums widths - a reordered row has the same children and
 * the same total.
 *
 * --home-header-toggle-w MIRRORS session-sidebar.css's
 * `.session-sidebar-toggle` (width 32px + margin-right 4px). That is a
 * duplicated constant across two files, so it is not left on trust:
 * tests/test_header_help_and_toggle.node.mjs measures the toggle's real
 * outer width in a browser and fails if the two ever disagree, and the
 * same suite asserts the two flanks are equal rather than asserting
 * either number. */
.header--home {
    --home-header-toggle-w: 36px;
}

.header--home #session-sidebar-toggle:not(.hidden) {
    order: -1;
}

/* Only shrink the spacer while the toggle is actually rendering. When
 * the toggle is `.hidden` (it is `display: none` then, and contributes
 * no width) the spacer must keep the full flank or the title would go
 * off centre the other way. Three states, one rule each, never a guess
 * about which one is live. */
.header--home:has(#session-sidebar-toggle:not(.hidden)) .header-home-spacer {
    width: calc(var(--home-header-flank-w) - var(--home-header-toggle-w));
}

.header--home #appTitle {
    /* Grows to fill the room the spacer/controls leave AND shrinks
     * through header-title-fit.js's normal middle-elision path if that
     * room gets tight - same budget mechanism the terminal's session
     * title already uses, just with a short fixed string here instead of
     * a long session name. */
    flex: 1 1 auto;
    min-width: 0;
    justify-content: center;
}

.header--home .controls {
    flex-shrink: 0;
}

/* Second row of `.header`, a sibling of `.header-row` (NOT a grid cell of
 * it - see the `.header-row` comment above for why h1's actual parent
 * must stay exactly the original three children). Centred by plain
 * `text-align`, which is real centring against the header's own width at
 * every viewport, not a fixed margin. */
.home-subheader {
    margin-top: 4px;
    text-align: center;
    color: var(--color-fg-muted);
    font-size: 0.8rem;
    /* Truncate rather than wrap: a second wrapped line on a phone would
     * make the header taller than the block it replaced, which is exactly
     * what this change must not do (see .header--home comment above). */
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 100%;
}

/* SESSION-IDENTITY-V2 - header identity icon. Wraps either an emoji
 * (launchpad/auth) or the brand SVG (terminal). Sizing is consistent
 * across both modes so the title baseline doesn't jump on screen swap. */
.header-icon {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 1.2em;
    height: 1.2em;
    /* NO `vertical-align` HERE, AND IT MUST NOT COME BACK. This carried
     * `vertical-align: -0.18em` from the days when the icon was an inline
     * box on the title's baseline. h1 is `display: flex`, so the icon is a
     * flex item and `vertical-align` does not apply to it at all - the
     * declaration was inert, and the harm was that it made this box LOOK
     * like alignment had been handled here. It has not: alignment is the
     * `line-height` on h1 above. */
    margin-right: 0.15em;
    /* h1 is a flex container now; the identity icon is a fixed-size
     * marker and must never be squeezed to make room for the name. */
    flex-shrink: 0;
}

.header-icon img {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* v0.7.2 - inline rename affordance in the page header. A small pencil
 * button sits next to the title text and triggers inline edit mode.
 * Styled to match the launchpad's .running-session-rename for visual consistency. */
.header-rename-pencil {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    margin-left: 0.5em;
    cursor: pointer;
    color: var(--color-fg-muted);
    border-radius: var(--radius-md);
    font-size: 14px;
    flex-shrink: 0;
    transition: color 120ms ease, transform 120ms ease, background 120ms ease;
    vertical-align: middle;
}

.header-rename-pencil:hover {
    color: var(--color-accent);
    transform: scale(1.15);
    background: var(--color-accent-bg-soft);
}

.header-rename-input {
    background: var(--color-bg-elevated, #1a1a1a);
    color: var(--color-fg);
    border: 1px solid var(--color-accent);
    border-radius: var(--radius-sm);
    padding: 2px 6px;
    font: inherit;
    font-size: 0.9em;
    margin-left: 4px;
    min-width: 120px;
    max-width: 320px;
}

.header-rename-error {
    color: var(--color-danger-strong, #e64);
    font-size: 0.75em;
    margin-left: 8px;
}

/* v0.7.2 - inline rename affordance on the launchpad's running-sessions
 * rows. The pencil button triggers inline-edit mode (swaps the name span
 * for an input). Only rows with a known session_id render the pencil
 * (toggled in renderRunningSessions); external adopt-target rows get no
 * pencil - the user can adopt first, then rename from the in-session header. */
/* The UNAVAILABLE and CANNOT-DETERMINE rename states (see
 * launchpad.js::_renderRenamePencilHtml). Same box as the live pencil so
 * the row's layout does not shift between a renameable session and one
 * that is not - the control occupies the same place either way, which is
 * the whole point of drawing it instead of omitting it.
 *
 * It is dimmed and takes no hover treatment, so it reads as present but
 * inert. `cursor: not-allowed` is the only cursor here that is not a lie:
 * `pointer` would promise an action, and `default` would read as decorative
 * text. No new colour - the same --color-fg-muted the live pencil rests
 * at, carried at reduced opacity.
 *
 * DELIBERATELY NOT `.running-session-rename` plus a modifier. The
 * delegated click handler selects on `.running-session-rename`, and a
 * class token matches whole, so a separate token is what structurally
 * guarantees this element can never reach the rename call. A modifier
 * class would leave that guarantee to a runtime `if` somebody could
 * later drop. */
.running-session-rename-unavailable {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    color: var(--color-fg-muted);
    opacity: 0.4;
    cursor: not-allowed;
    border-radius: var(--radius-md);
    flex-shrink: 0;
    font-size: 14px;
}

.running-session-rename {
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    color: var(--color-fg-muted);
    border-radius: var(--radius-md);
    flex-shrink: 0;
    transition: color 120ms ease, transform 120ms ease, background 120ms ease;
    font-size: 14px;
}

.running-session-rename:hover {
    color: var(--color-accent);
    transform: scale(1.15);
    background: var(--color-accent-bg-soft);
}

.controls {
    display: flex;
    gap: 8px;
    align-items: center;
    /* THE CONTROLS ARE THE LAST THING TO GIVE UP SPACE. Every child is a
     * fixed-size hit target, so there is nothing here that can shrink
     * gracefully - squeezing this box just pushes the buttons off screen,
     * which is exactly the bug the h1 min-width fix above exists to kill.
     * The title truncates; this does not. */
    flex-shrink: 0;
    /* Was `overflow: hidden`. Children never exceed this box's static
     * bounds (fixed-size buttons + gap), so hidden was only ever clipping
     * intentional overflow effects: the button:hover scale/glow (and,
     * until issue #981, two CSS tooltips drawn below the box). */
    overflow: visible;
}

/* Header control icons. Inline SVG paths use stroke="currentColor" so a
 * theme override of --color-accent re-skins them automatically. We pin
 * `color` on the <svg> itself (not the <button>) so danger-styled
 * buttons still render the icon in accent - preserves pre-token visual. */
.header .controls button > svg {
    color: var(--color-accent);
}

/* The delete button's icon must read as destructive, not merely its
 * border/background - a red-bordered button drawing its glyph in the
 * default accent color muddies the "this is dangerous" signal the
 * trash-can icon is there to give. Overrides the accent rule above for
 * any button.danger specifically. */
.header .controls button.danger > svg {
    color: var(--color-danger);
}

/* Round header icon-button.
 *
 * This used to be a bare `button {}` element rule reaching every <button>
 * in the DOM, not just these. That caused two separate incident classes:
 * a `border-radius: 50%` turned the settings tabs into literal ellipses
 * (fixed in place at the time, not at the source; the tabs are gone
 * with the settings redesign), and `transform: scale(1.05)` on hover leaked onto the
 * per-session theme picker rows and overflowed the list horizontally
 * (fixed at terminal-tools.css instead of here, commit 842d333).
 *
 * Audited every <button> in the app (client/index.html's ~12 static
 * tags, ~13 `createElement('button')` call sites in client/js/, plus
 * every per-item row they render) before writing this class. Result:
 * every one of them already carries its own class with EXPLICIT
 * background/border/color/width/height/cursor - the comments scattered
 * through config-editor-modal.css, config-editor.css, config-drawer.css,
 * home-bar.css, header-menu.css, server-status.css, session-sidebar.css,
 * toast.css and slash-command-chips.css record each one deliberately
 * escaping this rule. Exactly two elements in the whole app never
 * escaped it because they were never meant to: `#configEditorBtn` (the
 * one header control that stays inline at every width - see
 * client/js/header-menu.js) and `.header-menu-toggle` (the kebab
 * trigger it builds). Both get this class explicitly rather than
 * matching a bare element, and nothing else does. No fallback bare
 * `button {}` reset is added: since every other button already declares
 * every property this rule used to hand out for free, a reset would be
 * dead weight, not a safety net. */
.btn-icon {
    background: var(--color-accent-bg);
    color: var(--color-accent);
    border: 1px solid var(--color-accent-border);
    padding: 0;
    /* The third part of --header-h: these are the controls in .header,
     * so their size is the header's content height. */
    width: var(--control-size);
    height: var(--control-size);
    border-radius: var(--radius-full);
    cursor: pointer;
    font-size: 16px;
    transition: all 0.3s ease;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
}

.btn-icon:hover:not(:disabled) {
    background: var(--color-accent-bg-hover);
    border-color: var(--color-accent);
    box-shadow: 0 0 12px var(--color-accent-glow);
    transform: scale(1.05);
}

.btn-icon:disabled {
    background: color-mix(in srgb, var(--color-border) 10%, transparent);
    border-color: color-mix(in srgb, var(--color-border) 30%, transparent);
    color: var(--color-fg-subtle);
    cursor: not-allowed;
    opacity: 0.4;
}

.btn-icon.danger {
    color: var(--color-danger);
    border-color: var(--color-danger-border);
    background: var(--color-danger-bg);
}

.btn-icon.danger:hover:not(:disabled) {
    background: var(--color-danger-bg-hover);
    border-color: var(--color-danger);
    box-shadow: 0 0 12px var(--color-danger-glow);
}

/* NO CSS TOOLTIP LIVES HERE (issue #981, Adam (Adoom666), 2026-09-27: no
 * instant tooltips, one look, never two at once). These icon buttons' words
 * are their `title`, which the app's one tooltip
 * (web/src/lib/tooltip/tooltip.ts) shows after a hover or keyboard-focus
 * delay while keeping the browser's own away. A second, differently styled
 * `::after` bubble used to sit here beside the native one. Never add one
 * back: a surface that needs a tooltip sets `title` or `data-tooltip`. */

/* Status indicator */
.status {
    width: 12px;
    height: 12px;
    border-radius: var(--radius-full);
    background: var(--color-status-pending);
    display: inline-block;
    box-shadow: 0 0 4px var(--color-status-pending-glow);
    transition: all 0.3s ease;
    position: relative;
}

.status.connected {
    background: var(--color-status-connected);
    box-shadow: 0 0 6px var(--color-status-connected-glow);
    animation: pulse 2s infinite;
}

.status.error {
    background: var(--color-status-error);
    box-shadow: 0 0 6px var(--color-status-error-glow);
}

@keyframes pulse {
    0%, 100% { opacity: 1; }
    50% { opacity: 0.6; }
}

/* The status light has NO tooltip of its own any more (issue #981, the other
 * operator, 2026-09-27: no instant tooltips anywhere). It used to carry
 * an instant `::after` bubble reading `data-status`; its one remaining
 * home, the archive bar, prints that same text as a visible label beside
 * it (App._syncStatusLabel), and the span's `aria-label` names it. */

/* Terminal container.
 *
 * `min-height: 0`, never a pixel floor. A floor here is the same ratchet
 * as the one on `body`: it makes the container taller than the space it
 * was given, the overflow goes below the fold, and FitAddon then reads
 * the floor rather than the viewport and hands tmux too many rows. The
 * old 400px floor (350px under 768px, 300px under 480px) is exactly what
 * a phone in landscape or with the keyboard up cannot satisfy. */
.terminal-container {
    flex: 1;
    padding: 15px;
    overflow: hidden;
    display: flex;
    flex-direction: column;
    min-height: 0;

    /* THE PANE IS THE CONTAINING BLOCK FOR EVERY OVERLAY INSIDE IT.
     * ------------------------------------------------------------------
     * This element is a flex column, so an in-flow child of it steals
     * rows from `#terminal`; the ResizeObserver reads that as a real
     * geometry change, ships a pty_resize, tmux raises SIGWINCH and
     * claude answers with `ESC[2J`, which on the alternate screen erases
     * the conversation. So everything that appears over the terminal is
     * `position: absolute` instead - today that is the session search
     * panel and the prompt rail (web/src/lib/terminal-search/), and
     * `#localServersContainer` before them.
     *
     * WITHOUT THIS DECLARATION those overlays escape to the nearest
     * positioned ancestor, `#terminal-screen` (`.screen` is
     * `position: relative`). An absolutely positioned box resolves
     * `left`/`right` against its containing block's PADDING box, and
     * `body.session-sidebar-pinned .screen` puts
     * `padding-left: var(--sidebar-dock-w)` on exactly that element - so
     * `left: 15px` measured from the viewport edge, BEHIND the docked
     * sidebar, rather than from the terminal the overlay belongs to.
     * Measured in a real engine at 1357px with the sidebar docked: the
     * overlay's box was x=15 w=1327 against the pane's x=320 w=1037, so
     * 290px too wide and 305px into the sidebar column.
     *
     * SAFE BECAUSE IT WAS COUNTED. With a real xterm mounted there are
     * SEVEN absolutely positioned descendants here, and making this a
     * containing block moved exactly ONE of them - the overlay. The
     * other six are xterm's own, and `.xterm` is itself
     * `position: relative` (client/vendor/xterm/xterm.css), so they were
     * never resolving against anything above it.
     *
     * It is declared HERE, in the file that owns `.terminal-container`,
     * and deliberately nowhere else: two `position: relative`
     * declarations on one element is how one of them gets deleted as a
     * duplicate and the other turns out to be the one that mattered.
     * Guarded by tests/test_terminal_container_containing_block.node.mjs,
     * which resolves the real cascade rather than grepping for the text. */
    position: relative;

    /* Pull-to-refresh barrier. A drag on the terminal lands on
     * `canvas.xterm-link-layer`, which is NOT a descendant of
     * `.xterm-viewport` - the two are siblings under `.xterm`. So when
     * xterm's own touchmove handler declines the gesture (it calls
     * preventDefault only while the viewport can still move, and
     * deliberately releases it at either boundary), the browser walks
     * the ancestor chain of the CANVAS, not of the scroller. That chain
     * is `.xterm-screen` -> `.xterm` -> `#terminal` -> here, and every
     * link above was `overscroll-behavior: auto`, so a downward drag at
     * the top of the scrollback reached the document and iOS Safari
     * read it as pull-to-refresh.
     *
     * This is the NARROWEST element that can stop it: the first
     * ancestor of the touch target that is a scroll container at all
     * (`overflow: hidden` makes one). `contain`, not `none`, so the
     * terminal keeps its own local bounce. Nothing document-wide is
     * touched, so page-level gestures everywhere else are unchanged. */
    overscroll-behavior: contain;
}

#terminal {
    flex: 1;
    background: var(--color-bg);
    min-height: 0;
}

/* Hide scrollbars on any xterm host. `.xterm-host` is carried by BOTH the
 * main terminal's `#terminal` div and each opscenter viewer cell's stage
 * div (web/src/opscenter/cells/terminal-viewer/Cell.svelte), so a cell's
 * `.xterm-viewport` gets the same unthemed-scrollbar treatment the main
 * terminal gets instead of showing a raw white browser scrollbar. */
.xterm-host .xterm-viewport {
    overflow-y: scroll !important;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE/Edge */
}

.xterm-host .xterm-viewport::-webkit-scrollbar {
    display: none; /* Chrome/Safari */
    width: 0;
    height: 0;
}

/* Prevent line-height/letter-spacing from bleeding into xterm measurement */
.xterm, .xterm * {
    line-height: normal !important;
    letter-spacing: normal !important;
    font-kerning: normal !important;
}

/* Auth screen styles */
.screen {
    display: none;
    flex: 1;
    overflow: auto;
    /* A flex item's automatic minimum size is its CONTENT size, which
     * would let a tall child (the terminal) push the shell past one
     * viewport and re-open the ratchet described on `body`. */
    min-height: 0;
}

.screen.active {
    display: flex;
}

/* THE LOGIN SCREEN, Military Terminal look (#1176): the text-prompt modal's
 * recipe (text-prompt-modal.css) - a title-row strip with the wordmark and ONE
 * accent label, hairline field, `.modal-btn-confirm` button, every colour an
 * --oc-* token. Focus is the shared brightened edge (focus.css). */
#auth-screen {
    flex-direction: column;
    justify-content: center;
    align-items: center;
    padding: 20px;
    font-family: var(--font-mono);
}

.auth-container {
    max-width: 380px;
    width: 100%;
    border: 1px solid var(--oc-line-rule);
    border-radius: 2px;
    background: var(--oc-ground-panel);
    overflow: hidden;
}

.auth-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: var(--oc-titlerow-h, 37px);
    padding: 0 14px;
    background: var(--oc-ground-titlerow);
    box-shadow: inset 0 -1px 0 0 var(--oc-line-rule);
}

.auth-wordmark {
    height: 20px;
    width: auto;
}

.auth-prompt {
    font-size: var(--oc-fs-title, 12px);
    letter-spacing: var(--oc-tracking-title, 0.8px);
    text-transform: uppercase;
    color: var(--oc-orange);
}

.auth-body {
    padding: 16px 14px 14px;
}

.auth-description {
    color: var(--oc-text-muted);
    font-size: 12px;
    line-height: 1.6;
    margin-bottom: 16px;
}

.auth-input-group {
    margin-bottom: 16px;
}

.auth-input-label {
    display: block;
    margin-bottom: 6px;
    font-size: var(--oc-fs-micro, 10px);
    letter-spacing: var(--oc-tracking-label, 0.6px);
    text-transform: uppercase;
    color: var(--oc-text-muted);
}

.auth-input {
    width: 100%;
    padding: 7px 10px;
    background: var(--oc-ground-field);
    border: var(--oc-border, 1px) solid var(--oc-line-rule);
    border-radius: 2px;
    color: var(--oc-text-bright);
    font-family: var(--font-mono);
    font-size: 16px; /* 16px: iOS zooms a smaller focused field */
    letter-spacing: 0.3em;
    text-align: center;
    transition: border-color var(--oc-motion-fast, 120ms) ease;
}

.auth-input::placeholder {
    letter-spacing: normal;
}

.auth-container .auth-button {
    width: 100%;
    height: auto;
    padding: 7px 16px;
    font-size: 13px;
}

.auth-button:disabled {
    opacity: 0.4;
    cursor: not-allowed;
}

@media (pointer: coarse), (max-width: 600px) {
    .auth-container .auth-button {
        min-height: 44px;
    }
}

.auth-error,
.auth-info {
    margin-top: 16px;
    padding: 8px 10px;
    border: 1px solid var(--oc-line-rule);
    border-radius: 2px;
    font-size: 12px;
    line-height: 1.6;
}

.auth-error {
    border-color: var(--oc-coral);
    color: var(--oc-coral);
}

.auth-info {
    color: var(--oc-text-muted);
}

.auth-info code {
    color: var(--oc-orange);
}

/* Launchpad screen styles */
#launchpad-screen {
    flex-direction: column;
    padding: var(--screen-pad-x);
}

.launchpad-container {
    max-width: 800px;
    width: 100%;
    margin: 0 auto;
}

/* .new-session-btn and .reset-server-btn used to live here. Both are gone,
 * for the same reason and by two different routes: the "new session" button
 * left index.html at 344d82d when the launchpad was consolidated, and
 * "reset server" became the "restart server" row inside the bottom-bar
 * server-controls popup at cc248bf. Neither class is emitted by any
 * template or any JS any more, so the generic `button` height reset that
 * fix/audit-sweep wrote for them corrected a box that nothing renders.
 * Removed rather than merged - a rule for an absent element is furniture
 * that reads as coverage. */

.project-list {
    display: flex;
    flex-direction: column;
    gap: 10px;
}

.project-item {
    /* ITEM 41 - AN 80 PERCENT FILL, NOT AN 8 PERCENT ONE. This was
     * `var(--color-accent-bg-soft)`, an alpha 0.06 to 0.08 accent wash,
     * so effectively the whole card was the animated theme canvas at
     * z-index -1 with a hint of colour on top, and body text sat
     * directly on moving pixels. The fill is now 80 percent of the
     * theme's own card surface token, with the original accent wash
     * kept as a background-IMAGE layer on top so the card still reads
     * the same colour. Because the base is a theme token, a light theme
     * fills with its light page colour (codex #f7f7fa, legacy_windows
     * #C0C0C0) and a dark theme with its dark one - a single hardcoded
     * fill tuned for near-black would be wrong on both of those.
     * The base is --color-bg, the PAGE colour, deliberately not
     * --color-bg-elevated: over a still backdrop an 80 percent page
     * colour composites back to exactly the colour this card already
     * was, so the look is unchanged and only the transparency moved.
     * --color-bg-elevated was measured first and rejected: on dracula it
     * is #44475a against a #282a36 page, which lifted the card and
     * dropped the muted path line from 4.6:1 to 4.37:1. Every muted
     * token in every theme was chosen against --color-bg, so that is the
     * colour the fill has to land on.
     * background-color and background-image are declared separately on
     * purpose: any rule below that wants a different tint sets only the
     * IMAGE and keeps this base, and any rule that uses the `background`
     * shorthand would silently discard one of the two layers. */
    background-color: color-mix(in srgb, var(--color-bg, #1e1e1e) 80%, transparent);
    background-image: linear-gradient(var(--color-accent-bg-soft), var(--color-accent-bg-soft));
    /* ITEM 37 - ONE BORDER, ONE COLOUR. This box used to declare a 1px
     * ring in --color-border and then override the LEFT edge to 3px in
     * --color-accent. Two adjacent borders of different widths and
     * colours are joined by the painting algorithm with a diagonal
     * miter, and under `border-radius` that miter is drawn along the
     * corner arc - which is the smeared two-tone wedge at the top-left
     * and bottom-left corners, the "bleed on the left side of a
     * different color". It is not an overlay and not a theme bug; it is
     * what a mismatched border shorthand renders as. The ring is now
     * uniform, and the accent rail is an inset shadow instead: shadows
     * are clipped to the border radius rather than mitered into the
     * neighbouring edge, so the rail keeps its exact former width,
     * colour and position with no corner transition at all. Every rule
     * below that changes the rail colour changes THIS shadow, never a
     * border-left-color, and every rule that adds a glow must re-declare
     * the inset rail alongside it (box-shadow is one property, not a
     * list that accumulates). */
    border: 1px solid var(--color-border);
    box-shadow: inset 3px 0 0 var(--color-accent);
    /* Right padding clears the inline action buttons: pencil at right:8,
     * archive at right:40 - the hard-delete trash button that used to
     * sit at right:8 (pushing these two out to 40/72) is gone from the
     * UI, so 72px is the width actually needed, not 104px. */
    padding: 14px 72px 14px 16px;
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: all 0.3s ease;
    position: relative;
    overflow: hidden;
}

.project-item:hover {
    background-color: color-mix(in srgb, var(--color-bg-hover, #323235) 80%, transparent);
    border-color: var(--color-accent);
    box-shadow: inset 3px 0 0 var(--color-accent), 0 0 8px var(--color-accent-border-soft);
}

.project-name {
    color: var(--color-accent);
    font-size: 1rem;
    font-weight: bold;
    margin-bottom: 4px;
}

/* Edit + archive are ALWAYS rendered and ALWAYS visible (no `:hover`-only
 * reveal anywhere in this file) so they are reachable on touch devices
 * with no hover state at all - the user drives this from a phone. See
 * the `@media (pointer: coarse)` block below for the touch-specific
 * hit-area bump. A hard-delete trash button used to sit at right:8; it
 * is gone from the UI, and this rule's neighbours shifted in to fill
 * its slot (owner's instruction, 2026-09-08: "sessions and projects can
 * be archived not deleted"). */
[data-project-key='edit'] {
    /* The base IconButton (web/src/lib/base/icon-button.css) wears the look; this only places and sizes it. Width and height
     * are set here as well as the knob so the base 44px thumb rule below 1024 cannot overlap the two keys. */
    --oc-ibtn-size: 28px;
    --_btn-h: var(--oc-ibtn-size);
    position: absolute;
    top: 8px;
    right: 8px;
    width: var(--oc-ibtn-size);
    height: var(--oc-ibtn-size);
}

/* Archive / restore button - sits immediately left of the pencil, and is
 * the only destructive-shaped control on the row now that the hard-
 * delete trash button is gone (owner's instruction, 2026-09-08:
 * "sessions and projects can be archived not deleted"). Deliberately
 * calm: archiving is reversible and routine. */
[data-project-key='archive'] {
    /* Placed and sized as the pencil beside it (above). */
    --oc-ibtn-size: 28px;
    --_btn-h: var(--oc-ibtn-size);
    position: absolute;
    top: 8px;
    right: 40px;
    width: var(--oc-ibtn-size);
    height: var(--oc-ibtn-size);
}

.project-archived-badge {
    margin-top: 8px;
    text-transform: uppercase;
    padding: 3px 8px;
    border-radius: var(--radius-sm, 4px);
    font-size: 0.72rem;
    font-weight: bold;
    letter-spacing: 0.02em;
    display: inline-block;
    color: var(--color-fg-muted);
    background: var(--color-bg-hover, rgba(255, 255, 255, 0.06));
    border: 1px dashed var(--color-fg-muted);
}

/* Show-archived toggle in the projects section heading. */
.launchpad-archived-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-left: auto;
    padding: 2px 8px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--color-fg-muted);
    font-size: 0.72rem;
    font-family: inherit;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: all 0.2s ease;
}

.launchpad-archived-toggle:hover {
    color: var(--color-fg, #ddd);
    border-color: var(--color-fg-muted);
}

.launchpad-archived-toggle.is-on {
    color: var(--color-accent, #6cf);
    border-color: var(--color-accent, #6cf);
}

/* Touch hit-area bump. `pointer: coarse` covers phones/tablets - no
 * `:hover` involved, since a coarse pointer generally can't hover at
 * all. 44px is the standard minimum touch target (Apple HIG / WCAG
 * 2.5.5); `.project-item` gets matching right padding so the wider
 * buttons never overlap the description text. */
@media (pointer: coarse) {
    .project-item {
        padding-right: 92px;
    }

    [data-project-key] {
        --oc-ibtn-size: 40px;
    }

    [data-project-key='edit'] {
        right: 4px;
    }

    [data-project-key='archive'] {
        right: 48px;
    }
}

.launchpad-empty {
    color: var(--color-fg-muted);
    font-size: 0.9rem;
    font-style: italic;
    padding: 20px;
    text-align: center;
}

/* Adopt-external-session disclosure. Native <details>/<summary>.
 *
 * Sits at the TOP of the launchpad pane, under the launcher title and its
 * subtitle - see the markup comment in launchpad.js for why it moved out
 * of the running-sessions heading row.
 *
 * The marker used to be a bare "?" character in a bordered pill: a 21.7 x
 * 17 box of 12.75px text, the only glyph on the pane that was not an SVG,
 * and visibly heavier than its neighbours. It is now a 16 x 16 inline SVG
 * drawn from the same family as `.new-fab__icon` (viewBox "0 0 24 24",
 * stroke-width 1.8) and smaller than both the old pill and that 18px
 * family, which is what "it can be smaller" asked for.
 *
 * NO `stroke-width` HERE ON PURPOSE. The value is a presentation attribute
 * on the <svg> in the markup, inherited by the paths. A CSS rule targeting
 * `svg` loses to a presentation attribute on a child `path`, so declaring
 * it in both places is how a restyle ends up silently ignored - it has
 * happened twice in this codebase. One home for the value, and it is the
 * markup. */
.adopt-disclosure { display: block; margin: 0 0 22px; font-size: 0.85rem; font-weight: normal; }
.adopt-disclosure > summary {
    cursor: help;
    color: var(--color-fg-muted);
    list-style: none;
    /* inline-flex, not inline: the box must be exactly the icon so the
     * 16px figure is the whole visible affordance. */
    display: inline-flex;
    align-items: center;
    /* Anchor for the invisible touch target below. */
    position: relative;
    transition: color 160ms ease;
}
.adopt-disclosure > summary:hover,
.adopt-disclosure[open] > summary { color: var(--color-accent); }
/* 44px touch target WITHOUT a 44px layout box. The user asked for a
 * smaller icon; a finger still needs 44px. A pseudo-element takes no part
 * in layout, so the summary's own rect stays 16 x 16 while the tappable
 * area is 44 x 44 centred on it. The 22px of margin below the disclosure
 * is what keeps this overlay clear of the next section's heading. */
.adopt-disclosure > summary::after {
    content: '';
    position: absolute;
    inset: -14px;
}
.adopt-disclosure > summary:focus-visible { border-radius: var(--radius-sm); }
.adopt-disclosure__icon { display: block; }
.adopt-disclosure > summary::-webkit-details-marker { display: none; }
.adopt-disclosure-body { margin-top: 10px; padding: 12px 14px; background: var(--color-bg-elevated); border: 1px solid var(--color-border); border-left: 3px solid var(--color-accent); border-radius: var(--radius-md); color: var(--color-fg); font-size: 0.85rem; line-height: 1.5; font-weight: normal; }
.adopt-disclosure-body p + p { margin-top: 10px; }
.adopt-disclosure-body code { background: var(--color-bg-overlay-faint); padding: 2px 6px; border-radius: var(--radius-sm); color: var(--color-accent); }
/* README link - was a bare <a> with NO rule targeting it at all, so every
 * theme showed the browser's default blue (link) / purple (visited)
 * instead of the theme's own accent. Underlined at rest (not just on
 * hover) so it stays distinguishable from the surrounding body text by
 * more than color alone; focus-visible gets the same outline treatment
 * as the disclosure's own summary above. */
.adopt-disclosure-body a {
    color: var(--color-accent);
    text-decoration: underline;
    text-underline-offset: 2px;
}
.adopt-disclosure-body a:visited { color: var(--color-accent-strong); }
.adopt-disclosure-body a:hover { color: var(--color-accent-strong); }
.adopt-disclosure-body a:focus-visible {
    border-radius: var(--radius-sm);
}

/* THE LAUNCHPAD FOOTER IS GONE. It was a centred block at the end of the
 * scrolled home-screen content holding one link, so you only ever saw it
 * if you scrolled to the bottom. It is now a real bottom bar pinned to the
 * home screen: client/css/home-bar.css. Do not reintroduce a
 * `.launchpad-footer` rule here - the bar is a different box in a
 * different place, not a restyle of this one. */

/* Terminal screen.
 *
 * `position: relative` was the containing block for the tool strip that
 * used to overlay the terminal's top-right corner. That strip is gone
 * (see terminal-tools.css); this stays because the terminal screen is
 * still a positioning context for anything mounted inside it.
 *
 * `overflow: hidden` overrides `.screen`'s `auto`. The terminal screen
 * must never scroll in either axis: xterm owns vertical scrolling inside
 * `.xterm-viewport`, and horizontal scroll here is always a bug - it
 * means something escaped the viewport width. Belt and braces alongside
 * the tooltip containing-block fix above. */
#terminal-screen {
    flex-direction: column;
    position: relative;
    overflow: hidden;
    min-height: 0;
}

/* Mobile responsive */
@media (max-width: 768px) {
    .header h1 {
        font-size: 1.1rem;
    }

    .btn-icon {
        font-size: 18px;
    }

    .status {
        width: 14px;
        height: 14px;
    }

    .controls {
        gap: 6px;
    }

    .terminal-container {
        padding: 10px;
    }

    .launchpad-container {
        padding: 0 10px;
    }

    .project-item {
        /* Left/top/bottom only - right stays whatever the base rule (or
         * the pointer:coarse touch bump above) set, so edit/delete never
         * get clipped under the description text on a narrow phone. */
        padding-top: 12px;
        padding-bottom: 12px;
        padding-left: 14px;
    }
}

@media (max-width: 480px) {
    .header h1 {
        font-size: 1rem;
    }

    .terminal-container {
        padding: 8px;
    }

    .home-subheader {
        font-size: 0.72rem;
    }
}

/* NO D-PAD STYLES HERE ANY MORE, AND NO `.slash-commands-btn` BLOCK
 * BELOW EITHER.
 *
 * Two round `position: fixed` buttons used to sit in the terminal's
 * bottom row, one opening a full-screen arrow-key overlay and one
 * opening the slash command modal. Both painted over live terminal
 * output, which is the habit this page has been removing control by
 * control. The owner replaced them, and the arrow overlay with them,
 * with ONE in-flow row of keys under the pane:
 * client/js/terminal-key-bar.js and client/css/terminal-key-bar.css.
 *
 * The rules are DELETED rather than left unreferenced. A block of
 * styling for markup nothing emits is how a retired control gets revived
 * by accident, the same call already made for the top-right rail and its
 * `--fab-top-edge` token. `@keyframes slideInFromBottomRight` went with
 * the overlay that was its only user; `fadeIn`, which it also used, is
 * shared and stays.
 *
 * The `--fab-*` tokens are gone from the token block above for the same
 * reason: every floating button that read them is deleted. */

/* Utility classes */
.hidden {
    display: none !important;
}

/* ===========================================================================
 * THE SHARED MODAL CHASSIS, IN MILITARY TERMINAL STYLE.
 *
 * WHAT READS THIS BLOCK. Twenty two dialogs across BOTH front-end trees,
 * because `web/src/lib/dialog-frame/ModalShell.svelte` carries no <style> of
 * its own and borrows `.modal-overlay`, `.modal-content`, `.modal-header`,
 * `.modal-body` and `.modal-footer` by name. Thirteen vanilla dialogs and
 * nine compiled ones change together whenever anything below changes.
 *
 * WHY THE COLOURS COME FROM --oc-*, NOT FROM --color-*. A dialog is app
 * chrome, not session content. `client/css/terminal-carnivore.css` makes
 * the header, the sidebar and the terminal page read military terminal
 * WHATEVER session theme is on, by rebinding --color-* inside a closed
 * list of chrome roots; every `.modal-overlay` is appended to <body> and
 * therefore sits outside that list. Pointing this block straight at the
 * --oc-* palette (defined on :root by client/dist/app.css, which
 * client/index.html loads on every screen) is what puts the dialogs in the
 * same chrome as everything else. The consequence is deliberate: a dialog
 * no longer changes colour with the user's theme, the way the header
 * already does not.
 *
 * WHY THE PADDING IS 31px AND NOT A ROUND NUMBER. The border was
 * `2px solid var(--color-accent)` - a thick coral frame, the single most
 * un-military thing in the app - and is now a one pixel hairline. Every
 * box in this file is `box-sizing: border-box` (the `*` reset above), so
 * the panel's outer WIDTH is fixed by `width`/`max-width` and cannot move.
 * Its outer HEIGHT is `border + padding + content + padding + border`, so
 * losing 1px of border on each edge would shrink the panel by 2px and
 * widen its content box by 2px, which rewraps text and moves the height
 * again. Adding the pixel back to the padding holds BOTH the outer rect
 * and the content box exactly where they were: 2+30 = 1+31 on every edge.
 * Do not "tidy" 31px back to 30px without re-measuring the dialogs.
 * ======================================================================== */
.modal-overlay {
    position: fixed;
    inset: 0;
    /* A dialog taller than the screen scrolls here; .modal-content's auto margin keeps its top reachable. */
    overflow-y: auto;
    background: var(--oc-backdrop);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 9999;
    animation: backdropIn 0.2s ease;
}

/* A modal overlay fades only its BACKDROP in: fading the overlay's opacity would fade the dialog with it and let the page show through. */
@keyframes backdropIn {
    from { background-color: transparent; }
}

@keyframes fadeIn {
    from { opacity: 0; }
    to { opacity: 1; }
}

.modal-content {
    background: var(--oc-ground-raised);
    /* A hairline, not a frame. See the padding note in the block comment
     * above before changing either of these two numbers. */
    border: var(--oc-border) solid var(--oc-line-panel);
    border-radius: var(--oc-radius-sm);
    padding: 31px;
    max-width: 500px;
    width: 90%;
    margin: auto;
    animation: slideIn 0.3s ease;
}

@keyframes slideIn {
    from {
        transform: translateY(-50px);
        opacity: 0;
    }
    to {
        transform: translateY(0);
        opacity: 1;
    }
}

/* THE TITLE IS NOT THE ACCENT. In this palette the orange marks the one
 * thing in a surface that matters, which in a dialog is the button that
 * commits, not the sentence at the top. */
.modal-header {
    color: var(--oc-text-primary);
    font-size: 1.3rem;
    margin-bottom: 20px;
    font-weight: bold;
}

.modal-body {
    margin-bottom: 25px;
}

.modal-input-group {
    margin-bottom: 20px;
}

.modal-label {
    color: var(--oc-text-secondary);
    font-size: 0.9rem;
    margin-bottom: 8px;
    display: block;
}

.modal-input {
    width: 100%;
    padding: 12px 16px;
    background: var(--oc-ground-field);
    border: var(--oc-border) solid var(--oc-line-faint);
    color: var(--oc-text-primary);
    /* NOT --oc-font. That family names JetBrains Mono first, which this
     * tree does not load, and a family swap changes the glyph advance,
     * which is geometry. --font-mono is already the platform monospace. */
    font-family: var(--font-mono);
    font-size: 1rem;
    border-radius: var(--oc-radius-sm);
    transition: all var(--oc-motion) ease;
}

/* A CRISP RING, NOT A GLOW. The focused field used to bloom an 8px accent
 * blur; a 1px ring outside the hairline says the same thing in this
 * palette's vocabulary and, being a box-shadow, still costs no layout. */
.modal-input:focus {
    outline: none;
    border-color: var(--oc-orange);
    box-shadow: 0 0 0 1px var(--oc-orange);
}

.modal-input::placeholder {
    color: var(--oc-text-dim);
}

.modal-description {
    color: var(--oc-text-muted);
    font-size: 0.85rem;
    margin-top: 6px;
}

.modal-message {
    color: var(--oc-text-primary);
    font-size: 1rem;
    line-height: 1.5;
    margin-bottom: 10px;
}

.modal-footer {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
}

/* THE ONE BUTTON RECIPE lives in web/src/lib/base/elements.css (slice 3): `.modal-btn` and its variants
 * (-secondary, -primary, -confirm, -danger, -alert) wear it there, sizes included. */
/* Slash Commands Modal
 *
 * MILITARY TERMINAL, LIKE EVERY OTHER OVERLAY (issue #405). Every colour
 * below is an --oc-* palette token and the panel wears the shared
 * `popup-focus` look (client/css/popup-focus.css, which owns its border,
 * ground and shadow and is added by client/js/slash-commands.js), the
 * same call b28b9236 made for the other surfaces over the terminal. No
 * --color-* name is read here: those follow the SESSION theme, and this
 * popup is app chrome. The orange marks one thing per surface: the hover,
 * the keyboard-active row and the filter's focus, never the frame. */
.modal {
    display: none !important;
}

.modal.active {
    display: flex !important;
}

.modal .modal-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: var(--oc-backdrop);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 10000;
    animation: backdropIn 0.2s ease;
}

.slash-commands-modal-content {
    border-radius: var(--oc-radius-sm);
    width: 90%;
    max-width: 700px;
    max-height: 85vh;
    max-height: 85dvh;
    display: flex;
    flex-direction: column;
    animation: slideUp 0.3s ease;
    position: relative;
}

.slash-commands-modal-content .modal-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 20px 24px;
    border-bottom: var(--oc-border) solid var(--oc-line-rule);
}

/* THE TITLE IS NOT THE ACCENT, the same rule the modal chassis carries. */
.slash-commands-modal-content .modal-header h2 {
    color: var(--oc-text-primary);
    font-size: 1.3rem;
    margin: 0;
    font-weight: bold;
}

.modal-close {
    background: transparent;
    border: none;
    color: var(--color-accent);
    font-size: 2rem;
    line-height: 1;
    cursor: pointer;
    padding: 0;
    width: 32px;
    height: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s ease;
}

.modal-close:hover {
    transform: scale(1.1);
    color: var(--color-accent-strong);
}

.slash-commands-modal-content .modal-close { color: var(--oc-text-secondary); }
.slash-commands-modal-content .modal-close:hover { transform: none; color: var(--oc-orange); }

.slash-commands-modal-content .modal-body {
    padding: 24px;
    overflow-y: auto;
    flex: 1;
}

.common-commands-section {
    margin-bottom: 30px;
}

.all-commands-section {
    border-top: var(--oc-border) solid var(--oc-line-rule);
    padding-top: 20px;
}

.all-commands-section h3,
.command-category h4 {
    color: var(--oc-text-muted);
    font-size: var(--oc-fs-label);
    letter-spacing: var(--oc-tracking-label);
    text-transform: uppercase;
    margin: 0 0 12px 0;
    font-weight: normal;
}

.all-commands-list {
    display: flex;
    flex-direction: column;
    gap: 16px;
}

.command-item {
    background: var(--oc-ground-card);
    border: var(--oc-border) solid var(--oc-line-faint);
    padding: 12px 16px;
    border-radius: var(--oc-radius-sm);
    cursor: pointer;
    transition: background var(--oc-motion) ease, border-color var(--oc-motion) ease;
    display: flex;
    flex-direction: column;
    gap: 4px;
}

.command-item:hover {
    background: var(--oc-ground-hover);
    border-color: var(--oc-orange);
}

/* overflow-wrap is LOAD-BEARING. Command descriptions come from scraped
 * docs and user-authored skill frontmatter, and some contain a single
 * unbreakable token - a filesystem path, a URL, a hostname. With the
 * default `overflow-wrap: normal` such a token never breaks: measured at
 * 375px, `~/Development/.../config/servers.yaml.` rendered 491.3px inside a
 * 234.3px line box, which pushed `.modal-body` (overflow-x: auto) to
 * scrollWidth 520 against clientWidth 292. That 228px of slack is what made
 * the whole list slide left and right while scrolling vertically. Breaking
 * inside the token keeps every row inside the container. */
.command-name,
.command-description {
    min-width: 0;
    overflow-wrap: anywhere;
    word-break: break-word;
}

.command-name {
    color: var(--oc-text-bright);
    font-family: var(--font-mono);
    font-size: 0.95rem;
    font-weight: bold;
}

.command-description {
    color: var(--oc-text-muted);
    font-size: 0.85rem;
    line-height: 1.4;
}

/* A flex item defaults to min-width: auto (its min-content width), so a
 * wide child would still be able to push these containers past the
 * viewport even after the wrapping fix above. Belt and braces: the list
 * can never be wider than the modal body that scrolls it. */
.all-commands-list,
.command-category,
.command-item {
    min-width: 0;
    max-width: 100%;
}

/* The "more"/"less" control that reveals a truncated description in place.
 * A phone has no hover, so the full text has to be reachable by a tap;
 * a title attribute would be a control the user can never see.
 *
 * SQUARE, not a pill. It was `--radius-pill` (50px), which on a 17px-tall
 * control is a full oval - the same shape defect the settings tab strip
 * was fixed for. It also now shares a row with the favorites star, and
 * two controls on one row should not be told apart by their corner
 * radius: `more` is a WORD in normal flow at the bottom left, the star is
 * an ICON pinned to the top right. */
.command-more {
    width: auto;
    height: auto;
    align-self: flex-start;
    margin-top: 2px;
    padding: 2px 8px;
    background: transparent;
    border: var(--oc-border) solid var(--oc-line-panel);
    border-radius: 0;
    color: var(--oc-text-dim);
    font-family: var(--font-mono);
    font-size: 0.7rem;
    cursor: pointer;
}

.command-more:hover,
.command-more:focus-visible {
    color: var(--oc-orange);
    border-color: var(--oc-orange);
    outline: none;
}

/* Live-filter input (Task 4) - sits above the all-commands list, inside
   the modal that's already open. Does not affect open/close behavior. */
.command-filter-input {
    width: 100%;
    box-sizing: border-box;
    background: var(--oc-ground-field);
    border: var(--oc-border) solid var(--oc-line-panel);
    color: var(--oc-text-primary);
    padding: 10px 14px;
    border-radius: var(--oc-radius-sm);
    font-family: var(--font-mono);
    font-size: 0.9rem;
    margin-bottom: 16px;
}

/* A CRISP RING, NOT A GLOW, as `.modal-input:focus` above. */
.command-filter-input:focus {
    outline: none;
    border-color: var(--oc-orange);
    box-shadow: 0 0 0 1px var(--oc-orange);
}

/* Keyboard-selected row while filtering - mirrors the :hover treatment
   so arrow-key navigation looks the same as a mouse hover. */
.command-item.keyboard-active {
    background: var(--oc-ground-hover);
    border-color: var(--oc-orange);
}

.command-category.filter-hidden,
.command-item.filter-hidden {
    display: none;
}

/* Folder picker modal */
.folder-picker-modal {
    max-width: 640px;
}

/* Editable path / address bar (folder-picker-modal.js) AND the
   read-only path display reused for project modals (launchpad.js). The
   input carries `.modal-input` alongside this class, which already
   supplies identical :focus and ::placeholder treatment, so this rule
   only needs the wrap fix - a long iCloud path has no spaces for the
   browser to break on, so without word-break it ran past the edge of
   the box instead of wrapping. Mirrors .folder-picker-status below.

   IT READS THE DIALOG'S TOKENS, NOT THE SESSION THEME'S. The dialog frame
   around it stopped following the theme in the re-skin, and these four
   declarations did not, so under a light theme the bar rendered white
   (calming) or silver (legacy_windows) inside a graphite dialog while the
   `.modal-input` beside it stayed dark. Deleting them instead of
   repointing them is not the fix: three of the four usages are read-only
   `<div>`s (ProjectFolderModal, ProjectNameModal, EditProjectModal) that
   do NOT carry `.modal-input`, so they would fall back to the browser
   default and read worse. The values below are `.modal-input`'s. */
.folder-picker-path {
    width: 100%;
    background: var(--oc-ground-field);
    border: var(--oc-border) solid var(--oc-line-faint);
    border-radius: var(--oc-radius-sm);
    color: var(--oc-text-primary);
    font-family: var(--font-mono);
    font-size: 0.85rem;
    padding: 10px 12px;
    margin-bottom: 10px;
    cursor: text;
    overflow-wrap: break-word;
    word-break: break-all;
    transition: border-color 0.2s ease, box-shadow 0.2s ease;
}

/* Inline feedback under the path bar (mkdir success / browse errors). */
.folder-picker-status {
    font-family: var(--font-mono);
    font-size: 0.8rem;
    margin-bottom: 10px;
    word-break: break-all;
    color: var(--oc-text-secondary);
}

.folder-picker-status:empty {
    display: none;
}

.folder-picker-status--success {
    color: var(--color-success);
}

.folder-picker-status--error {
    color: var(--color-danger);
}

.folder-picker-toolbar {
    display: flex;
    gap: 8px;
    margin-bottom: 12px;
}

.folder-picker-list {
    background: var(--oc-ground-field);
    border: var(--oc-border) solid var(--oc-line-faint);
    border-radius: var(--oc-radius-sm);
    max-height: 320px;
    overflow-y: auto;
    margin-bottom: 12px;
}

.folder-picker-item {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 14px;
    color: var(--oc-text-primary);
    font-family: var(--font-mono);
    font-size: 0.9rem;
    cursor: pointer;
    border-bottom: var(--oc-border) solid var(--oc-line-faint);
    transition: background 0.15s ease;
}

.folder-picker-item:last-child {
    border-bottom: none;
}

.folder-picker-item:hover {
    background: var(--oc-ground-hover);
    color: var(--oc-orange);
}

/* Keyboard / type-ahead selection - stronger than hover, with an inset
   accent bar so the active row reads clearly during arrow + type nav. */
.folder-picker-item-active {
    background: var(--oc-orange-tint);
    color: var(--oc-orange);
    box-shadow: inset 3px 0 0 var(--oc-orange);
}

.folder-picker-icon {
    font-size: 1rem;
    flex-shrink: 0;
}

/* No `min-width: 0` here on purpose. A flex item's automatic minimum size
 * is its content size ONLY while its main-axis overflow is `visible`; the
 * `overflow: hidden` below already zeroes it, so this shrinks and
 * ellipsizes correctly as written. Measured at 390px with a 460px name:
 * item 230px wide, list scrollWidth == clientWidth == 278. Same for
 * .provider-item-name below. */
.folder-picker-name {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.folder-picker-empty {
    padding: 24px;
    text-align: center;
    color: var(--oc-text-secondary);
    font-size: 0.9rem;
    font-style: italic;
}

/* Provider selector modal - reuses .folder-picker-list/-item/-status above.
   provider-item-name gets flex:1 so the remove "x" (model rows only) and
   the "+ add model" affordance line up flush right. */
.provider-item-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Mobile adjustments */
@media (max-width: 768px) {
    .slash-commands-modal-content {
        width: 95%;
        max-height: 90vh;
        max-height: 90dvh;
    }

    .slash-commands-modal-content .modal-body {
        padding: 16px;
    }

    .all-commands-section {
        padding-top: 16px;
    }

    .command-item {
        padding: 10px 12px;
    }

    .folder-picker-list {
        max-height: 240px;
    }
}

.running-sessions-attention__head {
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    font-weight: 700;
    color: var(--color-warning, #d79921);
    /* CASING IS THE STYLESHEET'S JOB. The markup used to carry the
     * literal words NEEDS ATTENTION in capitals; slice 5 moved them into
     * the string catalog, whose voice rule is lowercase, so the shout
     * lives here instead. Same precedent as `.badge`, which slice 4
     * moved for the same reason. A translation that wants a different
     * casing rule changes this line, not the message. */
    text-transform: uppercase;
}

.running-session-row {
    display: flex;
    flex-direction: column;
    padding: 10px 12px;
    margin-bottom: 6px;
    /* ITEM 41, same two-layer fill as .project-item above: 80 percent of
     * the theme's card surface, with the original accent wash kept as an
     * image layer so the row's colour is unchanged. */
    background-color: color-mix(in srgb, var(--color-bg, #1e1e1e) 80%, transparent);
    background-image: linear-gradient(var(--color-accent-bg-soft), var(--color-accent-bg-soft));
    /* ONE BORDER, ONE COLOUR, FOUR SIDES - the same ring .project-item
     * above already draws. This row used to have NO border at all except
     * a 3px left edge in the session-TYPE colour (see the removed
     * ownership-stripe block below), so three sides were bare and the
     * fourth was a coloured bar. The border is now a theme token on all
     * four edges, which is what "just the border and background based
     * upon the theme" asks for: nothing on this box encodes what KIND of
     * session it is any more, and the TMUX / EXTERNAL badge in the row
     * is the single source of truth for that. */
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    cursor: pointer;
    transition: background 120ms ease;
}

/* Hover changes the TINT layer only. Written as a `background` shorthand
 * it would reset background-color too and drop the row back to a 14
 * percent fill on hover, which is exactly the legibility problem item 41
 * is about - and it would only show up while the pointer was down the
 * list. */
.running-session-row:hover {
    background-image: linear-gradient(color-mix(in srgb, var(--color-accent) 14%, transparent), color-mix(in srgb, var(--color-accent) 14%, transparent));
}

/* THE OWNERSHIP STRIPE IS GONE, AND `.owned` / `.external` NOW PAINT
 * NOTHING. Deliberately no rule here, so this comment is the record of
 * why the selectors still exist in launchpad.js and carry no colour.
 *
 * It was a 3px `border-left` in --color-badge-tmux-fg /
 * --color-badge-external-fg: the SAME tokens as the TMUX / EXTERNAL
 * badge pill in the same row, chosen so the bar and the badge could
 * never disagree. Keeping them in step is the problem, not the fix. Two
 * places encoding one fact is two sources of truth, and the user hit the
 * failure mode directly - a card whose left bar was the wrong colour for
 * its badge. The badge is the cue that says the word out loud and is
 * never colour-only, so it wins; the bar was a colour-only restatement
 * of it and is the one that goes.
 *
 * What replaces it is nothing, on purpose. `.running-session-row` above
 * now carries a uniform 1px --color-border ring, so the left edge is the
 * same theme colour as the other three rather than a bare edge where a
 * coloured bar used to be. Nothing on the row's box encodes session type
 * any more.
 *
 * The `.owned` / `.external` classes are still written by
 * launchpad.js:976 and still select correctly; they are simply not a
 * paint hook. Do not reintroduce a colour here without also deciding
 * which of the two signals is authoritative. */

.running-session-top {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-bottom: 4px;
}

/* .running-session-dot replaced by the shared .status-dot classes
 * (client/css/session-sidebar.css) which color by REAL activity status
 * (running/idle/dead/unknown) instead of by ownership. */

.running-session-name {
    flex: 1;
    /* Unlike .folder-picker-name above this had NO overflow at all, so
     * its automatic minimum size really was its content size and the row
     * could not shrink. Measured at 390px with an all-underscore tmux
     * name: the name laid out 501px wide in a 280px row, and the kill
     * control's right edge landed at 573px - 293px off-screen, reachable
     * only by scrolling #launchpad-screen sideways. `overflow: hidden`
     * is what actually zeroes the automatic minimum; min-width: 0 states
     * the same intent explicitly so a later overflow change cannot
     * silently bring the bug back.
     *
     * No `white-space: nowrap`: ordinary multi-word names should still
     * wrap. This bounds the unbreakable-token case only. */
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-weight: bold;
    color: var(--color-accent);
}

/* The row's keys (X close on a running row, trash remove on a stopped one, restart on a dead one) are the base IconButton
 * (web/src/lib/launchpad/RunningSessionRow.svelte): close and remove are danger, restart quiet. This only sizes them. */
.running-session-row [data-session-action] {
    --oc-ibtn-size: 28px;
}

/* The RESTART control on a dead row - the one action in this family that
 * destroys nothing, so it must not wear the destructive palette.
 *
 * It sits directly beside the trash on the same row, and that is the only
 * place in the app where a harmless control and a permanent one are
 * adjacent. Color is the fastest thing the eye resolves, so painting
 * restart in the same danger red would make the safe button look exactly
 * as consequential as the one that deletes an uploads bucket - and, worse,
 * would make the trash beside it stop reading as a warning at all. Shape
 * carries the meaning (a circular arrow, not a play triangle - see
 * restartIconSvg); color carries only the stakes.
 *
 * Matched on the shared BASE_CLASS plus the modifier rather than on the
 * host's own class. The launcher once shared it; its restart key is now
 * the base IconButton's quiet variant (RunningSessionRow.svelte), so this
 * serves the sidebar (.session-sidebar-row-delete, session-sidebar.css),
 * which is at its 500-line budget. The doubled class is what makes the selector (0,3,0),
 * which is what lets it beat the sidebar's own (0,2,0) danger hover from
 * an EARLIER stylesheet - equal specificity would have lost on source
 * order and silently painted the safe button red in the sidebar only. */
.session-row-action.session-row-action-restart {
    color: var(--color-fg-faint);
}

.session-row-action.session-row-action-restart:hover,
.session-row-action.session-row-action-restart:focus-visible {
    color: var(--color-accent);
    background: var(--color-bg-hover);
    border-color: var(--color-accent);
    outline: none;
}

/* Non-blocking error banners, replacing a native alert().
   Fixed and stacked at the top so they are unmissable without halting
   the page - the whole point of the change is that an error message must
   not stop the program it is describing. Styled explicitly: an unclassed
   div here would render as bare serif text over the app. */
#launchpad-error-stack {
    position: fixed;
    top: 12px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 9999;
    display: flex;
    flex-direction: column;
    gap: 8px;
    max-width: min(640px, calc(100vw - 32px));
    pointer-events: none;
}

.launchpad-error-card {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding: 10px 12px;
    background: var(--color-bg-elevated, #1a1a1a);
    border: 1px solid var(--color-danger-strong, #e64);
    border-left-width: 3px;
    border-radius: var(--radius-md);
    color: var(--color-fg);
    font: inherit;
    font-size: 12px;
    line-height: 1.45;
    box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);
}

.launchpad-error-text {
    flex: 1;
    min-width: 0;
    /* A server detail can be long and can carry a path. Wrap it rather
       than truncating: an error you cannot read whole is not actionable. */
    overflow-wrap: anywhere;
}

.launchpad-error-dismiss {
    flex-shrink: 0;
    background: transparent;
    border: none;
    color: var(--color-fg-muted);
    cursor: pointer;
    font: inherit;
    font-size: 14px;
    line-height: 1;
    padding: 0 2px;
}

.launchpad-error-dismiss:hover { color: var(--color-fg); }
.badge {
    font-size: 0.7em;
    padding: 2px 6px;
    border-radius: var(--radius-sm);
    letter-spacing: 0.5px;
    /* CASING IS THE STYLESHEET'S JOB. The markup used to carry the
     * literal words TMUX, EXTERNAL and ENDED in capitals; slice 4
     * moved them into the string catalog, whose voice rule is
     * lowercase, so the shout lives here instead. Same precedent as
     * `.recent-session-lifecycle`. Pixels are unchanged. */
    text-transform: uppercase;
    /* THE TRANSPARENT BORDER IS LOAD-BEARING, and its absence was
     * visible. `.family-pill` below carries `border: 1px solid
     * transparent` so a coloured variant can fill it without shifting
     * layout. `.badge` did not, so two pills with identical font and
     * padding rendered 16px and 18px tall - measured side by side in one
     * centre-aligned flex strip, which is exactly where a 2px
     * disagreement reads as bad spacing.
     *
     * box-sizing: border-box does not save this: it only stops padding
     * and border eating into a SET height, and neither of these sets
     * one, so both are content-sized and the border simply adds 2px. */
    border: 1px solid transparent;
}

.badge-tmux     { background: var(--color-badge-tmux-bg);     color: var(--color-badge-tmux-fg); }
.badge-external { background: var(--color-badge-external-bg); color: var(--color-badge-external-fg); }
/* A BADGE, not a word. Same box as .badge / .family-pill - including the
 * 1px transparent border, so it sits at the identical height beside them
 * (see .badge for why that border is load-bearing). */
.recent-session-lifecycle {
    flex: 0 0 auto;
    font-size: 0.7em;
    padding: 2px 6px;
    border: 1px solid transparent;
    border-radius: var(--radius-sm);
    letter-spacing: 0.5px;
    text-transform: uppercase;
    color: var(--color-fg-faint);
    background: var(--color-accent-bg-soft);
}

@keyframes pulse-glow {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.55; }
}

/* =====================================================================
 * New-action speed-dial FAB
 * ---------------------------------------------------------------------
 * Inline "+" sitting at the right end of the "running sessions" section
 * heading row (NOT floating in the chrome, and NOT the "recent projects"
 * heading - both of those descriptions are stale). Tap to fan out 6
 * sub-action buttons in a vertical column. All colors flow through
 * theme tokens so every theme retints automatically. All animations
 * use transform + opacity ONLY (GPU compositor) - no layout thrash.
 * Reduced-motion gated at the bottom of the block.
 * ===================================================================== */
/* ---- Inline FAB wrapper (lives inside the section heading row) ----- */
/* The wrapper is the absolute-positioning context for the fan-out menu.
 * It sits as a flex child of .launchpad-actions - a row that is ALWAYS
 * present - so .hidden isn't needed (it shows/hides with the launchpad
 * screen and nothing else).
 *
 * It used to be a child of the "running sessions" section title instead,
 * and that section is display:none while the user has no sessions. The
 * comment here previously read "shows/hides naturally with the launchpad
 * screen", which was the assumption that hid it: it also hid with the
 * SECTION, so a fresh install had no create control at all. */
.new-fab {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: flex-end;
    pointer-events: auto;
    /* Default z stays low so neighboring rows can paint normally. */
    z-index: 2;
}

/* When open, lift ABOVE the backdrop (1050) so item clicks land on the
 * pills, not the backdrop. Without this, the backdrop intercepts every
 * click and silently closes the menu - which presents to the user as
 * "the buttons don't do anything." */
.new-fab--open { z-index: 1100; }

.new-fab.hidden { display: none; }

/* ---- Trigger ("+" / "×") - ghost style ----------------------------- */
.new-fab__trigger {
    /* Outline/ghost: transparent fill, visible border + readable glyph at rest.
     * Border + glyph use --color-fg-muted (always contrasts against bg because
     * regular text uses it), so the trigger reads on every theme without
     * needing a solid accent fill. Accent retint kicks in on :hover /
     * :focus-visible / open only. */
    width: 36px;
    height: 36px;
    padding: 0;
    border-radius: var(--radius-full);
    background: transparent;
    color: var(--color-fg-muted);
    border: 2px solid var(--color-fg-muted);
    box-shadow: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    opacity: 0.85;
    transition: color 160ms ease,
                border-color 160ms ease,
                background 160ms ease,
                opacity 160ms ease,
                transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
    will-change: transform;
}

.new-fab__trigger:hover:not(:disabled) {
    opacity: 1;
}

.new-fab__trigger:hover:not(:disabled) {
    color: var(--color-accent);
    border-color: var(--color-accent);
    background: var(--color-accent-bg-soft, transparent);
    transform: scale(1.04);
}

.new-fab__trigger:active:not(:disabled) {
    transform: scale(0.94);
    background: var(--color-accent-bg-soft, transparent);
}

.new-fab__trigger:focus-visible {
    /* Keep ghost discipline on focus - no solid fill */
    background: transparent;
}

/* When OPEN: × glyph + border in accent color (still ghost - no fill) */
.new-fab--open .new-fab__trigger {
    color: var(--color-accent);
    border-color: var(--color-accent);
    background: var(--color-accent-bg-soft, transparent);
}

.new-fab__plus {
    transition: transform 250ms cubic-bezier(0.34, 1.56, 0.64, 1);
    transform-origin: 50% 50%;
    will-change: transform;
}

/* When open: rotate the + into an × */
.new-fab--open .new-fab__plus {
    transform: rotate(45deg);
}

/* ---- Menu (vertical fan-out column) -------------------------------- */
/* FIXED, NOT ABSOLUTE, AND WHY. This used to be `position: absolute;
 * top: 44px; right: 0`, which pinned the column BELOW the trigger
 * unconditionally and left it a descendant of `.launchpad-scroll`
 * (client/css/home-bar.css) - a real `overflow-y: auto` box. So whenever
 * the heading sat low in the viewport the ~318px column was CLIPPED by
 * the scroller instead of flipping above the trigger. The home bar did
 * not cause that, it only moved the clip point up by the bar's height
 * (36px, 44px on a coarse pointer) and made it easy to hit.
 *
 * `position: fixed` takes the menu out of the scroller's clip entirely
 * (no ancestor here establishes a containing block via transform /
 * filter / will-change, so fixed really is viewport-relative), and
 * client/js/anchor-popover.js supplies left/top: above the anchor by
 * default, below only when there is no room above, clamped into the
 * VISUAL viewport either way. Same primitive the two terminal FAB menus
 * use via client/js/fab-menu.js.
 *
 * left/top here are only the pre-measurement values. Launchpad.openNewFab()
 * overwrites both before the menu becomes visible. Do NOT put `right: 0`
 * back: AnchorPopover writes `left`, and a competing `right` would
 * stretch the box and destroy the shared right edge below. */
.new-fab__menu {
    position: fixed;
    top: 0;
    left: 0;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 10px;
    padding-top: 4px;
    pointer-events: none;   /* off until open */
    /* Sit above adjacent project rows when fanned out */
    z-index: 3;
}

.new-fab--open .new-fab__menu {
    pointer-events: auto;
}

.new-fab__item {
    /* Pill row: slide-in label + circular icon. Width grows from icon-only
     * to full pill once open so the label slides into existing space.
     *
     * ICON ON THE RIGHT, AND WHY IT HAS TO BE. The menu is a column with
     * `align-items: flex-end`, so every pill shares a right edge and each
     * one is a different width because each label is a different length.
     * With the icon first in the visual order its x was therefore a
     * function of label length: measured live at 1527px the six icons
     * started at 969.5 / 984 / 976.8 / 962.2 / 976.8 / 1020.4 - a 58.2px
     * ragged column, and 55.2px ragged at 390px. Reversing the row anchors
     * the icon to the shared right edge instead, so all six x-positions
     * collapse to one value and the column lines up under the trigger "+",
     * which is right-aligned too.
     *
     * `row-reverse` rather than reordering the markup: DOM order stays
     * icon-then-label, which is the order a screen reader should read and
     * the order `transform-origin: top right` was written against.
     * The padding flips with it - the free edge is now the LEFT one. */
    width: auto;
    flex-direction: row-reverse;
    /* 44px is the iOS minimum touch target, and these are the primary
     * "start something" controls on the launchpad. Was 40px here and,
     * worse, 38px under the max-width:600px rule below - the phone got
     * the SMALLEST target of the three. */
    height: 44px;
    padding: 0 0 0 14px;
    border-radius: 22px;
    background: var(--color-bg-elevated);
    color: var(--color-fg);
    border: 1px solid var(--color-border);
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 10px;
    font-family: var(--font-mono);
    font-size: 0.78rem;
    letter-spacing: 0.02em;
    /* Initial (closed) state: collapsed, hidden, slightly above */
    opacity: 0;
    transform: translateY(-14px) scale(0.5);
    transform-origin: top right;
    transition: transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1),
                opacity 180ms ease-out,
                background 160ms ease,
                color 160ms ease,
                border-color 160ms ease,
                box-shadow 160ms ease;
    will-change: transform, opacity;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}

.new-fab__item:hover:not(:disabled) {
    background: var(--color-accent);
    color: var(--color-on-accent);
    border-color: var(--color-accent-strong);
    box-shadow: 0 4px 14px var(--color-accent-shadow-soft);
}

.new-fab__icon {
    width: 38px;
    height: 38px;
    /* Hugs the pill's right edge (the shared one) now that the row is
     * reversed. This margin is what makes the icon column's x constant,
     * so it must stay on the same side as the shared edge. */
    margin-right: 1px;
    border-radius: var(--radius-full);
    background: var(--color-accent-bg);
    color: var(--color-accent);
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    transition: background 160ms ease, color 160ms ease;
}

.new-fab__item:hover:not(:disabled) .new-fab__icon {
    background: var(--color-accent-bg-hover);
    color: var(--color-on-accent);
}

.new-fab__label {
    white-space: nowrap;
    /* Label slides in from the right when item appears */
    opacity: 0;
    transform: translateX(14px);
    transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
                opacity 180ms ease-out;
    will-change: transform, opacity;
}

/* ---- OPEN state: stagger items in (top → bottom) ------------------- */
.new-fab--open .new-fab__item {
    opacity: 1;
    transform: translateY(0) scale(1);
}
.new-fab--open .new-fab__item:nth-child(1) { transition-delay: 0ms; }
.new-fab--open .new-fab__item:nth-child(2) { transition-delay: 60ms; }
.new-fab--open .new-fab__item:nth-child(3) { transition-delay: 120ms; }
.new-fab--open .new-fab__item:nth-child(4) { transition-delay: 180ms; }
.new-fab--open .new-fab__item:nth-child(5) { transition-delay: 240ms; }

.new-fab--open .new-fab__label {
    opacity: 1;
    transform: translateX(0);
}
.new-fab--open .new-fab__item:nth-child(1) .new-fab__label { transition-delay: 80ms; }
.new-fab--open .new-fab__item:nth-child(2) .new-fab__label { transition-delay: 140ms; }
.new-fab--open .new-fab__item:nth-child(3) .new-fab__label { transition-delay: 200ms; }
.new-fab--open .new-fab__item:nth-child(4) .new-fab__label { transition-delay: 260ms; }
.new-fab--open .new-fab__item:nth-child(5) .new-fab__label { transition-delay: 320ms; }

/* ---- CLOSE state: reverse-stagger out (bottom → top) --------------- */
/* When .new-fab--open is removed, default rules apply (opacity 0,
 * collapsed). Stagger the OUT animation so item 3 leaves first. */
.new-fab:not(.new-fab--open) .new-fab__item {
    transition: transform 150ms cubic-bezier(0.4, 0, 1, 1),
                opacity 130ms ease-in,
                background 160ms ease,
                color 160ms ease,
                border-color 160ms ease,
                box-shadow 160ms ease;
}
.new-fab:not(.new-fab--open) .new-fab__item:nth-child(1) { transition-delay: 100ms; }
.new-fab:not(.new-fab--open) .new-fab__item:nth-child(2) { transition-delay: 50ms; }
.new-fab:not(.new-fab--open) .new-fab__item:nth-child(3) { transition-delay: 0ms; }
.new-fab:not(.new-fab--open) .new-fab__item:nth-child(4) { transition-delay: 0ms; }
.new-fab:not(.new-fab--open) .new-fab__item:nth-child(5) { transition-delay: 0ms; }
.new-fab:not(.new-fab--open) .new-fab__label {
    transition: transform 120ms ease-in, opacity 100ms ease-in;
    transition-delay: 0ms;
}

/* ---- Backdrop: subtle dim + click-outside catcher ------------------ */
.new-fab__backdrop {
    position: fixed;
    inset: 0;
    z-index: 1050;          /* under the FAB (1100), over header (1000) */
    background: var(--oc-backdrop-soft);
    opacity: 0;
    pointer-events: none;
    transition: opacity 180ms ease-out;
    will-change: opacity;
}

.new-fab__backdrop[data-open="1"] {
    opacity: 1;
    pointer-events: auto;
}

/* ---- Mobile tweaks ------------------------------------------------- */
@media (max-width: 600px) {
    .new-fab__trigger { width: 34px; height: 34px; }
    /* `top` is JS-owned now (see the placement note on .new-fab__menu);
     * this rule used to also set `top: 42px`, which is exactly the value
     * that pinned the column below the trigger and let the scroller clip
     * it. Only the gap is a phone concern. */
    .new-fab__menu { gap: 8px; }
    /* Height deliberately NOT shrunk. The label gets smaller on a phone;
     * the touch target must not. This rule used to set 38px, which put
     * the launchpad's primary actions under the 44px minimum on the only
     * device where they are touched rather than clicked. */
    .new-fab__item { font-size: 0.74rem; }
    .new-fab__icon { width: 36px; height: 36px; }
}

/* ---- Reduced motion: kill all transitions/animations --------------- */
@media (prefers-reduced-motion: reduce) {
    .new-fab__trigger,
    .new-fab__plus,
    .new-fab__item,
    .new-fab__label,
    .new-fab__backdrop {
        transition: none !important;
        animation: none !important;
    }
    .new-fab--open .new-fab__item,
    .new-fab--open .new-fab__label {
        transition-delay: 0ms !important;
    }
}

/* The image attach button that used to live here is GONE. Its two jobs -
 * opening the OS picker and pasting the device clipboard - are rows of
 * the terminal tools menu, opened from the phone key bar's clipboard key
 * (client/js/terminal-key-bar.js). */

/* =====================================================================
 * IMG-PASTE - inline status pill
 * ---------------------------------------------------------------------
 * Top-center toast for upload progress + result. Three semantic kinds
 * driven by ``data-kind`` attribute: info (uploading), success (paste
 * complete), error (upload failure). Animates in via opacity +
 * translateY (compositor-only - no layout thrash). Auto-dismissed by
 * the JS controller (3s info/success, 5s error).
 * ===================================================================== */
/* THE ONE TOAST. Was .cloude-status-pill, built by terminal.js at
 * `z-index: 70` - under the sticky header (1000), which sits in exactly
 * this band, and under every overlay a menu row can open. So every
 * caller reported invisibly. The element and this rule are now the same
 * thing that fab-menu.js's notify() builds, and there is no second
 * mechanism to drift.
 *
 * NO z-index HERE ON PURPOSE. fab-menu.js writes it inline from
 * NOTICE_Z, because "above every chrome layer" is an invariant a test
 * asserts against the other stylesheets, not a styling choice.
 *
 * BELOW THE HEADER, NOT OVER IT. Raising the stacking alone put the
 * toast on top of the header title, and the kind backgrounds are
 * deliberately 10% alpha (--color-danger-bg), so the title read
 * straight through it and BOTH became unreadable - measured in the
 * browser 2026-08-16. Being on top of the header is not the goal; not
 * being hidden is. Clearing --header-h means it never argues with the
 * title, while the stacking still beats every overlay a row can open. */
.fab-menu-notice {
    position: fixed;
    top: calc(var(--header-h) + 12px);
    left: 50%;
    transform: translateX(-50%) translateY(-12px);
    padding: 8px 14px;
    border-radius: var(--radius-pill);
    font-size: 13px;
    font-weight: 500;
    background: var(--color-bg-elevated);
    color: var(--color-fg);
    border: 1px solid var(--color-border);
    box-shadow: var(--shadow-modal);
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease, transform 200ms ease;
    max-width: 80vw;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.fab-menu-notice.visible {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
}

.fab-menu-notice[data-kind="error"] {
    background: var(--color-danger-bg);
    color: var(--color-danger);
    border-color: var(--color-danger-border);
}

.fab-menu-notice[data-kind="success"] {
    background: var(--color-accent-bg);
    color: var(--color-accent);
    border-color: var(--color-accent-border);
}


/* =====================================================================
 * CLIPBOARD - 📎 attach menu
 * ---------------------------------------------------------------------
 * Small fixed-position popup anchored above the 📎 button (bottom-right
 * slot) by clipboard.js#positionMenu. Token-only styling, mirroring the
 * modal/pill surface treatment. Shown on coarse-pointer devices (the
 * button itself is pointer:coarse-gated) but harmless on desktop.
 * ===================================================================== */
.cloude-attach-menu {
    position: fixed;
    display: flex;
    flex-direction: column;
    min-width: 200px;
    padding: 6px;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-modal);
    z-index: 71;   /* over the 📎 button (60) + status pill (70), under modals (9999) */
    animation: fadeIn 0.15s ease;
}

.cloude-attach-menu__item {
    display: block;
    width: 100%;
    padding: 10px 14px;
    background: transparent;
    border: none;
    border-radius: var(--radius-lg);
    color: var(--color-fg);
    font-family: inherit;
    font-size: 14px;
    text-align: left;
    cursor: pointer;
}

.cloude-attach-menu__item:hover,
.cloude-attach-menu__item:focus-visible {
    background: var(--color-bg-hover);
    color: var(--color-accent-strong);
    outline: none;
}

/* =====================================================================
 * TOUCH-SELECT - floating copy button (touch-select.js)
 * ---------------------------------------------------------------------
 * Small fixed-position action shown at the finger lift point after a
 * long-press drag selection. Token-only styling, mirroring the attach
 * menu surface treatment. Only ever created on coarse-pointer devices.
 * ===================================================================== */
.cloude-touch-copy {
    position: fixed;
    /* The generic `button` rule pins every button to a
     * var(--control-size) SQUARE, and with border-box that swallowed the
     * padding below whole: the box measured 38x36 on desktop and 40x40 at
     * 390px, a 0-2px content box for a 33.7px label. Reset both axes to
     * auto, the way .launchpad-section-toggle does, so this is a real
     * labelled pill sized by its own padding. */
    width: auto;
    height: auto;
    padding: 10px 18px;
    background: var(--color-bg-elevated);
    color: var(--color-fg);
    border: 1px solid var(--color-accent-border);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-modal);
    font-family: inherit;
    font-size: 14px;
    cursor: pointer;
    z-index: 72;   /* over the 📎 button (60) + attach menu (71), under modals (9999) */
    animation: fadeIn 0.15s ease;
}

.cloude-touch-copy:hover,
.cloude-touch-copy:focus-visible {
    background: var(--color-bg-hover);
    color: var(--color-accent-strong);
    outline: none;
}

/* Long-press on the terminal must never raise the iOS text callout. */
@media (pointer: coarse) {
    #terminal .xterm {
        -webkit-touch-callout: none;
    }
}

/* ===========================================================================
 * Settings panels' shared field styles (feat/settings-screen).
 *
 * The settings PAGE is compiled now (web/src/lib/settings-page/, the
 * settings redesign, 2026-09-28): its rail, rows, search and save bar are
 * there, on the manager frame's vocabulary. What stays here is what the
 * classic-script panels its rows mount still render into - `.settings-section`,
 * the `.settings-field*` family and the hint - for the terminal command
 * list, the legacy agent commands and the
 * settings import. The page's own stylesheet hides a panel's section title
 * and description, because the row already says them.
 * =========================================================================== */

.settings-section {
    margin-bottom: 28px;
}

.settings-section:last-child {
    margin-bottom: 4px;
}

.settings-section-title {
    color: var(--oc-text-primary);
    font-size: 1rem;
    margin: 0 0 4px 0;
    font-weight: bold;
    text-transform: lowercase;
}

.settings-section-description {
    color: var(--color-fg-muted);
    font-size: 0.82rem;
    margin-bottom: 12px;
}

.settings-field {
    margin-bottom: 14px;
}

.settings-field-checkbox .settings-field-label {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--color-fg);
    font-size: 0.9rem;
    cursor: pointer;
}

.settings-field-checkbox input[type="checkbox"] {
    width: auto;
    height: auto;
    accent-color: var(--color-accent);
}

/* A field label is quieter than the heading above it, which is the level
 * `.modal-label` prints at in the shared dialog. */
.settings-field-label {
    color: var(--oc-text-secondary);
    font-size: 0.85rem;
    margin-bottom: 6px;
    display: block;
    text-transform: lowercase;
}

.settings-field-hint {
    color: var(--color-fg-muted);
    font-size: 0.78rem;
    margin-top: 4px;
}

.settings-field-effective {
    color: var(--color-fg-muted);
    font-size: 0.78rem;
    margin-top: 6px;
}

.settings-field-effective code {
    color: var(--color-fg);
    background: var(--color-bg-elevated);
    padding: 1px 5px;
    border-radius: var(--radius-sm, 4px);
}

.settings-save-status {
    color: var(--color-fg-muted);
    font-size: 0.8rem;
    margin-right: auto;
}

/* feat/launch-wrappers - wrapper list/editor (its panel was retired with the wrappers page, S9a) */

.settings-wrapper-security-note {
    margin-bottom: 14px;
}

.settings-wrapper-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-bottom: 12px;
}

/* A wrapper row is TWO STACKED LINES: the title line (label, id, badges,
 * action buttons) and, under it, the description at the row's FULL width.
 * It used to be a single horizontal row with the description sharing the
 * left column, which made the description's width a function of the
 * button count in the right column - 272.1px on a two-button row and
 * 115.5px on a three-button row, measured in a 556px modal. See the
 * retired agent-wrappers-view.js renderRow (S9a). */
.settings-wrapper-row {
    display: flex;
    flex-direction: column;
    align-items: stretch;
    gap: 4px;
    padding: 10px 12px;
    background: var(--color-bg-elevated);
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
}

/* THE TITLE LINE WRAPS. Moving the DESCRIPTION onto its own full-width
 * line (see the comment above) fixed the description and left the LABEL
 * standing in the same squeezed column, so the defect simply moved up one
 * line and kept the same numbers. Measured in a 556px modal at 1400px,
 * before this rule:
 *     row `claude` (2 buttons, actions 164.9px)  label block 272.1 x 20
 *     row `cld`    (3 buttons, actions 321.5px)  label block 115.5 x 57
 *     worst case   (3 buttons, long label)       label block 115.5 x 210
 * 115.5px at this type size is about fourteen characters, which is why
 * `cld (keychain-backed)` rendered as a three-line ribbon and a long
 * label as a ten-line one. The actions group is `flex-shrink: 0`, so
 * every pixel a third button costs comes out of the label.
 *
 * `flex-wrap: wrap` with the label keeping `flex-basis: auto` is what
 * fixes it for the WORST case rather than for one row: the wrap decision
 * uses the label's max-content size, so a title line that fits stays on
 * one line, and one that does not puts the buttons on their own line and
 * hands the label the row's full width. It needs no width threshold, no
 * button count and no media query, so it cannot be outgrown by a fourth
 * button or a longer label. `min-width: 0` on the label block still does
 * the second job - keeping one unbreakable token from setting a floor
 * wider than the row. */
.settings-wrapper-row-head {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px;
}

/* Full-width by construction: it is a block child of a column flex
 * container, so nothing above it can squeeze it. min-width:0 keeps a
 * single unbreakable token (a long entry name in <code>) from setting a
 * floor wider than the row. */
.settings-wrapper-row-desc {
    min-width: 0;
    overflow-wrap: anywhere;
}

/* A flex item defaults to min-width:auto, so a long id/label/description
 * sets a floor wider than the container and the whole row overflows
 * sideways instead of wrapping. This is what pushed the delete button off
 * the right edge on a phone. */
.settings-wrapper-row-main {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
}

.settings-wrapper-row-main code {
    color: var(--color-fg-muted);
    font-size: 0.8rem;
}

.settings-wrapper-row-actions {
    display: flex;
    gap: 6px;
    flex-shrink: 0;
}

/* THE DEFAULT MARKER. Was an outlined, rounded chip in --color-accent -
 * a lozenge, and accent-on-background besides, which is the contrast
 * problem the old settings tab strip's active tab was fixed for (the `calming`
 * accent is about 2.6:1 on its own background, under the 4.5:1 floor).
 *
 * It now carries the SAME cue set the active tab carries, deliberately,
 * so the settings screen has one flat way of saying "this is the
 * selected one": --color-fg text, a soft accent tint, an accent
 * underline, bold weight. Square. Three cues, and the state survives
 * losing any one of them. The word "default" is unchanged - the shape
 * went, the information did not. */
.settings-wrapper-badge {
    display: inline-block;
    color: var(--color-fg);
    background: var(--color-accent-bg-soft);
    border: none;
    border-bottom: 2px solid var(--color-accent);
    border-radius: 0;
    font-size: 0.7rem;
    font-weight: bold;
    padding: 1px 6px;
    margin-left: 6px;
    text-transform: lowercase;
}

.settings-wrapper-actions-row {
    display: flex;
    gap: 8px;
}

/* The save affordance for a family's legacy static command, folded in
 * from the removed "agents" settings tab. `min-width: 0` on the flex
 * items is deliberate: a flex child defaults to `min-width: auto`, which
 * refuses to shrink below its content, and the status line here can carry
 * a whole server error message. Without it that message pushes the button
 * off the edge of a 390px modal instead of wrapping. */
.settings-legacy-actions {
    display: flex;
    align-items: center;
    gap: 10px;
    margin-top: 10px;
    flex-wrap: wrap;
}

.settings-legacy-actions > * {
    min-width: 0;
}

.settings-legacy-status {
    color: var(--color-fg-muted);
    font-size: 0.8rem;
    overflow-wrap: anywhere;
}

.settings-wrapper-editor {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 14px;
    margin-bottom: 12px;
    background: var(--color-bg-elevated);
}

.settings-wrapper-script-input {
    font-family: var(--font-mono);
    white-space: pre;
    resize: vertical;
    line-height: 1.4;
}

.settings-wrapper-editor-actions {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 10px;
}

/* Collapsed "advanced" disclosure (the legacy claude_command row). */
.settings-advanced-summary {
    color: var(--color-fg-muted);
    font-size: 0.82rem;
    cursor: pointer;
    text-transform: lowercase;
    padding: 6px 0;
}

.settings-advanced[open] .settings-advanced-summary {
    color: var(--color-fg);
}

/* feat/universal-wrappers - one group per command family on the wrappers
 * settings screen. The group is a visual container only; every wrapper row
 * inside it keeps the markup it already had. */
.settings-wrapper-family {
    border: 1px solid var(--color-border);
    border-radius: var(--radius-md);
    padding: 12px;
    margin-bottom: 14px;
}

/* The heading now does its own grouping work with a rule under it,
 * rather than leaning on a bordered chip to look like a heading. */
.settings-wrapper-family-title {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 0 0 10px;
    padding-bottom: 6px;
    border-bottom: 1px solid var(--color-border);
    font-size: 0.85rem;
    letter-spacing: 0.06em;
    text-transform: lowercase;
    color: var(--oc-text-primary);
}

/* The count was a bordered, rounded 20.8x15 chip. It is now a plain
 * muted numeral parked at the end of the heading rule - the table-header
 * idiom - so the number is still there and the box is not. */
.settings-wrapper-family-count {
    color: var(--color-fg-muted);
    border: none;
    border-radius: 0;
    background: none;
    font-size: 0.72rem;
    padding: 0;
    margin-left: auto;
    font-weight: normal;
}

/* "takes model" is an ANNOTATION, not a state, so it gets neither the
 * tint nor the underline the default marker gets. A muted run of text
 * behind a hairline separator: still legible as its own field, with no
 * chip around it. */
.settings-wrapper-badge-model {
    color: var(--color-fg-muted);
    background: none;
    border: none;
    border-left: 1px solid var(--color-border);
    border-radius: 0;
    font-weight: normal;
    padding: 0 0 0 6px;
}

/* Provider picker: a non-selectable family heading between wrapper
 * groups. Deliberately NOT a .folder-picker-item so it stays out of the
 * keyboard nav index. */
.provider-family-heading {
    color: var(--color-fg-muted);
    font-size: 0.7rem;
    text-transform: lowercase;
    letter-spacing: 0.06em;
    padding: 10px 12px 4px;
    border-top: 1px solid var(--color-border);
    margin-top: 4px;
}

.provider-family-heading:first-child {
    border-top: none;
    margin-top: 0;
    padding-top: 4px;
}

/* Narrow phones: let action buttons take the full row rather than
 * squeezing the command text to nothing. */
@media (max-width: 480px) {
    .settings-wrapper-row-actions {
        width: 100%;
    }

    .settings-wrapper-row-actions .modal-btn {
        flex: 1 1 auto;
    }

    /* Widening the action group alone was not enough: the ROW is still a
     * horizontal flex container, so the buttons kept being pushed off the
     * right edge and the modal body scrolled sideways. Stack the row on a
     * phone so the actions sit under the text they act on.
     * `.settings-wrapper-row` is a column at every width now, so what has
     * to stack here is its TITLE LINE, not the row. */
    .settings-wrapper-row-head {
        flex-direction: column;
        align-items: stretch;
    }

    /* The head is a column here, so its own gap has to supply the space
     * the horizontal `gap: 12px` used to. */
    .settings-wrapper-row-head {
        gap: 8px;
    }

    /* A family group's own padding is a meaningful slice of a 375px
     * viewport; trim it so the rows inside keep their readable width. */
    .settings-wrapper-family {
        padding: 8px;
    }

    /* Both add buttons on one line leaves each too narrow to read. */
    .settings-wrapper-actions-row {
        flex-wrap: wrap;
    }
}

/* ---------------------------------------------------------------------
 * feat/home-screen-mechanics - home screen behaviour round. Mechanics
 * only: no new palette, no new tokens, every colour below already
 * existed and is read from the same theme variables it was read from
 * before.
 * --------------------------------------------------------------------- */

/* ITEM 48 - THE HELP CONTROL MOVES, THE HELP PANEL DOES NOT.
 * The panel is still the first child of `.launchpad-container`, still a
 * native <details>, still rendered by launchpad.js. Only its control
 * moved into the header's top-right cluster. The in-pane <summary> stays
 * in the markup because that is what makes the element a disclosure at
 * all, but it is taken out of the layout so there is exactly ONE control
 * on screen rather than two that toggle the same thing.
 * `visibility: hidden` would still cost its box; `display: none` on a
 * <summary> leaves the <details> perfectly openable via its `open`
 * property, which is what the header button sets. */
#launchpad-screen .adopt-disclosure > summary {
    display: none;
}

/* THE WHITE SQUARE, AND WHY IT WAS THERE.
 *
 * This button used to carry NO class at all. It was written on the
 * assumption, stated in its old comment, that it "inherits the shared
 * header icon-button look from the `.controls button` rules far above".
 * There are no such rules. The only `.controls`-scoped declarations in
 * this file are `.header .controls button > svg { color }` and its
 * `.danger` sibling - both of which style the GLYPH, neither of which
 * styles the BUTTON. The button look lives in `.btn-icon` (far above),
 * and that rule replaced an earlier bare `button {}` element reset which
 * was deliberately deleted; its comment records that exactly two
 * elements were expected to opt in by class and that no fallback reset
 * would be added. This button was written after that deletion and opted
 * into nothing.
 *
 * So it fell through to the USER-AGENT stylesheet. Measured in Chromium
 * before the fix: `background-color: rgb(239, 239, 239)` (UA ButtonFace),
 * `border: 2px outset`, `border-radius: 0px`, `appearance: auto`,
 * 20x20 shrink-wrapped around its 16px glyph instead of the
 * `--control-size` box every sibling uses. A near-white, hard-cornered,
 * bevelled square with a correctly-coloured "?" drawn on top of it -
 * which is the white square the user reported. It was never an asset
 * problem: the glyph is inline SVG with `stroke="currentColor"` and it
 * resolved correctly the whole time (measured `rgb(215, 119, 87)`, the
 * accent). Nothing here introduces a colour; `.btn-icon` on the element
 * hands it the same theme tokens every other header icon button has
 * always used.
 *
 * Note this cannot be caught by a DOM assertion. The element, its class,
 * its glyph and its aria were all correct while it rendered wrong, so
 * the test for it reads `getComputedStyle` in a real browser - see
 * scripts/ci/mutate-header-icons-and-menu.sh and
 * tests/test_header_help_and_toggle.node.mjs.
 *
 * Home screen only, gated on the class app.js already stamps on the
 * header for the launchpad, so no screen-switch code has to remember to
 * show or hide this button. The ID selector here outranks `.btn-icon`'s
 * own `display: flex`, so the gate keeps working. */
#launchpad-help-btn {
    display: none;
}

/* BESIDE THE TITLE, NOT IN THE RIGHT-HAND CLUSTER. The button is a child
 * of `#appTitle` now (see client/index.html), so it inherits that box's
 * centring and stays glued to the title text at every width.
 *
 * `flex-shrink: 0` is load-bearing, not cosmetic: header-title-fit.js's
 * availableWidth() enumerates the h1's LIVE children and subtracts each
 * one's outer width from the title's elision budget, and its doc comment
 * states the invariant that every such child is fixed-size. A shrinkable
 * child would make the measured budget disagree with what the browser
 * actually lays out, which is the precise failure mode that once clipped
 * the title to "Cloude Code L..." with the full string still in the DOM.
 * The same function skips `display: none` children, so this button costs
 * the terminal screen's session title exactly nothing. */
.header--home #launchpad-help-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    margin-left: 8px;
}

/* THE HOME SCREEN'S THEMED-ROW OVERRIDE IS GONE, because there is
 * nothing left for it to override. Deliberately no rule here, so this
 * comment is the record of why.
 *
 * It was `box-shadow: inset 0 0 0 1px var(--session-theme-ring)` on
 * `.launchpad-container .running-session-row[data-session-theme]`,
 * existing for exactly one reason: session-theme-tint.css declared a 3px
 * inline-start rail plus the ring for BOTH surfaces, and the home screen
 * was not to carry a left-side colour bar, so this re-declared the ring
 * alone and won on specificity. The sidebar kept the rail.
 *
 * The sidebar does not keep it any more ("get rid of the thick left bar
 * like on the homepage and clean it like the homepage looks now"), so
 * the shared rule in session-theme-tint.css is now ring-only for both
 * and this rule would be a byte-for-byte duplicate of it. A rule that
 * restates what it overrides is not a safety net, it is a second place
 * to forget to change.
 *
 * If a left-side rail is ever wanted on one surface and not the other,
 * this is where the exception goes - but decide it here rather than
 * declaring it for both and cancelling it in one place. */


/* ITEMS 51/52/53 - the speed-dial's first item now carries the app's own
 * icon FILE rather than a drawn glyph. Sized to the same 18px box every
 * sibling <svg> uses so the icon column stays one width. */
.new-fab__icon-img {
    width: 18px;
    height: 18px;
    display: block;
    object-fit: contain;
}

/* The shared choice modal (`_showChoiceModal`) reuses
 * `.folder-picker-list` / `.folder-picker-item` verbatim. These three
 * rules only add what a two-line row and a refused row need; no colour
 * here is new. */
.folder-picker-item {
    /* Rows in the choice modal stack a label over a subtitle. The picker
     * this borrows from has single-line rows, so it centres them; column
     * direction with a left alignment is the only change a two-line row
     * needs, and a single-line row renders identically under it. */
    flex-direction: column;
    align-items: flex-start;
    gap: 2px;
}

/* "you have none" and "I could not find out" are different answers and
 * must not look the same. The second one is a WARNING colour because it
 * is an unmeasured state, not a clean empty list. */
.folder-picker-empty--unknown {
    color: var(--color-warning);
    font-style: normal;
}
