* { box-sizing: border-box; }
  :root {
    /* The first attempt here tinted toward the palette's pale yellow and
       read as "too yellowish" once lived with -- this instead derives
       from the palette's green (#7D9C65), diluted toward white heavily
       enough to stay unmistakably "light" (contrast of body text against
       it measures 15.5:1) while still reading as a deliberate, muted
       sage rather than either clinical white or a yellow cast. */
    --bg: #f7f9f6; --bg-subtle: #eef2eb; --bg-elevated: #fbfcfa;
    --border: #d5dad6; --text: #1f1f23; --text-muted: #6e6e78;
    /* Palette: https://colorhunt.co/palette/7d9c65b93636ff9c4cfff2a0 --
       accent is the palette's own green (#7D9C65) deepened just enough
       for white button text to clear AA (raw green vs white is only
       3.08:1; #5E754C measures 5.09:1) -- accent-soft is the palette's
       pale yellow verbatim (already light, per the "background colors
       light" request), and error is the palette's red verbatim (5.76:1
       vs white already clears AA with no adjustment needed). */
    --accent: #5e754c; --accent-soft: #fff2a0; --accent-strong: #516542;
    --info: #2563eb; --success: #16a34a; --warn: #ff9c4c; --error: #b93636;
    --badge-text: #1f1f23;
  }
  body {
    background:var(--bg); color:var(--text);
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
    font-size: 14px; margin:0; padding:0;
  }
  h1 {
    font-size: 15px; color:var(--text); font-weight:600; letter-spacing: .1px;
    margin: 0; flex-shrink: 0;
  }
  .line { margin: 3px 0; line-height: 1.5; }
  /* A rule line (a milestone badge like "Claude · Sonnet 5.5") used to
     be prefixed with a literal "───── " text rule --
     ascii-art that read as a stray typo rather than a deliberate divider.
     Extra top margin gives the same "this starts a new beat" cue through
     spacing instead, the way a modern chat log separates milestones. */
  .line-rule { margin-top: 12px; }
  /* Dark text, not white: every badge background (an explicit data.color,
     or the LEVEL_BADGE_HEX fallback below -- a deliberately lighter
     palette than LEVEL_HEX, which is for bullet/status TEXT instead) is
     a light/pastel accent color, and white text on any of them fails
     WCAG AA contrast. */
  .badge { padding: 2px 10px; border-radius: 999px; color:var(--badge-text); font-weight:600; font-size: 12px; }
  .bullet-dot {
    display:inline-block; width:6px; height:6px; border-radius:50%;
    margin: 0 9px 1px 1px; vertical-align:middle; flex-shrink:0;
  }
  /* margin-inline-start, not margin-left: this element's own dir=
     (pickDirection() below, set per-line from the line's actual content)
     must control which side the indent falls on -- a fixed margin-left
     would indent an RTL detail line away from its own reading direction's
     start edge, looking visually backwards. */
  .detail { color:var(--text-muted); margin-inline-start: 16px; }
  .hint { color:var(--text-muted); font-size: 12px; margin: 0 0 8px; }
  form input {
    font: inherit; padding: 8px 10px; background:var(--bg); color:var(--text);
    border:1px solid var(--border); border-radius: 8px;
  }
  form button {
    font: inherit; padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border);
    background: var(--accent); color:#fff; cursor:pointer; transition: background .15s ease;
  }
  form button:hover { background: var(--accent-strong); }
  .error { color:var(--error); margin: 8px 0 0; }

  /* Login page: centered card, not a bare form floating at the top
     left -- the one surface a brand-new visitor sees before anything
     else, so it should already look like a real product. */
  body.login { display:flex; align-items:center; justify-content:center; min-height:100vh; }
  .login-card {
    width: 100%; max-width: 360px; padding: 32px; border: 1px solid var(--border);
    border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,.06);
  }
  .login-card h1 { margin-bottom: 18px; }
  .login-card form { display:flex; flex-direction:column; gap:10px; }

  /* .page fills the viewport; .topbar is a fixed-height header, .layout
     takes the rest via flex:1. Scoped to .page/.layout, never to
     body/html, since _STYLE is shared with the login page too. */
  .page { display:flex; flex-direction:column; height: 100vh; }
  .topbar {
    display:flex; align-items:center; justify-content:space-between; gap:12px;
    padding: 12px 16px; border-bottom: 1px solid var(--border); flex-shrink:0;
    background: var(--bg);
  }
  /* The sidebar toggle lives on the START side, right beside the title --
     spatially over the sidebar's own left edge, not grouped with the
     unrelated agent/model menu on the right, and not a hamburger (which
     conventionally means "open navigation", not "toggle this panel"). */
  .topbar-start { display:flex; align-items:center; gap:10px; min-width:0; }
  .topbar-start h1 { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .topbar-actions { display:flex; align-items:center; gap:6px; flex-shrink:0; }
  /* Hidden (height:0) until JS sets .lost or .back -- the one honest
     answer to "it said shutting down, then nothing": the SERVER that
     would ever send a "shutdown finished" message is the same one being
     shut down, so it can't -- by definition, nothing can message you
     once it's gone. What the CLIENT can and must report instead is its
     own connection state: poll() failing repeatedly means lost, not
     silence; a poll succeeding again after that means back. */
  .conn-banner {
    overflow:hidden; max-height:0; transition: max-height .2s ease;
    font-size: 13px; text-align:center; padding: 0 16px;
  }
  .conn-banner.lost { max-height:36px; padding:8px 16px; background: var(--warn); color:#1f1f23; }
  .conn-banner.back { max-height:36px; padding:8px 16px; background: var(--success); color:#fff; }
  .icon-btn {
    font: inherit; font-size: 16px; width: 34px; height: 34px; border-radius: 8px;
    border: 1px solid transparent; background: transparent; color:var(--text);
    cursor:pointer; display:flex; align-items:center; justify-content:center;
    flex-shrink:0;
  }
  .icon-btn:hover { background: var(--bg-subtle); border-color: var(--border); }
  .icon-btn.active { background: var(--accent-soft); color: var(--accent-strong); }
  /* Icons are inline SVG (currentColor), not mixed emoji/text glyphs --
     an emoji (the old paperclip) renders as a colorful, differently-sized
     pictograph depending on the OS/font, inconsistent with every other
     icon here; a plain SVG always matches the button's own text color
     and size exactly. */
  .icon-btn svg { display:block; flex-shrink:0; }

  .layout { display:flex; gap:0; flex:1; min-height:0; }
  .sidebar {
    width: var(--sidebar-width, 420px); flex-shrink: 0; overflow-y: auto; overflow-x: hidden;
    display:flex; flex-direction:column; gap:10px; padding: 12px; background: var(--bg-subtle);
    border-inline-end: 1px solid var(--border);
    transition: width .2s ease, padding .2s ease, opacity .15s ease;
  }
  /* Hidden by default (toggled via the topbar's menu button) -- project
     introspection (git status, file tree, apps) is useful, but it is
     not the first thing a non-developer visitor should have to look
     at. Collapsed via width/opacity, not display:none, so toggling is
     an actual slide, not an abrupt snap. */
  .layout.sidebar-hidden .sidebar {
    width: 0; padding-inline: 0; opacity: 0; border-inline-end-color: transparent;
  }
  .layout.sidebar-hidden .sidebar-resizer { display:none; }
  /* Grabbable strip between the sidebar and the chat, dragged in JS to
     set --sidebar-width. The visible line is a slim 2px centered inside
     a wider 8px hit target, so it's comfortable to grab without looking
     thick when idle. */
  .sidebar-resizer {
    width: 8px; flex-shrink: 0; cursor: col-resize;
    position: relative; z-index: 3;
  }
  .sidebar-resizer::after {
    content: ""; position: absolute; top: 0; bottom: 0; left: 3px; width: 2px;
    border-radius: 1px; background: transparent;
  }
  .sidebar-resizer:hover::after, .sidebar-resizer.dragging::after { background: var(--accent); }
  .main { flex: 1; min-width: 0; min-height: 0; position: relative; display:flex; flex-direction:column; }
  /* A centered column, not edge-to-edge text -- the same "chat in the
     middle of the page" convention a familiar assistant UI uses, rather
     than a log that fills the entire browser width. */
  .chat-scroll { flex:1; overflow-y:auto; }
  .chat { max-width: 720px; margin: 0 auto; padding: 20px 16px 8px; }

  /* Each sidebar panel is its own self-contained scope: one header, one
     body, both inside the SAME card -- never a header stacked above a
     DIFFERENT panel's own body. */
  .panel {
    background: var(--bg-elevated); border: 1px solid var(--border); border-radius: 10px;
    overflow: hidden; flex-shrink: 0;
  }
  .panel-header {
    display:flex; align-items:center; gap:6px;
    padding: 9px 12px; font-size: 12px; color:var(--text-muted);
    cursor: default; user-select: none;
    position: sticky; top: 0; z-index: 2; background: var(--bg-elevated);
  }
  .panel-header.clickable { cursor: pointer; }
  .panel-header.clickable:hover { background: var(--bg-subtle); }
  .panel-header .branch { color:var(--info); font-weight: 600; }
  /* pre, not pre-wrap: a long path wrapping mid-word inside a narrow
     sidebar is what produced garbled-looking text -- a horizontal
     scroll on the line itself reads correctly regardless of how long a
     path is, instead of the browser breaking it at an arbitrary
     character. */
  .panel-body {
    display:none; font-size: 12px; color:var(--text-muted); white-space: pre;
    padding: 0 12px 10px 30px; border-top: 1px solid var(--border);
    overflow-x: auto;
  }
  .panel-body.open { display:block; padding-top: 8px; }
  .panel-body.tree-view { padding-left: 14px; }
  .panel-body.tree-view div { white-space: pre; }
  .status-code { display:inline-block; width: 1.6em; font-weight:600; }
  .tree-name-dir { color:var(--info); }
  .tree-name-file { color:var(--text-muted); }

  /* The Apps panel: one row per app under runtime/apps/, a Run and a
     Stop button each -- the same run_app/stop_app operations typing
     "run <name>"/"stop <name>" already triggers, just reachable by
     clicking instead. */
  .app-row {
    display:flex; align-items:center; justify-content:space-between; gap:6px;
    padding: 5px 0; font-size: 12px; color:var(--text);
  }
  .app-row-left { display:flex; align-items:center; gap:6px; min-width:0; overflow:hidden; }
  .app-row-kind { display:flex; flex-shrink:0; color:var(--text-muted); }
  .app-row-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .app-row-actions { display:flex; gap:4px; flex-shrink:0; }
  .app-row-actions button, .app-row-actions a {
    font: inherit; font-size: 11px; padding: 3px 8px; border-radius: 6px;
    border: 1px solid var(--border); background: var(--bg-subtle); color:var(--text);
    cursor:pointer; text-decoration:none; display:inline-block; line-height: 1.4;
  }
  .app-row-actions button:hover, .app-row-actions a:hover { background: var(--border); }
  .app-empty { color:var(--text-muted); font-style: italic; font-size: 12px; }

  .disclosure-arrow {
    display:inline-block; width: 11px; flex-shrink:0; color:var(--text-muted);
    transition: transform .15s ease;
  }
  .disclosure-arrow.open { transform: rotate(90deg); }

  .tree-dir { cursor: pointer; border-radius: 4px; }
  .tree-dir:hover { background: var(--bg-subtle); }

  /* One turn = the viewer's own message, the agent's background
     activity (collapsed by default), then the final reply -- the same
     grouping a familiar chat assistant UI already uses, instead of a
     flat scrolling log where a heartbeat line and the final answer look
     equally important. */
  .turn { margin: 0 0 22px; }
  .msg-user {
    align-self: flex-end; margin-inline-start: auto; max-width: 75%;
    background: var(--accent-soft); color: var(--text); border-radius: 16px;
    padding: 10px 14px; white-space: pre-wrap; display:block; width:fit-content;
  }
  .msg-row-user { display:flex; align-items:center; gap:4px; margin: 0 0 8px; }
  .msg-reply { color: var(--text); line-height: 1.6; }
  .msg-reply.is-error { color: var(--error); }
  /* Agent replies are markdown-formatted far more often than not --
     rendered now (see renderMarkdown in app.js) instead of showing raw
     asterisks/hashes as literal text. white-space:pre-wrap moved OFF
     the container itself (these are now real block elements that
     already space themselves) and onto just the leftover plain
     paragraphs/code, which still need it for intentional line breaks. */
  .msg-reply p { margin: 0 0 10px; white-space: pre-wrap; }
  .msg-reply p:last-child { margin-bottom: 0; }
  .msg-reply h1, .msg-reply h2, .msg-reply h3, .msg-reply h4, .msg-reply h5, .msg-reply h6 {
    margin: 14px 0 6px; line-height: 1.3;
  }
  .msg-reply h1:first-child, .msg-reply h2:first-child, .msg-reply h3:first-child { margin-top: 0; }
  .msg-reply ul, .msg-reply ol { margin: 0 0 10px; padding-inline-start: 22px; }
  .msg-reply li { margin: 2px 0; }
  .msg-reply code {
    font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
    font-size: 0.9em; background: var(--bg-subtle); border-radius: 4px; padding: 1px 5px;
  }
  .msg-reply pre {
    background: var(--bg-subtle); border: 1px solid var(--border); border-radius: 8px;
    padding: 10px 12px; overflow-x: auto; margin: 0 0 10px;
  }
  .msg-reply pre code { background: none; padding: 0; white-space: pre; }
  .msg-reply a { color: var(--accent-strong); text-decoration: underline; }
  /* The reply plus a small "copy the request and result" button --
     observed for real: pasting a request+result pair elsewhere (to ask
     about it, to report it) meant manually selecting text around
     whatever else is on the page. flex-start, not center: the button
     sits at the TOP of a possibly-many-line reply, not vertically
     centered against its whole height. */
  .msg-reply-row { display:flex; align-items:flex-start; gap:6px; }
  .msg-reply-row .msg-reply { flex:1; min-width:0; }
  .copy-btn {
    flex-shrink:0; width:26px; height:26px; border-radius:6px; margin-top:2px;
    border:1px solid transparent; background:transparent; color:var(--text-muted);
    cursor:pointer; display:flex; align-items:center; justify-content:center;
    opacity:0; transition: opacity .1s ease;
  }
  /* Hidden until the row it belongs to is actually hovered (or the
     button itself has keyboard focus) -- permanently visible read as
     clutter sitting next to every single message. */
  .msg-row-user:hover .copy-btn, .msg-reply-row:hover .copy-btn, .copy-btn:focus-visible {
    opacity:1;
  }
  .copy-btn:hover { background: var(--bg-subtle); border-color: var(--border); color:var(--text); }
  .copy-btn svg { display:block; }

  .activity {
    margin: 8px 0; border: 1px solid var(--border); border-radius: 10px;
    background: var(--bg-subtle); overflow: hidden;
  }
  .activity-toggle {
    display:flex; align-items:center; gap:6px; width:100%; text-align:start;
    font: inherit; font-size: 12px; color:var(--text-muted); background:none; border:none;
    padding: 8px 12px; cursor:pointer;
  }
  .activity-toggle:hover { background: var(--border); }
  .activity-body {
    display:none; padding: 2px 12px 10px 28px; border-top: 1px solid var(--border);
    font-size: 12px;
  }
  .activity-body.open { display:block; }

  .ask {
    position: sticky; bottom: 0; display:flex; flex-direction:column; gap:6px;
    padding: 10px 16px 14px; background:var(--bg); border-top: 1px solid var(--border);
    max-width: 720px; margin: 0 auto; width:100%;
  }
  /* A single rounded box holding the textarea, with Attach/Send/Stop as
     round icon buttons overlaid in its own bottom-right corner -- the
     same composer shape a familiar chat assistant UI uses, instead of
     text-labeled buttons sitting in their own row beside the input. */
  .ask-box { position:relative; }
  .ask-box textarea {
    width:100%; font: inherit; padding: 12px 84px 12px 14px; background:var(--bg); color:var(--text);
    border:1px solid var(--border); border-radius: 16px; resize: vertical; box-sizing:border-box;
  }
  .ask-box textarea:focus { outline: none; border-color: var(--accent); }
  .ask-icons { position:absolute; right:8px; bottom:8px; display:flex; gap:4px; }
  .ask-icons .icon-btn {
    width:34px; height:34px; border-radius:50%; border:1px solid var(--border);
    background: var(--bg-subtle); color:var(--text); font-size:15px; line-height:1;
    display:flex; align-items:center; justify-content:center; cursor:pointer;
    transition: background .15s ease, opacity .15s ease;
  }
  .ask-icons .icon-btn:hover:not(:disabled) { background: var(--border); }
  .ask-icons .send { background: var(--accent); color:#fff; border-color: var(--accent); }
  .ask-icons .send:hover:not(:disabled) { background: var(--accent-strong); }
  .ask-icons .stop { background: var(--error); color:#fff; border-color: var(--error); }
  .ask-icons .stop:hover:not(:disabled) { background: #982c2c; }
  .ask-icons button:disabled, .ask-box textarea:disabled { opacity: 0.55; cursor: default; }
  /* The attached-file chip: its own small row below the textarea, shown
     only once a file has been picked, so the (possibly huge) file content
     never has to be dumped into the visible textarea just to prove
     something is attached. */
  .attachment {
    display:flex; align-items:center; gap:6px; font-size:12px; color:var(--text-muted);
    background:var(--bg-subtle); border:1px solid var(--border); border-radius:8px;
    padding:4px 8px; align-self:flex-start; max-width: 100%;
  }
  .attachment span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
  .attachment button {
    font: inherit; background:none; border:none; color:var(--text-muted); cursor:pointer; padding:0 2px;
  }
  .attachment button:hover { color:var(--error); }
  /* A hover-only title attribute on the textarea was never actually
     seen by anyone who didn't already know to hover for it -- a plain,
     always-visible line beneath the composer is the one way an end
     user with no prior knowledge of this shortcut ever discovers it. */
  .ask-hint { font-size: 11px; color: var(--text-muted); text-align: end; padding: 0 4px; }

  /* Agent/model picker: a small dropdown anchored under its own topbar
     button -- additive to the existing slash commands (/agents,
     /models, /agent <provider> ...), which keep working unchanged;
     clicking an entry here just sends the same utterance typing it
     would have sent. */
  .menu-wrap { position: relative; }
  .menu-panel {
    display:none; position:absolute; inset-inline-end:0; top: calc(100% + 6px);
    min-width: 220px; max-height: 320px; overflow-y:auto; background: var(--bg-elevated);
    border:1px solid var(--border); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.08);
    z-index: 5; padding: 6px;
  }
  .menu-panel.open { display:block; }
  .menu-item {
    display:block; width:100%; text-align:start; font: inherit; font-size: 13px;
    padding: 7px 10px; border-radius: 7px; border:none; background:none; color:var(--text);
    cursor:pointer;
  }
  .menu-item:hover { background: var(--bg-subtle); }
  .menu-item.current { color: var(--accent-strong); font-weight:600; }
  .menu-heading { font-size: 11px; color:var(--text-muted); padding: 6px 10px 2px; }
  /* A provider row: its own pin/select button plus a small "?" that
     reaches that provider's real CLI help/config via the existing
     "/agent <provider> --help" escape hatch -- two different actions,
     so two different clickable targets, not one button with two
     meanings. */
  .menu-item-row { display:flex; align-items:stretch; gap:2px; }
  .menu-item-row .menu-item { flex:1; min-width:0; }
  .menu-help-btn {
    flex-shrink:0; width:30px; border-radius:7px; border:none; background:none;
    color:var(--text-muted); cursor:pointer; display:flex; align-items:center; justify-content:center;
  }
  .menu-help-btn:hover { background: var(--bg-subtle); color:var(--text); }
  .menu-help-btn svg { display:block; }

  /* The credentials modal -- real dialog, not a plain-text yes/no round
     trip through the chat box. Submitting composes one instruction
     naming the state/.env convention and sends it through the normal
     /ask flow; the coding agent does the actual writing + running. */
  .modal-backdrop {
    display:none; position:fixed; inset:0; background: rgba(0,0,0,.35);
    align-items:center; justify-content:center; z-index: 20;
  }
  .modal-backdrop.open { display:flex; }
  .modal {
    width: 100%; max-width: 440px; max-height: 80vh; overflow-y:auto;
    background: var(--bg-elevated); border-radius: 14px; padding: 20px;
    box-shadow: 0 16px 48px rgba(0,0,0,.18);
  }
  .modal h2 { font-size: 15px; margin: 0 0 4px; }
  .modal p.hint { margin: 0 0 14px; }
  .cred-row { display:flex; align-items:center; gap:8px; margin-bottom: 8px; }
  .cred-row-label {
    font-size: 13px; font-weight:600; min-width: 140px; max-width: 200px;
    overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
  }
  .cred-row input {
    flex:1; font: inherit; font-size:13px; padding: 7px 9px; background:var(--bg);
    color:var(--text); border:1px solid var(--border); border-radius: 8px;
  }
  .modal-actions { display:flex; justify-content:flex-end; gap:8px; margin-top: 14px; }
  .modal-actions button {
    font: inherit; padding: 8px 16px; border-radius: 8px; border: 1px solid var(--border);
    cursor:pointer;
  }
  .modal-actions .primary { background: var(--accent); color:#fff; border-color: var(--accent); }
  .modal-actions .primary:hover { background: var(--accent-strong); }
  .modal-actions .secondary { background: var(--bg); color:var(--text); }
  .modal-actions .secondary:hover { background: var(--bg-subtle); }
  .modal-link {
    font: inherit; font-size: 12px; background:none; border:none; color: var(--accent-strong);
    cursor:pointer; padding: 0; text-align:start;
  }

  /* Narrow screens (a phone, a split pane): the sidebar overlays instead
     of squeezing beside the chat. */
  @media (max-width: 700px) {
    /* An overlay that slides in/out (transform), not a width collapse --
       width:0 on a position:fixed element has nothing to collapse
       relative to, so the same transition the desktop sidebar uses
       would do nothing visible here. */
    .sidebar {
      position:fixed; inset-block:56px 0; inset-inline-start:0; z-index: 10;
      width: min(85vw, 320px); transition: transform .2s ease; opacity: 1;
    }
    .layout.sidebar-hidden .sidebar {
      width: min(85vw, 320px); padding: 12px; opacity: 1; border-inline-end-color: var(--border);
      transform: translateX(-100%);
    }
    .sidebar-resizer { display: none; }
    .chat { padding: 16px 12px 8px; }
    .ask { padding: 10px 12px 14px; }
    .msg-user { max-width: 88%; }
  }
