/* ══════════════════════════════════════════════════════════════════════
   Octant design system
   Every colour, size, and duration in this file comes from a token
   declared in the two :root blocks. Nothing below them declares a raw
   colour. See DESIGN.md for the rules.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── display face ────────────────────────────────────────────────────
   Self-hosted, one variable file, no third-party requests. See
   fonts/LICENSE.txt. */
@font-face {
  font-family: "Octant Display";
  src: url("../fonts/octant-display.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}

/* ─── design tokens ───────────────────────────────────────────────────
   Colour is declared once per token with light-dark(), so the two themes
   can never drift apart the way two duplicated palette blocks do. The
   light value comes first.

   Light is NOT an inversion of dark. Brass reads 8.3:1 on ink but only
   2.0:1 on cream, and every semantic hue fails on white, so text-bearing
   colours have a separate light value. Fills keep the brand brass in both
   themes; only text darkens. That is what --oct-accent (fill) versus
   --oct-accent-fg (text) is for.

   Theme resolution: follows the OS by default, and [data-theme] on <html>
   overrides it in either direction. --oct-* colours track color-scheme,
   so flipping that one property flips the whole system. */
:root {
  color-scheme: light dark;

  /* ── ground and type ── */
  --oct-bg:           light-dark(#f2f1ed, #14130f);
  --oct-surface:      light-dark(#e6e5e0, #1c1b16);
  --oct-surface-warm: light-dark(#ebeae5, #232219);
  --oct-fg:           light-dark(#26251e, #f2f1ed);
  --oct-fg-2:         light-dark(rgba(38, 37, 30, 0.9),  rgba(242, 241, 237, 0.9));
  --oct-muted:        light-dark(rgba(38, 37, 30, 0.68), rgba(242, 241, 237, 0.58));
  --oct-meta:         light-dark(rgba(38, 37, 30, 0.62), rgba(242, 241, 237, 0.45));
  --oct-border:       light-dark(rgba(38, 37, 30, 0.14), rgba(242, 241, 237, 0.13));
  --oct-border-soft:  light-dark(rgba(38, 37, 30, 0.07), rgba(242, 241, 237, 0.07));

  /* ── brand ──
     accent is the fill and stays brass in both themes so the brand does
     not change colour. accent-fg is the same brand as *text*, darkened
     for light where brass on cream is unreadable. */
  --oct-accent:    #d9a441;
  --oct-accent-fg: light-dark(#8a6218, #d9a441);
  --oct-accent-on: #14130f;
  --oct-accent-hover:  light-dark(color-mix(in oklab, var(--oct-accent), black 10%),
                                  color-mix(in oklab, var(--oct-accent), white 12%));
  --oct-accent-active: light-dark(color-mix(in oklab, var(--oct-accent), black 18%),
                                  color-mix(in oklab, var(--oct-accent), white 20%));

  /* ── semantic ──
     The raw hues are the mixing source and are never used directly; each
     theme carries its own foreground, darkened for light and lifted for
     dark until it clears AA on that theme's ground. */
  --oct-success: #1f8a65;
  --oct-warn:    #eab308;
  --oct-danger:  #cf2d56;
  --oct-success-fg: light-dark(#0f6144, color-mix(in oklab, var(--oct-success), white 34%));
  --oct-warn-fg:    light-dark(#6f5300, color-mix(in oklab, var(--oct-warn),    white 12%));
  --oct-danger-fg:  light-dark(#a8102f, color-mix(in oklab, var(--oct-danger),  white 38%));

  /* ── tints ──
     Light grounds need a stronger mix to register at all. */
  --oct-accent-soft:  light-dark(color-mix(in oklab, var(--oct-accent)  30%, transparent),
                                 color-mix(in oklab, var(--oct-accent)  16%, transparent));
  --oct-success-soft: light-dark(color-mix(in oklab, var(--oct-success) 24%, transparent),
                                 color-mix(in oklab, var(--oct-success) 18%, transparent));
  --oct-warn-soft:    light-dark(color-mix(in oklab, var(--oct-warn)    30%, transparent),
                                 color-mix(in oklab, var(--oct-warn)    15%, transparent));
  --oct-danger-soft:  light-dark(color-mix(in oklab, var(--oct-danger)  22%, transparent),
                                 color-mix(in oklab, var(--oct-danger)  20%, transparent));
  --oct-fg-soft:      light-dark(color-mix(in oklab, var(--oct-fg) 6%, transparent),
                                 color-mix(in oklab, var(--oct-fg) 7%, transparent));

  /* the hero canvas draws lines in this colour; JS resolves it rather
     than reading raw channel triplets */
  --oct-grid-line: light-dark(#26251e, #f2f1ed);

  /* ── elevation and focus ── */
  --oct-elev-flat: none;
  --oct-elev-ring: 0 0 0 1px var(--oct-border);
  --oct-elev-raised:
    light-dark(0 28px 70px rgba(38, 37, 30, 0.13), 0 28px 70px rgba(0, 0, 0, 0.5)),
    light-dark(0 14px 32px rgba(38, 37, 30, 0.10), 0 14px 32px rgba(0, 0, 0, 0.36)),
    0 0 0 1px var(--oct-border);
  --oct-focus-ring: 0 0 0 2px var(--oct-bg), 0 0 0 4px var(--oct-accent-fg);

  /* ── type ── */
  --oct-font-display: "Octant Display", system-ui, "Helvetica Neue", Helvetica, Arial, sans-serif;
  --oct-font-body: "Iowan Old Style", "Palatino Linotype", "URW Palladio L", P052, ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
  --oct-font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  /* The fourth face, and the one exception to "three faces, three jobs".
     The body serif is right for a page you read once; a transcript is a
     working surface read for hours, interleaved with code and UI chrome,
     and a serif fights that. This is the neutral UI sans both Claude Code
     and Codex use. Set it to var(--oct-font-body) to go back to serif —
     it is the single token that decides. */
  --oct-font-transcript: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --oct-text-xs: 11px;
  --oct-text-sm: 14px;
  --oct-text-base: 16px;
  --oct-text-lg: 19px;
  --oct-text-xl: 22px;
  --oct-text-2xl: 26px;
  --oct-text-3xl: 36px;
  --oct-text-4xl: 72px;
  --oct-fs-h1: clamp(56px, 11vw, 132px);
  --oct-fs-h2: clamp(30px, 3.6vw, 44px);

  --oct-leading-body: 1.5;
  --oct-leading-tight: 1.10;
  --oct-leading-heading: 1.14;
  --oct-leading-snug: 1.3;
  --oct-leading-code: 1.7;

  --oct-weight-display: 500;
  --oct-weight-strong: 600;

  /* tracking: negative tightens display type as it grows, positive opens
     mono labels. Every letter-spacing in the system comes from here. */
  --oct-tracking-stat: -0.04em;
  --oct-tracking-tight: -0.025em;
  --oct-tracking-snug: -0.01em;
  --oct-tracking-wordmark: 0.04em;
  --oct-tracking-wide: 0.06em;
  --oct-tracking-wider: 0.14em;
  --oct-tracking-widest: 0.3em;

  /* ── space and shape ── */
  --oct-space-1: 4px;
  --oct-space-2: 8px;
  --oct-space-3: 12px;
  --oct-space-4: 16px;
  --oct-space-5: 20px;
  --oct-space-6: 24px;
  --oct-space-8: 32px;
  --oct-space-12: 48px;
  --oct-section-y-desktop: 80px;
  --oct-section-y-tablet: 48px;
  --oct-section-y-phone: 32px;
  --oct-radius-sm: 8px;
  --oct-radius-md: 10px;
  --oct-radius-lg: 12px;
  --oct-radius-pill: 9999px;

  --oct-motion-fast: 150ms;
  --oct-motion-base: 200ms;
  --oct-ease-standard: ease;

  --oct-container-max: 1200px;
  --oct-container-gutter-desktop: 24px;
  --oct-container-gutter-tablet: 16px;
  --oct-container-gutter-phone: 12px;
  --oct-gutter: var(--oct-container-gutter-desktop);

  /* controls: this is the real rendered height, not a floor. Components
     carry no vertical padding, so changing these actually resizes them. */
  --oct-control-height: 44px;
  --oct-control-height-sm: 34px;
  --oct-disabled-opacity: 0.45;

  --oct-icon-sm: 16px;
  --oct-icon-md: 19px;
  --oct-icon-lg: 22px;
  --oct-icon-mark: 38px;

  --oct-avatar-sm: 24px;
  --oct-avatar-md: 32px;
  --oct-avatar-lg: 44px;

  /* the wash behind a modal. Dark needs more of it because the ground is
     already dark and a weak scrim leaves the page competing with the dialog. */
  --oct-scrim: light-dark(rgba(38, 37, 30, 0.38), rgba(0, 0, 0, 0.64));
  /* the travelling highlight in a skeleton */
  --oct-sheen: light-dark(rgba(38, 37, 30, 0.07), rgba(242, 241, 237, 0.08));
  /* one indent step in a file tree */
  --oct-tree-indent: 14px;

  /* ── chart series ──────────────────────────────────────────────────
     Six hues, each picked twice so it holds against both grounds. Series
     one is the brass, so a single-series chart reads as part of the
     product rather than as a foreign widget.

     Every one clears 3:1 against its own background, which is the bar
     for a graphical object under WCAG 1.4.11. That is enough for a mark
     but not for a label, so a series label is drawn in --oct-fg-2 and
     the colour only appears in its swatch.

     Colour is never the only difference: lines carry --oct-dash-1..6 as
     well, so the chart still separates in greyscale or for a viewer who
     cannot tell two hues apart. */
  --oct-series-1: light-dark(#8a6218, #d9a441);
  --oct-series-2: light-dark(#0f6f68, #45c8bc);
  --oct-series-3: light-dark(#a8324a, #f4809a);
  --oct-series-4: light-dark(#5b4bb0, #ab98f2);
  --oct-series-5: light-dark(#41761c, #93cb58);
  --oct-series-6: light-dark(#1e5fae, #74b0f3);

  --oct-dash-1: none;
  --oct-dash-2: 6 3;
  --oct-dash-3: 2 3;
  --oct-dash-4: 9 3 2 3;
  --oct-dash-5: 1 4;
  --oct-dash-6: 12 4;

  /* chart chrome. The grid is deliberately fainter than --oct-border-soft:
     it repeats a dozen times inside one small frame, and at border weight
     it reads as a table rather than as a backdrop. */
  --oct-chart-grid: light-dark(rgba(38, 37, 30, 0.10), rgba(242, 241, 237, 0.09));
  --oct-chart-axis: light-dark(rgba(38, 37, 30, 0.22), rgba(242, 241, 237, 0.20));
  --oct-chart-h: 180px;

  /* canvas document. The measure is narrower than the prose measure —
     a canvas interleaves blocks, and a long line between two charts is
     harder to re-enter than a long line in running text. */
  --oct-canvas-measure: 62ch;
  --oct-canvas-gutter: 26px;

  /* app shell. The sidebar width is a token rather than a literal because
     the drag handle writes to it — resizing and the default come from the
     same place, so a dragged sidebar and a fresh one behave identically. */
  --oct-sidebar-w: 236px;
  --oct-sidebar-w-compact: 52px;
  --oct-sidebar-w-min: 180px;
  --oct-sidebar-w-max: 420px;
  --oct-titlebar-h: 44px;
  --oct-statusbar-h: 26px;
  --oct-dock-w: 320px;

  --oct-z-base: 1;
  --oct-z-sticky: 20;
  --oct-z-overlay: 40;
  --oct-z-modal: 60;
  --oct-z-toast: 80;

  /* Media queries cannot read custom properties, so the literals below
     are repeated by hand in @media rules. These exist so JS and future
     components read the same numbers. Only these three are legal. */
  --oct-bp-sm: 560px;
  --oct-bp-md: 720px;
  --oct-bp-lg: 920px;

  /* ─── mobile ────────────────────────────────────────────────────────
     The phone is not the desktop at a narrower width. It has its own
     material — translucent glass over an atmospheric ground — its own
     radii, and a touch target that sets the floor for everything.

     Glass earns its place on the phone and nowhere else. On desktop this
     system draws elevation as a hairline ring and no shadow, because a
     dense information surface wants edges, not depth. A phone shows one
     thing at a time over a photographic ground, and there translucency
     is what tells you a panel is floating rather than painted on. Both
     are correct for their surface; neither should be exported to the
     other.

     Five thicknesses, matching the app's own recipes. Blur radius and
     fill opacity move together — a thicker glass is both more opaque and
     more diffuse, which is what keeps text on it readable as the panel
     carries more weight. */
  --oct-glass-thin:    light-dark(rgba(255, 255, 255, 0.28), rgba(38, 37, 30, 0.28));
  --oct-glass-regular: light-dark(rgba(255, 255, 255, 0.42), rgba(38, 37, 30, 0.40));
  --oct-glass-thick:   light-dark(rgba(255, 255, 255, 0.58), rgba(45, 43, 36, 0.55));
  --oct-glass-chrome:  light-dark(rgba(255, 255, 255, 0.50), rgba(45, 43, 36, 0.48));
  --oct-glass-blur-thin:    32px;
  --oct-glass-blur-regular: 48px;
  --oct-glass-blur-thick:   62px;
  --oct-glass-blur-chrome:  40px;
  --oct-glass-stroke:        light-dark(rgba(38, 37, 30, 0.10), rgba(242, 241, 237, 0.14));
  --oct-glass-stroke-strong: light-dark(rgba(38, 37, 30, 0.16), rgba(242, 241, 237, 0.22));
  --oct-glass-highlight:     light-dark(rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.10));
  /* Laid over the atmosphere so the backdrop has a known floor. 66% is
     the measured minimum that keeps every ink-weight text role legible
     over any image, on glass or bare; see .phone-atmos::after. */
  --oct-glass-floor:         light-dark(rgba(242, 241, 237, 0.66), rgba(20, 19, 15, 0.66));

  /* Phone radii are far larger than desktop's 8/10/12. A small radius on
     a 390px-wide panel reads as a hard corner; the phone's whole chrome
     is one continuous soft rectangle. */
  --oct-radius-bubble: 22px;
  --oct-radius-card-m: 26px;
  --oct-radius-sheet:  30px;

  /* Touch. 44px is the floor for anything tappable, which is why the
     desktop control height is already 44px — one number, not two. */
  --oct-touch-min: 44px;

  /* Stage pastels: what the agent is doing right now, at a glance, on a
     surface too small for a label. Deliberately desaturated — they sit
     behind text and must never compete with it. */
  --oct-stage-thinking: light-dark(#ffd6c9, #5c2e22);
  --oct-stage-reading:  light-dark(#b8e8d4, #1f4a3a);
  --oct-stage-editing:  light-dark(#bfd9ff, #243b5c);
  --oct-stage-grepping: light-dark(#d9cff5, #3a2f55);
  --oct-stage-done:     light-dark(#f2dfa0, #4a4020);

  /* The device viewport this system draws into. iPhone logical points. */
  --oct-phone-w: 390px;
  --oct-phone-h: 844px;
}

/* An explicit choice on <html> overrides the OS. Because every colour is
   a light-dark(), flipping color-scheme is the entire theme switch. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"]  { color-scheme: dark; }

/* ─── reset & base ──────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--oct-bg);
  color: var(--oct-fg);
  font-family: var(--oct-font-body);
  font-size: var(--oct-text-base);
  line-height: var(--oct-leading-body);
  text-rendering: optimizeLegibility;
  -webkit-font-smoothing: antialiased;
  font-synthesis: none;
  overflow-x: hidden;
}
img, svg, canvas { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; cursor: pointer; }
p { text-wrap: pretty; }
h1, h2, h3, h4 { text-wrap: balance; }
:focus-visible { outline: none; box-shadow: var(--oct-focus-ring); border-radius: var(--oct-radius-sm); }

/* ─── layout primitives ─────────────────────────────────────────────── */
.container { max-width: var(--oct-container-max); margin-inline: auto; padding-inline: var(--oct-gutter); }
.section { padding-block: clamp(var(--oct-section-y-phone), 8vw, var(--oct-section-y-desktop)); }
.section + .section { border-top: 1px solid var(--oct-border); }
.stack { display: flex; flex-direction: column; }
.stack > * + * { margin-top: var(--oct-space-5); }
.row { display: flex; align-items: center; gap: var(--oct-space-5); }
.row-wrap { flex-wrap: wrap; }
.row-between { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-5); }
.grid-2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--oct-space-8); }
.grid-3 { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--oct-space-8); }
.grid-4 { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: var(--oct-space-5); }
.grid-2-1 { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); gap: var(--oct-space-12); align-items: start; }
.grid-1-2 { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); gap: var(--oct-space-12); align-items: center; }
@media (max-width: 920px) {
  .grid-2, .grid-3, .grid-4, .grid-2-1, .grid-1-2 { grid-template-columns: minmax(0, 1fr); }
}

/* ─── type ──────────────────────────────────────────────────────────── */
.h1, h1 { font-family: var(--oct-font-display); font-size: var(--oct-fs-h1); line-height: var(--oct-leading-tight); letter-spacing: var(--oct-tracking-tight); margin: 0; font-weight: var(--oct-weight-display); }
.h2, h2 { font-family: var(--oct-font-display); font-size: var(--oct-fs-h2); line-height: var(--oct-leading-heading); letter-spacing: var(--oct-tracking-tight); margin: 0; font-weight: var(--oct-weight-display); }
.h3, h3 { font-family: var(--oct-font-display); font-size: var(--oct-text-xl); font-weight: var(--oct-weight-strong); line-height: var(--oct-leading-snug); letter-spacing: var(--oct-tracking-snug); margin: 0; }
.h4, h4 { font-family: var(--oct-font-display); font-size: var(--oct-text-lg); font-weight: var(--oct-weight-strong); line-height: var(--oct-leading-snug); letter-spacing: var(--oct-tracking-snug); margin: 0; }
.lead { font-size: var(--oct-text-lg); line-height: 1.5; color: var(--oct-muted); max-width: 56ch; margin: 0; }
.prose { max-width: 68ch; color: var(--oct-fg-2); }
.prose p { margin: 0 0 var(--oct-space-5); }
.eyebrow {
  font-family: var(--oct-font-mono);
  font-size: var(--oct-text-xs);
  letter-spacing: var(--oct-tracking-wider);
  text-transform: uppercase;
  color: var(--oct-accent-fg);
  margin: 0 0 var(--oct-space-5);
}
.meta { font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); color: var(--oct-muted); }
.num  { font-family: var(--oct-font-mono); font-variant-numeric: tabular-nums; }
.link { color: var(--oct-fg); text-decoration: underline; text-decoration-color: var(--oct-border); text-underline-offset: 3px; transition: text-decoration-color var(--oct-motion-fast) var(--oct-ease-standard); }
.link:hover { text-decoration-color: var(--oct-accent-fg); }
.link-accent { color: var(--oct-accent-fg); text-decoration: underline; text-underline-offset: 3px; }

/* ─── chrome ────────────────────────────────────────────────────────── */
.topnav {
  position: sticky; top: 0; z-index: var(--oct-z-sticky);
  background: color-mix(in oklab, var(--oct-bg) 78%, transparent);
  backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--oct-border-soft);
}
.topnav-inner { display: flex; align-items: center; justify-content: space-between; padding-block: var(--oct-space-3); }
.topnav .logo {
  display: inline-flex; align-items: center; gap: 10px;
  /* The wordmark is the one thing in the bar that must not be squeezed. As a
     flex item it shrinks by default, and because it is nowrap it does not get
     narrower when it shrinks — it keeps its glyphs and slides them under
     whatever sits beside it. The box measures fine and the paint is wrong,
     which is why this needs saying out loud. */
  flex: 0 0 auto; white-space: nowrap;
  font-family: var(--oct-font-display); font-size: var(--oct-text-lg);
  font-weight: var(--oct-weight-display); letter-spacing: var(--oct-tracking-wordmark);
}
.topnav .logo svg { width: var(--oct-icon-lg); height: var(--oct-icon-lg); color: var(--oct-fg); }
.pagefoot { padding-block: var(--oct-space-12); color: var(--oct-muted); font-size: var(--oct-text-sm); border-top: 1px solid var(--oct-border); }
.pagefoot .row-between { flex-wrap: wrap; gap: var(--oct-space-5); }
.pagefoot a:hover { color: var(--oct-fg); text-decoration: underline; text-underline-offset: 3px; }

/* ─── buttons ───────────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--oct-space-2);
  min-height: var(--oct-control-height);
  padding-block: 0;
  padding-inline: var(--oct-space-5);
  border-radius: var(--oct-radius-sm);
  border: 1px solid transparent;
  font-family: var(--oct-font-display);
  font-size: var(--oct-text-sm);
  font-weight: var(--oct-weight-display);
  transition: background var(--oct-motion-fast) var(--oct-ease-standard),
              border-color var(--oct-motion-fast) var(--oct-ease-standard),
              color var(--oct-motion-fast) var(--oct-ease-standard),
              transform 60ms var(--oct-ease-standard);
}
.btn:active { transform: translateY(1px); }
.btn-sm { min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-4); font-size: var(--oct-text-xs); }
.btn-block { display: flex; width: 100%; }
.btn-primary { background: var(--oct-accent); color: var(--oct-accent-on); border-color: var(--oct-accent); }
.btn-primary:hover { background: var(--oct-accent-hover); border-color: var(--oct-accent-hover); color: var(--oct-accent-on); }
.btn-primary:active { background: var(--oct-accent-active); border-color: var(--oct-accent-active); }
.btn-secondary { background: transparent; color: var(--oct-fg); border-color: var(--oct-border); }
.btn-secondary:hover { background: var(--oct-surface-warm); border-color: var(--oct-fg); color: var(--oct-fg); }
.btn-ghost { background: transparent; color: var(--oct-muted); border-color: transparent; padding-inline: var(--oct-space-2); }
.btn-ghost:hover { color: var(--oct-fg); }
.btn-danger { background: transparent; color: var(--oct-danger-fg); border-color: color-mix(in oklab, var(--oct-danger) 45%, transparent); }
.btn-danger:hover { background: var(--oct-danger-soft); border-color: var(--oct-danger-fg); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: var(--oct-disabled-opacity); pointer-events: none; }
.btn-arrow::after { content: '→'; transition: transform var(--oct-motion-fast) var(--oct-ease-standard); }
.btn-arrow:hover::after { transform: translateX(3px); }
.btn-group { display: inline-flex; gap: var(--oct-space-3); flex-wrap: wrap; }

/* ─── card / surface ────────────────────────────────────────────────── */
.card { background: var(--oct-surface); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); padding: var(--oct-space-8); }
.card-flat { background: transparent; border: 0; padding: 0; }
.card-rule { background: transparent; border: 0; border-top: 1px solid var(--oct-fg); padding: var(--oct-space-6) 0 0; border-radius: 0; }
.card-raised { box-shadow: var(--oct-elev-raised); }
.card-tight { padding: var(--oct-space-5); }
.card-head { display: flex; align-items: baseline; justify-content: space-between; gap: var(--oct-space-4); margin-bottom: var(--oct-space-4); }
.card-interactive { transition: border-color var(--oct-motion-fast) var(--oct-ease-standard), background var(--oct-motion-fast) var(--oct-ease-standard); }
.card-interactive:hover { border-color: color-mix(in oklab, var(--oct-fg) 26%, transparent); background: var(--oct-surface-warm); }

/* ─── feature cell ──────────────────────────────────────────────────── */
.feature .icon-mark { margin-bottom: var(--oct-space-5); }
.feature h3 { margin-bottom: var(--oct-space-2); }
.feature p  { margin: 0; color: var(--oct-muted); font-size: var(--oct-text-base); }

/* ─── stat ──────────────────────────────────────────────────────────── */
.stat .stat-num { font-family: var(--oct-font-display); font-size: clamp(56px, 8vw, 96px); line-height: 0.95; letter-spacing: var(--oct-tracking-stat); color: var(--oct-accent-fg); font-weight: 500; }
.stat .stat-label { color: var(--oct-muted); font-size: var(--oct-text-sm); margin-top: var(--oct-space-2); max-width: 24ch; }
.stat .stat-unit  { font-size: 0.5em; opacity: 0.7; margin-left: 2px; }

/* ─── quote ─────────────────────────────────────────────────────────── */
.quote { font-family: var(--oct-font-display); font-size: clamp(24px, 2.6vw, 32px); line-height: var(--oct-leading-snug); letter-spacing: var(--oct-tracking-snug); max-width: 28ch; }
.quote-author { color: var(--oct-muted); font-size: var(--oct-text-sm); margin-top: var(--oct-space-5); }
.quote-mark { font-family: var(--oct-font-display); font-size: 140px; line-height: 0.7; color: var(--oct-accent-fg); opacity: 0.18; margin-bottom: -28px; }

/* ─── pill / tag / badge ────────────────────────────────────────────── */
.pill { display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; background: var(--oct-accent-soft); color: var(--oct-accent-fg); border-radius: var(--oct-radius-pill); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; }
.tag {
  display: inline-flex; align-items: center; gap: var(--oct-space-2);
  padding: 5px 12px;
  background: var(--oct-border-soft);
  color: var(--oct-fg-2);
  border: 1px solid var(--oct-border);
  border-radius: var(--oct-radius-pill);
  font-family: var(--oct-font-mono);
  font-size: var(--oct-text-xs);
  letter-spacing: var(--oct-tracking-wider);
  text-transform: uppercase;
}
.tag .dot { width: 5px; height: 5px; border-radius: var(--oct-radius-pill); background: var(--oct-accent-fg); }
.badge { display: inline-flex; align-items: center; gap: 6px; padding: 3px 9px; border-radius: var(--oct-radius-sm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; background: var(--oct-fg-soft); color: var(--oct-fg-2); }
.badge-ok     { background: var(--oct-success-soft); color: var(--oct-success-fg); }
.badge-warn   { background: var(--oct-warn-soft);    color: var(--oct-warn-fg); }
.badge-danger { background: var(--oct-danger-soft);  color: var(--oct-danger-fg); }
.badge-accent { background: var(--oct-accent-soft);  color: var(--oct-accent-fg); }

/* ─── callout ───────────────────────────────────────────────────────── */
.callout { display: flex; gap: var(--oct-space-4); padding: var(--oct-space-5); border: 1px solid var(--oct-border); border-left-width: 2px; border-radius: var(--oct-radius-md); background: var(--oct-surface); color: var(--oct-fg-2); font-size: var(--oct-text-sm); }
.callout p { margin: 0; }
.callout-title { font-family: var(--oct-font-display); font-weight: var(--oct-weight-strong); color: var(--oct-fg); margin-bottom: 2px; }
.callout-accent { border-left-color: var(--oct-accent-fg); }
.callout-ok     { border-left-color: var(--oct-success-fg); }
.callout-warn   { border-left-color: var(--oct-warn-fg); }
.callout-danger { border-left-color: var(--oct-danger-fg); }

/* ─── form ──────────────────────────────────────────────────────────── */
.field { display: flex; flex-direction: column; gap: 6px; }
.field label { font-size: var(--oct-text-sm); color: var(--oct-muted); }
.field-hint { font-size: var(--oct-text-xs); color: var(--oct-meta); font-family: var(--oct-font-mono); }
.field-error { font-size: var(--oct-text-xs); color: var(--oct-danger-fg); font-family: var(--oct-font-mono); }
.input, .textarea, .select {
  width: 100%;
  min-height: var(--oct-control-height);
  padding-block: 0;
  padding-inline: var(--oct-space-4);
  border: 1px solid var(--oct-border);
  border-radius: var(--oct-radius-sm);
  background: var(--oct-surface);
  color: var(--oct-fg);
  font-family: var(--oct-font-body);
  font-size: var(--oct-text-base);
}
.input::placeholder, .textarea::placeholder { color: var(--oct-muted); }
.input:hover, .textarea:hover, .select:hover { border-color: color-mix(in oklab, var(--oct-fg) 26%, transparent); }
.input:focus-visible, .textarea:focus-visible, .select:focus-visible { border-color: var(--oct-accent-fg); box-shadow: var(--oct-focus-ring); }
.input[aria-invalid="true"] { border-color: var(--oct-danger-fg); }
.input[disabled], .textarea[disabled], .select[disabled] { opacity: var(--oct-disabled-opacity); cursor: not-allowed; }
.textarea { min-height: 96px; padding-block: var(--oct-space-3); resize: vertical; line-height: var(--oct-leading-body); }
.select { appearance: none; padding-inline-end: var(--oct-space-8); }
.check { display: inline-flex; align-items: center; gap: var(--oct-space-3); font-size: var(--oct-text-sm); color: var(--oct-fg-2); cursor: pointer; }
.check input { position: relative; appearance: none; width: 18px; height: 18px; margin: 0; flex: 0 0 auto; border: 1px solid var(--oct-border); border-radius: 5px; background: var(--oct-surface); display: grid; place-items: center; transition: background var(--oct-motion-fast) var(--oct-ease-standard), border-color var(--oct-motion-fast) var(--oct-ease-standard); }
/* 18px is the box that reads right beside a line of text; 24 is the
   floor WCAG 2.5.8 sets for a pointer target. Grow the target past the
   paint rather than the paint itself. Absolute, so it stays out of the
   grid above and cannot displace the checkmark drawn in ::after. */
.check input::before { content: ''; position: absolute; inset: -4px; }
.check input[type="radio"] { border-radius: var(--oct-radius-pill); }
.check input:checked { background: var(--oct-accent); border-color: var(--oct-accent); }
.check input:checked::after { content: ''; width: 9px; height: 5px; border-left: 2px solid var(--oct-accent-on); border-bottom: 2px solid var(--oct-accent-on); transform: rotate(-45deg) translate(1px, -1px); }
.check input[type="radio"]:checked::after { content: ''; width: 6px; height: 6px; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-accent-on); transform: none; }
/* qualified with input so these win against `.check input` above, which
   would otherwise size the switch like a checkbox */
input.switch { position: relative; width: 38px; height: 22px; flex: 0 0 auto; appearance: none; margin: 0; border-radius: var(--oct-radius-pill); background: var(--oct-border); border: 0; display: block; cursor: pointer; transition: background var(--oct-motion-base) var(--oct-ease-standard); }
/* 22px is the height that reads right beside a line of text, and it is
   two short of the 24px floor WCAG 2.5.8 sets for a pointer target.
   Rather than inflate the control until it looks like a button, the
   target is grown past the paint: this is part of the input's hit
   region, is invisible, and costs no layout. Spacing alone would also
   satisfy the rule, but only as long as nothing is ever placed beside
   it — a guarantee the markup cannot make and this can. */
input.switch::before { content: ''; position: absolute; inset: -1px 0; }
input.switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 16px; height: 16px; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-fg); transform: none; transition: transform var(--oct-motion-base) var(--oct-ease-standard); }
input.switch:checked { background: var(--oct-accent); border-color: transparent; }
input.switch:checked::after { content: ''; width: 16px; height: 16px; border: 0; border-radius: var(--oct-radius-pill); transform: translateX(16px); background: var(--oct-accent-on); }

/* ─── table ───────────────────────────────────────────────────────────
   Wrap every table in .table-scroll. Body sets overflow-x: hidden, so an
   unwrapped table does not scroll on a narrow screen — it silently clips
   its right-hand columns. */
.table-scroll { overflow-x: auto; overscroll-behavior-x: contain; }
.table-scroll .ds-table { min-width: 440px; }
.ds-table { width: 100%; border-collapse: collapse; font-size: var(--oct-text-sm); }
.ds-table th, .ds-table td { padding: var(--oct-space-3) var(--oct-space-4); text-align: left; border-bottom: 1px solid var(--oct-border); }
.ds-table th { color: var(--oct-muted); font-weight: 500; font-family: var(--oct-font-mono); font-size: 12px; letter-spacing: var(--oct-tracking-wordmark); text-transform: uppercase; }
.ds-table tbody tr:hover { background: var(--oct-fg-soft); }
.ds-table .num-col { font-family: var(--oct-font-mono); font-variant-numeric: tabular-nums; text-align: right; }

/* ══════════════════════════════════════════════════════════════════════
   Data surfaces

   .ds-table is a presentation table — a short, hand-written table in a
   document. These are for data you did not write: a decoded CSV, a
   worksheet, a query result. Different job, different component.

   The rule that makes both readable: numbers are tabular and right-
   aligned, headers stick, and the cell never wraps unless you ask.
   ══════════════════════════════════════════════════════════════════════ */

.datawrap { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); overflow: auto; overscroll-behavior: contain; scrollbar-width: thin; max-height: 420px; }
.datatable { width: 100%; border-collapse: separate; border-spacing: 0; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
/* the header stays put through a long scroll, which is the whole point */
.datatable thead th { position: sticky; top: 0; z-index: var(--oct-z-base); padding: var(--oct-space-2) var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); color: var(--oct-muted); font-weight: 500; letter-spacing: var(--oct-tracking-wordmark); text-transform: uppercase; text-align: left; white-space: nowrap; }
.datatable td { padding: var(--oct-space-2) var(--oct-space-3); border-bottom: 1px solid var(--oct-border-soft); color: var(--oct-fg-2); white-space: nowrap; }
.datatable tbody tr:hover td { background: var(--oct-fg-soft); }
.datatable tbody tr[aria-selected="true"] td { background: var(--oct-accent-soft); }
/* numbers line up under each other or they cannot be compared */
.datatable .num { text-align: right; font-variant-numeric: tabular-nums; }
.datatable .nil { color: var(--oct-meta); }
.datatable .wrap { white-space: normal; min-width: 220px; }
/* a row-number gutter that survives horizontal scrolling */
.datatable .rownum { position: sticky; left: 0; z-index: var(--oct-z-base); width: 1%; padding-inline: var(--oct-space-3); background: var(--oct-surface); border-right: 1px solid var(--oct-border); color: var(--oct-meta); text-align: right; font-variant-numeric: tabular-nums; user-select: none; }
.datatable thead .rownum { z-index: 2; background: var(--oct-surface-warm); }
/* sortable header: the arrow is the affordance, the aria-sort is the truth */
.datatable th.sortable { cursor: pointer; }
.datatable th.sortable:hover { color: var(--oct-fg); }
.datatable th[aria-sort] { color: var(--oct-fg); }
.datatable th .sortmark { display: inline-block; margin-left: var(--oct-space-1); opacity: 0; }
.datatable th[aria-sort] .sortmark { opacity: 1; }
.datatable th[aria-sort="descending"] .sortmark { transform: rotate(180deg); }

/* ─── worksheet ───────────────────────────────────────────────────────
   A spreadsheet grid: column letters across, row numbers down, and a
   corner that pins to both. Cells are terse because a sheet is scanned. */
.sheet { border-collapse: separate; border-spacing: 0; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.sheet th, .sheet td { border-right: 1px solid var(--oct-border-soft); border-bottom: 1px solid var(--oct-border-soft); padding: 3px var(--oct-space-2); white-space: nowrap; }
.sheet thead th { position: sticky; top: 0; z-index: 2; background: var(--oct-surface-warm); color: var(--oct-meta); font-weight: 500; text-align: center; min-width: 84px; }
.sheet tbody th { position: sticky; left: 0; z-index: 1; background: var(--oct-surface-warm); color: var(--oct-meta); font-weight: 500; text-align: right; min-width: 34px; }
.sheet thead th:first-child { position: sticky; left: 0; z-index: 3; min-width: 34px; }
.sheet td { color: var(--oct-fg-2); background: var(--oct-bg); }
.sheet td.num { text-align: right; font-variant-numeric: tabular-nums; }
.sheet td.bool { color: var(--oct-accent-fg); }
.sheet td[aria-selected="true"] { outline: 1.5px solid var(--oct-accent-fg); outline-offset: -1.5px; }
/* worksheet tabs sit under the grid, the way a spreadsheet does */
.sheet-tabs { display: flex; gap: 2px; padding: var(--oct-space-2); border-top: 1px solid var(--oct-border); background: var(--oct-surface-warm); overflow-x: auto; scrollbar-width: thin; }
.sheet-tab { min-height: 24px; padding-inline: var(--oct-space-3); border: 1px solid transparent; border-radius: 5px; background: none; color: var(--oct-muted); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; }
.sheet-tab:hover { color: var(--oct-fg); }
.sheet-tab[aria-selected="true"] { border-color: var(--oct-border); background: var(--oct-surface); color: var(--oct-fg); }

/* ─── viewer chrome ───────────────────────────────────────────────────
   Shared by every preview: table, workbook, document, slides, PDF. The
   bar names the artefact and carries the controls; the notice states what
   the preview cannot show, which matters more than it looks. */
.viewer { display: flex; flex-direction: column; min-height: 0; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.viewer-bar { display: flex; align-items: center; gap: var(--oct-space-3); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); flex: 0 0 auto; }
.viewer-bar .viewer-gap { flex: 1; }
.viewer-body { flex: 1; min-height: 0; overflow: auto; scrollbar-width: thin; background: var(--oct-bg); }
/* honest about fidelity: a preview that silently drops formatting is worse
   than one that says so */
.viewer-notice { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-2) var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-warn-soft); color: var(--oct-warn-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
/* page-based viewers: PDF, slides */
.viewer-pager { display: flex; align-items: center; gap: var(--oct-space-2); font-variant-numeric: tabular-nums; }
.viewer-page { margin: var(--oct-space-4) auto; max-width: 780px; padding: var(--oct-space-8); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-sm); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); }
.viewer-slide { aspect-ratio: 16 / 9; display: grid; place-items: center; margin: var(--oct-space-4) auto; max-width: 640px; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-sm); background: var(--oct-surface); }
.viewer-filmstrip { display: flex; gap: var(--oct-space-2); padding: var(--oct-space-2); border-top: 1px solid var(--oct-border); background: var(--oct-surface-warm); overflow-x: auto; scrollbar-width: thin; }
.viewer-thumb { flex: 0 0 auto; width: 76px; aspect-ratio: 16 / 9; display: grid; place-items: center; border: 1px solid var(--oct-border); border-radius: 5px; background: var(--oct-bg); color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: 10px; }
.viewer-thumb[aria-selected="true"] { border-color: var(--oct-accent-fg); color: var(--oct-fg-2); }

/* ─── code ──────────────────────────────────────────────────────────── */
.code { font-family: var(--oct-font-mono); font-size: 0.9em; padding: 2px 6px; border-radius: 5px; background: var(--oct-fg-soft); color: var(--oct-fg-2); }
.codeblock { font-family: var(--oct-font-mono); font-size: 13px; line-height: var(--oct-leading-code); padding: var(--oct-space-5); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); color: var(--oct-fg-2); overflow-x: auto; margin: 0; }
.codeblock .dim { color: var(--oct-muted); }
.codeblock .ok  { color: var(--oct-success-fg); }
.kbd { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); padding: 3px 7px; border: 1px solid var(--oct-border); border-bottom-width: 2px; border-radius: 6px; background: var(--oct-surface-warm); color: var(--oct-fg-2); }

/* ─── tabs ──────────────────────────────────────────────────────────── */
.tabs { display: flex; gap: var(--oct-space-5); border-bottom: 1px solid var(--oct-border); }
.tab { padding: var(--oct-space-3) 0; margin-bottom: -1px; border-bottom: 1px solid transparent; color: var(--oct-muted); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); background: none; border-top: 0; border-inline: 0; transition: color var(--oct-motion-fast) var(--oct-ease-standard); }
.tab:hover { color: var(--oct-fg); }
.tab[aria-selected="true"] { color: var(--oct-fg); border-bottom-color: var(--oct-accent-fg); }

/* ─── segmented control ─────────────────────────────────────────────── */
.segmented { display: inline-flex; padding: 3px; gap: 2px; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-sm); background: var(--oct-surface); }
/* inline-flex, not the button default, so a segment holding an icon centres it
   instead of sitting it on the text baseline. Text segments are unaffected. */
.segment { display: inline-flex; align-items: center; justify-content: center; gap: var(--oct-space-2); min-height: 26px; padding-inline: var(--oct-space-3); border: 0; border-radius: 5px; background: transparent; color: var(--oct-muted); font-family: var(--oct-font-display); font-size: var(--oct-text-xs); transition: background var(--oct-motion-fast) var(--oct-ease-standard), color var(--oct-motion-fast) var(--oct-ease-standard); }
.segment:hover { color: var(--oct-fg); }
.segment[aria-pressed="true"] { background: var(--oct-fg-soft); color: var(--oct-fg); }

/* ─── meter ─────────────────────────────────────────────────────────── */
.meter { height: 4px; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); overflow: hidden; }
.meter > span { display: block; height: 100%; background: var(--oct-accent); border-radius: inherit; }

/* ─── run panel ─────────────────────────────────────────────────────── */
.runpanel { background: var(--oct-surface); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); overflow: hidden; }
.runpanel-bar { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-4); padding: var(--oct-space-3) var(--oct-space-4); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-muted); }
.runpanel-body { padding: var(--oct-space-5) var(--oct-space-4) var(--oct-space-6); font-family: var(--oct-font-mono); font-size: 13px; line-height: 1.7; }
.runline { display: grid; grid-template-columns: 62px minmax(0, 1fr); gap: var(--oct-space-4); padding: 3px 0 3px var(--oct-space-3); border-left: 2px solid transparent; }
.runline .k { color: var(--oct-meta); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; }
.runline .v { color: var(--oct-fg-2); overflow-wrap: anywhere; }
.runline .v .dim { color: var(--oct-muted); }
.runline .v .ok { color: var(--oct-success-fg); }
.runline.is-active { border-left-color: var(--oct-accent-fg); background: var(--oct-border-soft); }
@media (max-width: 560px) {
  .runline { grid-template-columns: 1fr; gap: 0; }
  .runline .k { font-size: 10px; }
}

/* ─── status list ───────────────────────────────────────────────────── */
.statuslist .status-head {
  display: flex; align-items: center; gap: var(--oct-space-2);
  margin: 0 0 var(--oct-space-4); padding-bottom: var(--oct-space-3);
  border-bottom: 1px solid var(--oct-border);
  font-family: var(--oct-font-mono); font-size: var(--oct-text-xs);
  letter-spacing: var(--oct-tracking-wider); text-transform: uppercase; color: var(--oct-fg-2);
}
.statuslist .dot { width: 6px; height: 6px; border-radius: var(--oct-radius-pill); background: var(--oct-accent-fg); flex: 0 0 auto; }
.statuslist .dot.ok  { background: var(--oct-success-fg); }
.statuslist .dot.mid { background: var(--oct-accent-fg); }
.statuslist ul { margin: 0; padding: 0; list-style: none; }
.statuslist li {
  color: var(--oct-muted); font-size: var(--oct-text-base);
  padding: var(--oct-space-2) 0;
  border-bottom: 1px solid var(--oct-border-soft);
  break-inside: avoid;
}
.statusgrid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); gap: var(--oct-space-12); align-items: start; }
.statuslist ul.cols-2 { columns: 2; column-gap: var(--oct-space-8); }
@media (max-width: 920px) {
  .statusgrid { grid-template-columns: minmax(0, 1fr); gap: var(--oct-space-8); }
  .statuslist ul.cols-2 { columns: 1; }
}

/* ─── log row ───────────────────────────────────────────────────────── */
.log-row { display: grid; grid-template-columns: 120px minmax(0, 1fr) 100px; gap: var(--oct-space-8); padding: 22px 0; border-top: 1px solid var(--oct-border); align-items: baseline; }
.log-row h3 { font-size: var(--oct-text-lg); }
.log-row .pull { text-align: right; }

/* ─── image placeholder ─────────────────────────────────────────────── */
.ph-img { background: var(--oct-surface); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); aspect-ratio: 16 / 10; display: grid; place-items: center; color: var(--oct-muted); font-family: var(--oct-font-mono); font-size: 12px; letter-spacing: var(--oct-tracking-wordmark); }
.ph-img.square { aspect-ratio: 1 / 1; }
.ph-img.portrait { aspect-ratio: 3 / 4; }
.ph-img.wide { aspect-ratio: 16 / 9; }

/* ─── dividers ──────────────────────────────────────────────────────── */
.rule { border: 0; border-top: 1px solid var(--oct-border); margin: 0; }
.rule-strong { border: 0; border-top: 1px solid var(--oct-fg); margin: 0; }
.principle-idx { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wider); color: var(--oct-meta); display: block; margin-bottom: var(--oct-space-3); }

/* ─── icons ───────────────────────────────────────────────────────────
   One sprite, referenced externally:

     <svg class="icon" aria-hidden="true"><use href="icons.svg#i-terminal" /></svg>

   Colour follows currentColor, so an icon inside a button, a link, or a
   danger callout takes that context's colour with no extra rule. Give it a
   size class, never a width in a component rule. An icon that carries
   meaning on its own needs a label; a decorative one needs aria-hidden. */
.icon { width: var(--oct-icon-md); height: var(--oct-icon-md); flex: 0 0 auto; display: inline-block; vertical-align: -0.2em; }
.icon-sm { width: var(--oct-icon-sm); height: var(--oct-icon-sm); }
.icon-lg { width: var(--oct-icon-lg); height: var(--oct-icon-lg); }
/* The bordered box an icon sits in — feature cells, empty states, anywhere a
   single icon has to hold its own visual weight. One definition, because
   three near-identical boxes with slightly different radii is exactly the
   drift a design system exists to prevent. */
.icon-mark { display: grid; place-items: center; width: var(--oct-icon-mark); height: var(--oct-icon-mark); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-sm); color: var(--oct-fg); }
/* deliberate override, and it shows up in the cascade audit: .icon carries a
   baseline nudge for text runs, and the mark centres with place-items, so the
   nudge would push it off-centre inside the box */
.icon-mark .icon { vertical-align: 0; }

/* ─── scrollbars ──────────────────────────────────────────────────────
   Two properties, no vendor pseudo-elements, so it themes itself. */
* { scrollbar-color: var(--oct-border) transparent; }
.scroll-thin { scrollbar-width: thin; }

/* ─── avatar ────────────────────────────────────────────────────────── */
.avatar { display: inline-grid; place-items: center; width: var(--oct-avatar-md); height: var(--oct-avatar-md); border-radius: var(--oct-radius-pill); border: 1px solid var(--oct-border); background: var(--oct-surface-warm); color: var(--oct-fg-2); font-family: var(--oct-font-display); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wordmark); text-transform: uppercase; overflow: hidden; flex: 0 0 auto; }
.avatar img { width: 100%; height: 100%; object-fit: cover; }
.avatar-sm { width: var(--oct-avatar-sm); height: var(--oct-avatar-sm); font-size: 9px; }
.avatar-lg { width: var(--oct-avatar-lg); height: var(--oct-avatar-lg); font-size: var(--oct-text-sm); }
.avatar-accent { background: var(--oct-accent); border-color: transparent; color: var(--oct-accent-on); }
/* the ring is the page ground, so the stack reads as overlap rather than mush */
.avatar-group { display: inline-flex; }
.avatar-group .avatar + .avatar { margin-left: -8px; box-shadow: 0 0 0 2px var(--oct-bg); }

/* ─── spinner ─────────────────────────────────────────────────────────
   Under prefers-reduced-motion the rotation stops, so the arc alone has
   to read as "busy" — that is why it is a visible three-quarter ring. */
.spinner { display: inline-block; width: var(--oct-icon-md); height: var(--oct-icon-md); border: 2px solid var(--oct-border); border-top-color: var(--oct-accent-fg); border-radius: var(--oct-radius-pill); animation: oct-spin 700ms linear infinite; vertical-align: -3px; }
.spinner-sm { width: var(--oct-icon-sm); height: var(--oct-icon-sm); border-width: 2px; }
.spinner-lg { width: 28px; height: 28px; border-width: 3px; }
@keyframes oct-spin { to { transform: rotate(360deg); } }

/* ─── skeleton ──────────────────────────────────────────────────────── */
.skeleton { background: var(--oct-fg-soft); border-radius: var(--oct-radius-sm); position: relative; overflow: hidden; }
.skeleton::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, var(--oct-sheen), transparent); transform: translateX(-100%); animation: oct-sheen 1.4s ease-in-out infinite; }
.skeleton-text { height: 12px; margin-block: 6px; }
.skeleton-title { height: 22px; border-radius: var(--oct-radius-sm); }
.skeleton-circle { border-radius: var(--oct-radius-pill); width: var(--oct-avatar-md); height: var(--oct-avatar-md); }
.skeleton-block { height: 96px; border-radius: var(--oct-radius-lg); }
.w-40 { width: 40%; } .w-60 { width: 60%; } .w-80 { width: 80%; }
@keyframes oct-sheen { to { transform: translateX(100%); } }

/* ─── empty state ───────────────────────────────────────────────────── */
.empty { display: grid; justify-items: center; gap: var(--oct-space-3); padding: var(--oct-space-12) var(--oct-space-6); border: 1px dashed var(--oct-border); border-radius: var(--oct-radius-lg); text-align: center; }
.empty h3 { font-size: var(--oct-text-lg); margin: 0; }
.empty p { color: var(--oct-muted); font-size: var(--oct-text-sm); margin: 0; max-width: 42ch; }
.empty .icon-mark { color: var(--oct-muted); }

/* ─── menu ────────────────────────────────────────────────────────────
   Positioning is the caller's job — .menu only styles the surface, so it
   works inside a popover, a details, or an absolutely placed wrapper. */
.menu { min-width: 200px; padding: var(--oct-space-2); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); z-index: var(--oct-z-overlay); }
.menu-label { display: block; padding: var(--oct-space-2) var(--oct-space-3) var(--oct-space-1); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.menu-item { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-4); width: 100%; min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border: 0; border-radius: 6px; background: none; color: var(--oct-fg-2); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; }
.menu-item:hover, .menu-item:focus-visible { background: var(--oct-fg-soft); color: var(--oct-fg); }
.menu-item[aria-disabled="true"] { opacity: var(--oct-disabled-opacity); pointer-events: none; }
.menu-item .kbd { pointer-events: none; }
.menu-item-danger { color: var(--oct-danger-fg); }
/* a menu row that has to explain itself. Reserved for menus where the
   labels are product nouns rather than verbs — "Work" and "Code" do not
   tell a first-run user which one they want, and a menu is the one
   presentation with room to say. The check sits in its own column at
   full height so the row has a single obvious right edge. */
.menu-item-rich { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 0 var(--oct-space-4); min-height: 0; padding-block: var(--oct-space-2); }
/* muted, not meta: this line is the reason the rich row exists — it is
   read to make a choice, so it is text and holds 4.5:1, the same call
   .mopt-sub makes for the same job in the mobile sheet */
.menu-item-desc { grid-column: 1; font-size: var(--oct-text-xs); color: var(--oct-muted); white-space: normal; }
.menu-item-rich > .icon { grid-column: 2; grid-row: 1 / span 2; visibility: hidden; color: var(--oct-accent-fg); }
.menu-item-rich[aria-current="true"] > .icon { visibility: visible; }
.menu-sep { height: 1px; margin: var(--oct-space-2) 0; background: var(--oct-border); border: 0; }

/* ─── modal ───────────────────────────────────────────────────────────
   Built on <dialog> so focus trapping, Esc, and inertness come from the
   platform rather than from JS we would have to maintain. */
.modal { width: min(520px, calc(100vw - 2 * var(--oct-space-6))); padding: 0; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); color: var(--oct-fg); box-shadow: var(--oct-elev-raised); }
.modal::backdrop { background: var(--oct-scrim); }
.modal-head { display: flex; align-items: flex-start; justify-content: space-between; gap: var(--oct-space-4); padding: var(--oct-space-5) var(--oct-space-6) var(--oct-space-3); }
.modal-head h3 { margin: 0; font-size: var(--oct-text-xl); }
.modal-body { padding: 0 var(--oct-space-6) var(--oct-space-5); color: var(--oct-muted); font-size: var(--oct-text-base); }
.modal-foot { display: flex; justify-content: flex-end; gap: var(--oct-space-3); padding: var(--oct-space-4) var(--oct-space-6); border-top: 1px solid var(--oct-border); background: var(--oct-surface-warm); border-radius: 0 0 var(--oct-radius-lg) var(--oct-radius-lg); }
/* a scrim for anything that is not a <dialog> */
.overlay { position: fixed; inset: 0; background: var(--oct-scrim); z-index: var(--oct-z-overlay); }

/* ─── tooltip ─────────────────────────────────────────────────────────
   Hover-only, so the tip must never carry information that is not also
   available another way. */
.tooltip { position: relative; display: inline-flex; }
.tooltip > .tip { position: absolute; bottom: calc(100% + 8px); left: 50%; translate: -50% 0; padding: var(--oct-space-2) var(--oct-space-3); border-radius: 6px; background: var(--oct-fg); color: var(--oct-bg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity var(--oct-motion-fast) var(--oct-ease-standard); z-index: var(--oct-z-overlay); }
.tooltip:hover > .tip, .tooltip:focus-within > .tip { opacity: 1; }

/* ─── toast ─────────────────────────────────────────────────────────── */
.toast-stack { position: fixed; right: var(--oct-space-6); bottom: var(--oct-space-6); display: grid; gap: var(--oct-space-3); z-index: var(--oct-z-toast); }
.toast { display: flex; align-items: flex-start; gap: var(--oct-space-3); min-width: 280px; max-width: 380px; padding: var(--oct-space-3) var(--oct-space-4); border: 1px solid var(--oct-border); border-left: 2px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.toast-ok     { border-left-color: var(--oct-success-fg); }
.toast-warn   { border-left-color: var(--oct-warn-fg); }
.toast-danger { border-left-color: var(--oct-danger-fg); }
.toast .toast-title { display: block; color: var(--oct-fg); font-family: var(--oct-font-display); }
.toast p { margin: 2px 0 0; color: var(--oct-muted); }

/* ─── breadcrumb ────────────────────────────────────────────────────── */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.crumbs a { color: var(--oct-muted); text-decoration: none; }
.crumbs a:hover { color: var(--oct-fg); }
.crumbs [aria-current="page"] { color: var(--oct-fg); }
.crumbs .sep { color: var(--oct-meta); }

/* ─── pagination ────────────────────────────────────────────────────── */
.pager { display: flex; align-items: center; gap: var(--oct-space-1); }
.pager-item { min-width: var(--oct-control-height-sm); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-2); display: inline-grid; place-items: center; border: 1px solid transparent; border-radius: 6px; background: none; color: var(--oct-muted); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; }
.pager-item:hover { color: var(--oct-fg); background: var(--oct-fg-soft); }
.pager-item[aria-current="page"] { color: var(--oct-fg); border-color: var(--oct-border); background: var(--oct-surface); }
.pager-item:disabled { opacity: var(--oct-disabled-opacity); pointer-events: none; }
.pager-gap { color: var(--oct-meta); padding-inline: var(--oct-space-1); }

/* ─── slider ──────────────────────────────────────────────────────────
   The two thumb rules cannot be combined: an unknown pseudo-element in a
   selector list invalidates the whole rule, so each engine gets its own. */
.range { -webkit-appearance: none; appearance: none; width: 100%; height: 4px; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); outline-offset: 6px; }
.range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-accent); cursor: pointer; }
.range::-moz-range-thumb { width: 16px; height: 16px; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-accent); cursor: pointer; }
.range:focus-visible { outline: 2px solid var(--oct-accent-fg); }

/* ─── accordion ───────────────────────────────────────────────────────
   <details>, so it works with no script and stays keyboard-accessible. */
.accordion { border-top: 1px solid var(--oct-border); }
.accordion > summary { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-4); padding: var(--oct-space-4) 0; cursor: pointer; list-style: none; font-family: var(--oct-font-display); font-size: var(--oct-text-base); color: var(--oct-fg); }
.accordion > summary::-webkit-details-marker { display: none; }
.accordion > summary::after { content: '+'; font-family: var(--oct-font-mono); color: var(--oct-muted); }
.accordion[open] > summary::after { content: '–'; }
.accordion > summary:hover { color: var(--oct-fg); }
.accordion .accordion-body { padding: 0 0 var(--oct-space-5); color: var(--oct-muted); font-size: var(--oct-text-sm); max-width: 66ch; }

/* ─── command palette ───────────────────────────────────────────────── */
.palette { width: min(560px, calc(100vw - 2 * var(--oct-space-6))); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); overflow: hidden; }
.palette-input { width: 100%; min-height: var(--oct-control-height); padding-block: 0; padding-inline: var(--oct-space-4); border: 0; border-bottom: 1px solid var(--oct-border); border-radius: 0; background: transparent; color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-base); }
.palette-input::placeholder { color: var(--oct-meta); }
.palette-input:focus { outline: none; }
.palette-list { max-height: 260px; overflow-y: auto; padding: var(--oct-space-2); margin: 0; list-style: none; scrollbar-width: thin; }
.palette-item { display: flex; align-items: center; gap: var(--oct-space-3); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border-radius: 6px; color: var(--oct-fg-2); font-size: var(--oct-text-sm); }
.palette-item .hint { margin-left: auto; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.palette-item[aria-selected="true"] { background: var(--oct-fg-soft); color: var(--oct-fg); }
.palette-foot { display: flex; gap: var(--oct-space-4); padding: var(--oct-space-2) var(--oct-space-4); border-top: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* ══════════════════════════════════════════════════════════════════════
   Product surfaces — Chat, Work, Code
   ══════════════════════════════════════════════════════════════════════ */

/* ─── chat message ──────────────────────────────────────────────────── */
.msg { display: grid; grid-template-columns: var(--oct-avatar-md) minmax(0, 1fr); gap: var(--oct-space-3); padding: var(--oct-space-4) 0; }
.msg + .msg { border-top: 1px solid var(--oct-border-soft); }
.msg-role { display: flex; align-items: baseline; gap: var(--oct-space-2); margin-bottom: var(--oct-space-1); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-muted); }
.msg-role .time { color: var(--oct-meta); letter-spacing: 0; text-transform: none; }
.msg-body { color: var(--oct-fg-2); font-size: var(--oct-text-base); line-height: var(--oct-leading-body); overflow-wrap: anywhere; }
.msg-body p { margin: 0 0 var(--oct-space-2); }
.msg-body > :last-child { margin-bottom: 0; }
.msg-agent .msg-body { color: var(--oct-fg); }
.msg-tools { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); margin-top: var(--oct-space-3); }

/* ══════════════════════════════════════════════════════════════════════
   Agent transcript

   The shape both Claude Code and Codex converge on, and the reason it
   works: the agent's prose runs flush at full width like a document, the
   user's turns are small right-aligned bubbles, and everything the agent
   *did* collapses to a single quiet line. Reading a long session is then
   reading prose, with the machinery available but never in the way.
   ══════════════════════════════════════════════════════════════════════ */

/* the user's turn: a bubble, right-aligned, deliberately narrow */
.turn-user { display: flex; justify-content: flex-end; padding-block: var(--oct-space-4); }
.turn-user .bubble { max-width: min(560px, 80%); padding: var(--oct-space-3) var(--oct-space-4); border-radius: var(--oct-radius-lg); background: var(--oct-surface-warm); color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); line-height: var(--oct-leading-body); overflow-wrap: anywhere; }
.turn-user .thumb { display: block; margin-left: auto; margin-bottom: var(--oct-space-2); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); overflow: hidden; }

/* the agent's turn: no avatar, no rail, no bubble — just the text */
.turn-agent { padding-block: var(--oct-space-2); color: var(--oct-fg-2); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); line-height: var(--oct-leading-body); }
.turn-agent > p { margin: 0 0 var(--oct-space-4); max-width: 78ch; }
.turn-agent > ul, .turn-agent > ol { margin: 0 0 var(--oct-space-4); padding-left: var(--oct-space-5); max-width: 78ch; }
.turn-agent li { margin-bottom: var(--oct-space-2); }
.turn-agent > :last-child { margin-bottom: 0; }

/* ─── diff stat ───────────────────────────────────────────────────────
   Always a pair, always mono, always tabular — a column of these should
   line up down the edge of a file list. */
.diffstat { display: inline-flex; gap: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; }
.diffstat .add { color: var(--oct-success-fg); }
.diffstat .del { color: var(--oct-danger-fg); }

/* ─── tool row ────────────────────────────────────────────────────────
   One line per thing the agent did. <details>, so expanding needs no
   script and the collapsed state is the default. The whole row is the
   summary; the marker is a chevron that turns. */
.toolrow { border-radius: 6px; }
.toolrow > summary { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-2) var(--oct-space-2); list-style: none; cursor: pointer; color: var(--oct-muted); font-size: var(--oct-text-sm); }
.toolrow > summary::-webkit-details-marker { display: none; }
.toolrow > summary:hover { background: var(--oct-fg-soft); color: var(--oct-fg-2); }
.toolrow > summary .icon { color: var(--oct-meta); }
.toolrow > summary .toolrow-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolrow > summary .toolrow-gap { flex: 1; }
/* the chevron is the affordance; it rotates rather than swapping glyphs */
.toolrow > summary .chev { color: var(--oct-meta); transition: transform var(--oct-motion-fast) var(--oct-ease-standard); }
.toolrow[open] > summary .chev { transform: rotate(90deg); }
.toolrow-body { padding: 0 var(--oct-space-2) var(--oct-space-3) var(--oct-space-6); }
/* a tool that is still running */
.toolrow.is-running > summary { color: var(--oct-fg-2); }

/* ─── file change card ────────────────────────────────────────────────
   "Edited 38 files, +157 −272", the file rows, and the expander. The
   header carries the actions because they apply to the whole change. */
.filecard { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); overflow: hidden; }
.filecard-head { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-3) var(--oct-space-3); }
.filecard-head .icon-mark { width: 30px; height: 30px; }
.filecard-head .filecard-title { display: block; color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); }
.filecard-head .filecard-gap { flex: 1; }
.filerow { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-2) var(--oct-space-4); border-top: 1px solid var(--oct-border-soft); background: none; border-inline: 0; border-bottom: 0; width: 100%; color: var(--oct-fg-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); text-align: left; }
.filerow:hover { background: var(--oct-fg-soft); }
.filerow .filerow-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.filerow .filerow-gap { flex: 1; }
.filecard-more { width: 100%; padding: var(--oct-space-2); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: var(--oct-muted); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.filecard-more:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }

/* ─── run status ──────────────────────────────────────────────────────
   The quiet line under a working turn: elapsed, cost, and what it is
   doing. Metadata, so mono and muted, never competing with the prose. */
.runstatus { display: flex; align-items: center; gap: var(--oct-space-2); padding-block: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.runstatus .sep { opacity: 0.6; }
.runstatus .spinner { width: 11px; height: 11px; border-width: 1.5px; vertical-align: 0; }

/* ─── dock rows ───────────────────────────────────────────────────────
   The Environment-panel pattern: an icon, a label, and either a value or
   a chevron. One row shape for navigation and for read-out both. */
.dock-group { margin-bottom: var(--oct-space-5); }
.dock-group > .dock-group-head { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-1) var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.dock-group > .dock-group-head .dock-gap { flex: 1; }
.dock-row { display: flex; align-items: center; gap: var(--oct-space-2); width: 100%; min-height: 28px; padding-inline: var(--oct-space-2); border: 0; border-radius: 6px; background: none; color: var(--oct-fg-2); font-size: var(--oct-text-sm); text-align: left; }
.dock-row:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.dock-row .icon { color: var(--oct-meta); }
.dock-row .dock-value { margin-left: auto; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.dock-row .dock-value.strong { color: var(--oct-fg-2); }

/* ─── composer context strip ──────────────────────────────────────────
   What the next message will act on — repo, branch, pending diff — sat
   above the input rather than described inside it. */
.composer-context { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-2) var(--oct-space-2) var(--oct-space-2) var(--oct-space-3); border: 1px solid var(--oct-border); border-bottom: 0; border-radius: var(--oct-radius-lg) var(--oct-radius-lg) 0 0; background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.composer-context + .composer { border-radius: 0 0 var(--oct-radius-lg) var(--oct-radius-lg); }
.composer-context .composer-gap { flex: 1; }
/* a standing mode that changes what happens when you send */
.mode-pill { display: inline-flex; align-items: center; gap: var(--oct-space-1); min-height: 22px; padding-inline: var(--oct-space-2); border-radius: 5px; background: var(--oct-warn-soft); color: var(--oct-warn-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
/* the send/stop control: a circle, filled while it is stoppable */
.btn-send { display: inline-grid; place-items: center; width: 28px; height: 28px; min-height: 0; padding: 0; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-accent); color: var(--oct-accent-on); }
.btn-send:disabled { background: var(--oct-fg-soft); color: var(--oct-meta); }

/* ══════════════════════════════════════════════════════════════════════
   Thread board

   Threads are work in flight, so the board is the same object twice: a
   kanban when you are triaging by status, a list when you are scanning
   many. Both draw the same .board-card facts — status, PR, checks,
   changed files, sub-agents — so switching view never loses information.

   Columns are the app's own: ready, in-progress, waiting, done.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── status dot ──────────────────────────────────────────────────────
   Every status also carries a word somewhere. Colour is never the only
   thing distinguishing two states. */
.st { display: inline-block; width: 8px; height: 8px; border-radius: var(--oct-radius-pill); flex: 0 0 auto; box-shadow: inset 0 0 0 1.5px currentColor; background: transparent; }
.st-ready       { color: var(--oct-muted); }
.st-in-progress { color: var(--oct-accent-fg); background: currentColor; }
.st-waiting     { color: var(--oct-warn-fg); }
.st-done        { color: var(--oct-success-fg); background: currentColor; }

/* ─── kanban ──────────────────────────────────────────────────────────
   The board scrolls horizontally; each column scrolls on its own. The
   column head stays put so a long column keeps its label and count. */
.board { display: flex; gap: var(--oct-space-3); align-items: stretch; overflow-x: auto; overscroll-behavior-x: contain; padding-bottom: var(--oct-space-2); scrollbar-width: thin; }
.board-col { display: flex; flex-direction: column; min-height: 0; width: 268px; flex: 0 0 268px; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); }
.board-col-head { display: flex; align-items: center; gap: var(--oct-space-2); position: sticky; top: 0; min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); border-radius: var(--oct-radius-lg) var(--oct-radius-lg) 0 0; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-muted); }
.board-col-head .count { margin-left: auto; color: var(--oct-meta); font-variant-numeric: tabular-nums; }
.board-col-body { flex: 1; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: var(--oct-space-2); padding: var(--oct-space-2); scrollbar-width: thin; }
/* an empty column still shows, so the workflow stays legible */
.board-col-empty { padding: var(--oct-space-5) var(--oct-space-3); text-align: center; color: var(--oct-meta); font-size: var(--oct-text-xs); }

/* ─── board card ──────────────────────────────────────────────────────
   Title first, facts second. The facts row is mono so a column of cards
   scans as a table even though each is its own object. */
.board-card { display: block; width: 100%; padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-bg); color: inherit; text-align: left; text-decoration: none; }
.board-card:hover { border-color: color-mix(in oklab, var(--oct-fg) 26%, transparent); background: var(--oct-surface-warm); }
.board-card-top { display: flex; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-2); }
.board-card-title { display: block; color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); line-height: var(--oct-leading-snug); overflow-wrap: anywhere; }
/* unread is a presence, not a colour — it sits in the flow before the title */
.board-card .unread { width: 6px; height: 6px; border-radius: var(--oct-radius-pill); background: var(--oct-accent-fg); flex: 0 0 auto; }
.board-card-facts { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.board-card-facts .fact { display: inline-flex; align-items: center; gap: var(--oct-space-1); }
.board-card-facts .fact .icon { width: 12px; height: 12px; }
.board-card-facts .fact.ok   { color: var(--oct-success-fg); }
.board-card-facts .fact.warn { color: var(--oct-warn-fg); }
.board-card-facts .fact.bad  { color: var(--oct-danger-fg); }
.board-card-facts .fact.live { color: var(--oct-accent-fg); }
/* why a card cannot move — the one thing a triager needs and boards hide */
.board-card-blocked { display: flex; align-items: flex-start; gap: var(--oct-space-2); margin-top: var(--oct-space-2); padding: var(--oct-space-2); border-radius: 6px; background: var(--oct-warn-soft); color: var(--oct-warn-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }

/* ─── issue list ──────────────────────────────────────────────────────
   The same facts, one row per thread, for scanning rather than triaging. */
.issuelist { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.issuerow { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-3) var(--oct-space-4); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; text-decoration: none; }
.issuerow:first-child { border-top: 0; }
.issuerow:hover { background: var(--oct-fg-soft); }
.issuerow .issuerow-main { min-width: 0; flex: 1; }
.issuerow .issuerow-title { display: block; color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.issuerow .issuerow-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); margin-top: 2px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.issuerow .issuerow-right { display: flex; align-items: center; gap: var(--oct-space-3); flex: 0 0 auto; }
@media (max-width: 720px) { .issuerow .issuerow-right { display: none; } }

/* the toolbar above either view */
.boardbar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-4); }
.boardbar .boardbar-gap { flex: 1; }

/* ─── pull request pane ───────────────────────────────────────────────
   State is a word and a colour, never a colour alone. */
.pr-head { display: flex; align-items: flex-start; gap: var(--oct-space-3); padding-bottom: var(--oct-space-4); border-bottom: 1px solid var(--oct-border); }
.pr-title { margin: 0; font-size: var(--oct-text-xl); }
.pr-sub { margin: var(--oct-space-1) 0 0; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.pr-state { display: inline-flex; align-items: center; gap: var(--oct-space-1); min-height: 24px; padding-inline: var(--oct-space-3); border-radius: var(--oct-radius-pill); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); text-transform: uppercase; letter-spacing: var(--oct-tracking-wide); }
.pr-state-open   { background: var(--oct-success-soft); color: var(--oct-success-fg); }
.pr-state-draft  { background: var(--oct-fg-soft);      color: var(--oct-muted); }
.pr-state-merged { background: var(--oct-accent-soft);  color: var(--oct-accent-fg); }
.pr-state-closed { background: var(--oct-danger-soft);  color: var(--oct-danger-fg); }

/* ─── checks ──────────────────────────────────────────────────────────
   One row per check: icon, name, duration. The icon carries the state and
   the icon differs in shape per state, not only in colour. */
.checks { margin: 0; padding: 0; list-style: none; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); overflow: hidden; }
.check-row { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-2) var(--oct-space-3); border-top: 1px solid var(--oct-border-soft); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg-2); }
.check-row:first-child { border-top: 0; }
.check-row .check-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.check-row .check-time { color: var(--oct-meta); }
.check-row.is-pass .icon { color: var(--oct-success-fg); }
.check-row.is-fail .icon { color: var(--oct-danger-fg); }
.check-row.is-pending .icon, .check-row.is-pending .spinner { color: var(--oct-warn-fg); }
.check-row.is-unknown .icon { color: var(--oct-meta); }

/* ─── review findings ─────────────────────────────────────────────────
   Severity leads, then the claim, then where it is. */
.finding { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.finding:first-of-type { border-top: 0; }
.finding .finding-body { min-width: 0; }
.finding .finding-claim { display: block; color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); }
.finding .finding-where { display: block; margin-top: 2px; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.finding .finding-note { margin: var(--oct-space-2) 0 0; color: var(--oct-muted); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); }
.sev { display: inline-flex; align-items: center; justify-content: center; min-width: 58px; min-height: 20px; padding-inline: var(--oct-space-2); border-radius: 5px; font-family: var(--oct-font-mono); font-size: 10px; letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; }
.sev-high  { background: var(--oct-danger-soft);  color: var(--oct-danger-fg); }
.sev-med   { background: var(--oct-warn-soft);    color: var(--oct-warn-fg); }
.sev-low   { background: var(--oct-fg-soft);      color: var(--oct-muted); }
.sev-nit   { background: var(--oct-fg-soft);      color: var(--oct-meta); }

/* ─── test tree ───────────────────────────────────────────────────────
   A suite is a <details> so a failing run can be opened without expanding
   the passing ones. Failures are what you came for, so they lead. */
.testtree { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.suite { border-top: 1px solid var(--oct-border-soft); }
.suite:first-child { border-top: 0; }
.suite > summary { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-2) var(--oct-space-2); list-style: none; cursor: pointer; color: var(--oct-fg-2); }
.suite > summary::-webkit-details-marker { display: none; }
.suite > summary:hover { background: var(--oct-fg-soft); }
.suite > summary .chev { color: var(--oct-meta); transition: transform var(--oct-motion-fast) var(--oct-ease-standard); }
.suite[open] > summary .chev { transform: rotate(90deg); }
.suite > summary .suite-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.suite-tally { display: inline-flex; gap: var(--oct-space-2); font-variant-numeric: tabular-nums; }
.suite-tally .pass { color: var(--oct-success-fg); }
.suite-tally .fail { color: var(--oct-danger-fg); }
.suite-tally .skip { color: var(--oct-meta); }
.testrow { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-1) var(--oct-space-2) var(--oct-space-1) var(--oct-space-6); color: var(--oct-muted); }
.testrow .test-time { margin-left: auto; color: var(--oct-meta); }
.testrow.is-pass .icon { color: var(--oct-success-fg); }
.testrow.is-fail { color: var(--oct-fg-2); }
.testrow.is-fail .icon { color: var(--oct-danger-fg); }
.testrow.is-skip .icon { color: var(--oct-meta); }
.test-failure { margin: var(--oct-space-1) var(--oct-space-2) var(--oct-space-2) var(--oct-space-6); padding: var(--oct-space-2) var(--oct-space-3); border-left: 2px solid var(--oct-danger-fg); background: var(--oct-danger-soft); color: var(--oct-fg-2); white-space: pre-wrap; overflow-x: auto; }

/* ─── notifications ───────────────────────────────────────────────────
   Unread is a left rail plus a dot; read notifications stay legible
   rather than fading to unreadable. */
.notif { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-3); border: 0; border-left: 2px solid transparent; border-bottom: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; }
.notif:hover { background: var(--oct-fg-soft); }
.notif.is-unread { border-left-color: var(--oct-accent-fg); background: var(--oct-border-soft); }
.notif .notif-text { min-width: 0; font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.notif .notif-text strong { color: var(--oct-fg); font-weight: var(--oct-weight-strong); }
.notif .notif-when { color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; }
.notif-group { padding: var(--oct-space-2) var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); background: var(--oct-surface-warm); border-bottom: 1px solid var(--oct-border); }
/* a count on the bell */
.bell { position: relative; }
.bell .bell-count { position: absolute; top: -2px; right: -2px; min-width: 15px; height: 15px; padding-inline: 3px; border-radius: var(--oct-radius-pill); background: var(--oct-accent); color: var(--oct-accent-on); font-family: var(--oct-font-mono); font-size: 9px; line-height: 15px; text-align: center; }

/* ─── diff ────────────────────────────────────────────────────────────
   Added and removed lines carry a sign in the gutter as well as a tint,
   so the diff survives being read without colour. */
.diff { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); font-family: var(--oct-font-mono); font-size: 13px; line-height: var(--oct-leading-code); overflow: hidden; }
.diff-head { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-4); padding: var(--oct-space-2) var(--oct-space-4); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.diff-body { overflow-x: auto; scrollbar-width: thin; }
.diff-line { display: grid; grid-template-columns: 44px 14px minmax(0, 1fr); min-width: 340px; }
.diff-line .ln { padding-inline: var(--oct-space-3) var(--oct-space-2); text-align: right; color: var(--oct-meta); user-select: none; font-variant-numeric: tabular-nums; }
.diff-line .sign { color: var(--oct-meta); user-select: none; }
.diff-line .src { padding-right: var(--oct-space-4); color: var(--oct-fg-2); white-space: pre; }
.diff-add { background: var(--oct-success-soft); }
.diff-add .sign, .diff-add .src { color: var(--oct-success-fg); }
.diff-del { background: var(--oct-danger-soft); }
.diff-del .sign, .diff-del .src { color: var(--oct-danger-fg); }

/* ─── file tree ─────────────────────────────────────────────────────── */
.tree { margin: 0; padding: 0; list-style: none; font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); }
.tree ul { margin: 0; padding-left: var(--oct-tree-indent); list-style: none; border-left: 1px solid var(--oct-border-soft); }
.tree-item { display: flex; align-items: center; gap: var(--oct-space-2); min-height: 26px; padding-inline: var(--oct-space-2); border-radius: 5px; color: var(--oct-muted); }
.tree-item:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.tree-item[aria-selected="true"] { background: var(--oct-fg-soft); color: var(--oct-fg); }
.tree-item .mark { color: var(--oct-meta); flex: 0 0 auto; }
.tree-item .status { margin-left: auto; font-size: var(--oct-text-xs); }
.tree-item .status.added { color: var(--oct-success-fg); }
.tree-item .status.edited { color: var(--oct-accent-fg); }

/* ─── split pane ──────────────────────────────────────────────────────
   The handle is a real separator, not a decorative border, so it can take
   focus and be driven from the keyboard. */
.split { display: grid; grid-template-columns: var(--oct-split, minmax(0, 1fr)) 1px minmax(0, 1.6fr); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.split-pane { padding: var(--oct-space-4); min-width: 0; overflow: auto; scrollbar-width: thin; }
/* the hit area is wider than the hairline: a 1px target is a 1px target */
.split-handle { position: relative; background: var(--oct-border); border: 0; padding: 0; cursor: col-resize; touch-action: none; }
/* a 1px line to look at, a 25px column to grab. -4px was a comfortable
   grab zone but still under the 24px target floor; 12 each side clears
   it and matches what every other resizable pane does. */
.split-handle::before { content: ''; position: absolute; inset-block: 0; inset-inline: -12px; }
.split-handle:hover, .split-handle:focus-visible { background: var(--oct-accent-fg); }
/* while dragging, stop the pointer selecting text in the panes it crosses */
.is-dragging, .is-dragging * { user-select: none; cursor: col-resize !important; }
@media (max-width: 720px) {
  .split { grid-template-columns: minmax(0, 1fr); }
  .split-handle { height: 1px; cursor: row-resize; }
  .split-handle::before { inset-inline: 0; inset-block: -4px; }
}

/* ══════════════════════════════════════════════════════════════════════
   App shell
   ══════════════════════════════════════════════════════════════════════ */

.app { display: grid; grid-template-columns: var(--oct-sidebar-w) minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; height: 100%; background: var(--oct-bg); }
.app[data-compact="true"] { --oct-sidebar-w: var(--oct-sidebar-w-compact); }
/* the dock is a third column when there is one, declared by the presence
   of the dock rather than by a class on the shell. One less thing that
   can be set on one element and forgotten on the other. */
.app:has(> .dock) { grid-template-columns: var(--oct-sidebar-w) minmax(0, 1fr) var(--oct-dock-w); }
/* below the breakpoint the dock would leave the transcript unreadably
   narrow, so it goes rather than shrinking */
@media (max-width: 920px) { .app:has(> .dock) { grid-template-columns: var(--oct-sidebar-w) minmax(0, 1fr); } .app > .dock { display: none; } }
.app > .statusbar { grid-column: 1 / -1; }
.app-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.app-body { flex: 1; min-height: 0; overflow: auto; padding: var(--oct-space-5); scrollbar-width: thin; }

/* ─── sidebar ─────────────────────────────────────────────────────────
   Compact is a width change, not a different component. The labels stay in
   the DOM and are clipped, so the accessible name of every item survives
   collapsing — hiding them with display:none would take the name with it. */
.sidebar { display: flex; flex-direction: column; gap: var(--oct-space-1); min-width: 0; padding: var(--oct-space-3) var(--oct-space-2); border-right: 1px solid var(--oct-border); background: var(--oct-surface); overflow: hidden auto; scrollbar-width: thin; transition: none; }
.sidebar-head { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-titlebar-h); padding-inline: var(--oct-space-2); color: var(--oct-fg); font-family: var(--oct-font-display); letter-spacing: var(--oct-tracking-wordmark); }
.sidebar-head .icon { color: var(--oct-fg); }
.sidebar-section { margin-top: var(--oct-space-4); padding: var(--oct-space-1) var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); white-space: nowrap; }
.sidebar-item { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-2); border: 0; border-radius: 6px; background: none; color: var(--oct-muted); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; text-decoration: none; white-space: nowrap; }
.sidebar-item:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.sidebar-item[aria-current="page"] { background: var(--oct-fg-soft); color: var(--oct-fg); }
.sidebar-item .count { margin-left: auto; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.sidebar-foot { margin-top: auto; padding-top: var(--oct-space-3); }
/* clipped, not removed — the label is still the accessible name */
.app[data-compact="true"] .sidebar-label,
.app[data-compact="true"] .sidebar-item .count,
.app[data-compact="true"] .sidebar-section { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.app[data-compact="true"] .sidebar-section { margin-top: var(--oct-space-4); padding: 0; }
.app[data-compact="true"] .sidebar-item { justify-content: center; gap: 0; }
.app[data-compact="true"] .sidebar-head { justify-content: center; padding-inline: 0; }

/* ─── mode switcher ───────────────────────────────────────────────────
   Chat, Work and Code are the three surfaces, and moving between them is
   the most-taken action in the product. It is also the one people hold
   the strongest opinions about, so it ships in three presentations and
   the choice is a setting rather than a decision made here.

   Two of them — list and icons — are the SAME component. Same three
   links, same aria-current, same counts in the markup; only the density
   changes, driven by an attribute. The third is a menu, and it is a
   genuinely different control (a button plus a popover), because a
   dropdown has a trigger and a list does not. Pretending all three were
   one component would mean a container that is sometimes a popover and
   sometimes not, which is a worse lie than admitting there are two.

   What each one costs, which is the whole argument for offering a choice:

     list   three rows of sidebar. The only one that answers "is anything
            waiting for me" without a click, because it has room for the
            counts as numbers.
     icons  one row. Counts survive as a dot — the half of the number
            people actually act on — and the labels go to the
            accessible name.
     menu   one row, no counts at all, and the only one that still works
            in a sidebar narrow enough to truncate a label.

   Every presentation marks the active surface with aria-current="page",
   so what a screen reader announces never depends on which one is on. */
.modeswitch { display: flex; flex-direction: column; gap: var(--oct-space-1); }
.mode { display: flex; align-items: center; gap: var(--oct-space-3); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-2); border: 0; border-radius: 6px; background: none; color: var(--oct-muted); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; text-decoration: none; white-space: nowrap; }
.mode > .icon { flex: 0 0 auto; }
.mode:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.mode[aria-current="page"] { background: var(--oct-fg-soft); color: var(--oct-fg); }
.mode .count { margin-left: auto; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; }

/* Icons only, and nothing around them.

   An earlier version sat the three in a bordered tray and stretched them
   to the sidebar's width. Two things were wrong with that. A tray reads
   as a segmented control — a setting you adjust — when this is
   navigation, somewhere you go. And full-width stretching meant three
   icons wore the visual weight of three rows while saving none of the
   information, which is the worst of both presentations.

   So: no border, no ground, no stretch. Three buttons at their natural
   size sitting in a line, and the only mark on the row is the soft fill
   under the current surface — the same fill .sidebar-item uses for the
   same job, so the sidebar keeps one vocabulary for "you are here". */
.modeswitch[data-oct-modeswitch="icons"] { flex-direction: row; gap: var(--oct-space-1); padding: 0; border: 0; background: none; }
.modeswitch[data-oct-modeswitch="icons"] .mode { position: relative; flex: 0 0 auto; width: 30px; height: 30px; min-height: 0; justify-content: center; gap: 0; padding-inline: 0; border-radius: 8px; }
/* clipped, not removed — the label is still the accessible name */
.modeswitch[data-oct-modeswitch="icons"] .mode-label,
.modeswitch[data-oct-modeswitch="icons"] .count { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* Three 30px buttons need 98px and a compact sidebar is 52. It is already
   a vertical icon rail, so the switcher turns to match rather than
   overflowing it. Keyed to data-compact rather than to a width, because
   the sidebar can be collapsed by hand at any viewport — this was broken
   on a wide screen too, not only a narrow one. */
.app[data-compact="true"] .modeswitch[data-oct-modeswitch="icons"] { flex-direction: column; }

/* menu: a trigger that names the current surface, and the list of three
   in a popover. The trigger reads as the sidebar's heading rather than
   as a control, which is the point — it is where the wordmark would be. */
.mode-trigger { display: flex; align-items: center; gap: var(--oct-space-2); width: 100%; min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-2); border: 0; border-radius: 6px; background: none; color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; }
.mode-trigger:hover { background: var(--oct-fg-soft); }
.mode-trigger .mode-caret { margin-left: auto; color: var(--oct-meta); }
/* The two presentations take turns via the hidden attribute, and that
   attribute needs help here: [hidden] is only display:none in the user
   agent sheet, so the display:flex above it wins and both controls show
   at once. Anything in this file that sets display AND gets hidden by
   script needs the same line. */
.modeswitch[hidden], .mode-trigger[hidden] { display: none; }

/* ─── title bar, toolbar, status bar ──────────────────────────────────── */
.titlebar { display: flex; align-items: center; justify-content: space-between; gap: var(--oct-space-4); min-height: var(--oct-titlebar-h); padding-inline: var(--oct-space-4); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface); flex: 0 0 auto; }
.toolbar { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-control-height-sm); padding: var(--oct-space-2) var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); flex: 0 0 auto; }
.toolbar-gap { flex: 1; }
.statusbar { display: flex; align-items: center; gap: var(--oct-space-4); min-height: var(--oct-statusbar-h); padding-inline: var(--oct-space-3); border-top: 1px solid var(--oct-border); background: var(--oct-surface); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.statusbar .status-item { display: flex; align-items: center; gap: var(--oct-space-1); }
.statusbar .status-gap { flex: 1; }

/* ─── icon button ─────────────────────────────────────────────────────
   Square, for toolbars and title bars. It has no text, so it always needs
   an aria-label. */
.btn-icon { display: inline-grid; place-items: center; width: var(--oct-control-height-sm); height: var(--oct-control-height-sm); min-height: 0; padding: 0; border: 1px solid transparent; border-radius: 6px; background: none; color: var(--oct-muted); }
.btn-icon:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.btn-icon[aria-expanded="true"] { background: var(--oct-fg-soft); color: var(--oct-fg); }

/* ─── file tabs ───────────────────────────────────────────────────────── */
.filetabs { display: flex; align-items: stretch; overflow-x: auto; scrollbar-width: thin; border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); flex: 0 0 auto; }
.filetab { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border: 0; border-right: 1px solid var(--oct-border); background: none; color: var(--oct-muted); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; }
.filetab:hover { color: var(--oct-fg); }
.filetab[aria-selected="true"] { background: var(--oct-surface); color: var(--oct-fg); box-shadow: inset 0 -1px 0 var(--oct-accent-fg); }
.filetab .dirty { width: 6px; height: 6px; border-radius: var(--oct-radius-pill); background: var(--oct-accent-fg); }

/* ─── popover ─────────────────────────────────────────────────────────
   Native [popover], so the top layer, light-dismiss, and Esc come from the
   platform. Only the placement is ours; see octant.js. */
[popover].menu, [popover].palette { margin: 0; padding: var(--oct-space-2); border: 1px solid var(--oct-border); color: var(--oct-fg); inset: auto; }
[popover].palette { padding: 0; }
[popover]:not(:popover-open) { display: none; }
[popover]::backdrop { background: transparent; }
[popover].is-modal::backdrop { background: var(--oct-scrim); }

/* ─── picker ──────────────────────────────────────────────────────────
   A trigger that shows the current value, and a searchable panel. The app
   has nine of these — model, folder, host, branch, repository, execution
   context, thread — so it is one component, not nine.

   The panel reuses .palette wholesale: a search field over a filtered list
   is the same shape whether it is scoped to one field or to the whole app. */
.picker { display: inline-flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-control-height); padding-block: 0; padding-inline: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-sm); background: var(--oct-surface); color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; }
.picker:hover { border-color: color-mix(in oklab, var(--oct-fg) 26%, transparent); }
.picker[aria-expanded="true"] { border-color: var(--oct-accent-fg); }
.picker-sm { min-height: var(--oct-control-height-sm); font-size: var(--oct-text-xs); }
.picker .picker-value { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.picker .picker-key { color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); text-transform: uppercase; letter-spacing: var(--oct-tracking-wide); }
.picker .icon:last-child { margin-left: auto; color: var(--oct-meta); }
.picker-panel { width: min(340px, calc(100vw - 2 * var(--oct-space-6))); }
.picker-panel .palette-list { max-height: 220px; }
.palette-item .check { margin-left: auto; color: var(--oct-accent-fg); }
.palette-empty { padding: var(--oct-space-4) var(--oct-space-3); color: var(--oct-meta); font-size: var(--oct-text-sm); }

/* ─── dock ────────────────────────────────────────────────────────────
   The right-hand utility panel. Same surface whichever tool is in it. */
.dock { display: flex; flex-direction: column; min-height: 0; width: var(--oct-dock-w, 320px); border-left: 1px solid var(--oct-border); background: var(--oct-surface); }
.dock-head { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-muted); flex: 0 0 auto; }
.dock-head .dock-gap { flex: 1; }
.dock-body { flex: 1; min-height: 0; overflow: auto; padding: var(--oct-space-4); scrollbar-width: thin; }
.dock-foot { border-top: 1px solid var(--oct-border); padding: var(--oct-space-3) var(--oct-space-4); flex: 0 0 auto; }

/* ─── composer ────────────────────────────────────────────────────────
   The message input. The textarea grows with its content via field-sizing
   where supported, and the row underneath carries the pickers rather than
   floating them over the text. */
.composer { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); }
.composer:focus-within { border-color: var(--oct-accent-fg); }
.composer-chips { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); padding: var(--oct-space-3) var(--oct-space-3) 0; }
.composer-input { display: block; width: 100%; min-height: 60px; max-height: 240px; padding: var(--oct-space-3) var(--oct-space-4); border: 0; background: none; color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); line-height: var(--oct-leading-body); resize: none; field-sizing: content; }
.composer-input:focus { outline: none; }
.composer-input::placeholder { color: var(--oct-meta); }
.composer-row { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-2) var(--oct-space-2) var(--oct-space-2) var(--oct-space-3); border-top: 1px solid var(--oct-border-soft); }
.composer-row .composer-gap { flex: 1; }
/* an attachment, a mentioned file, a scoped folder */
.chip { display: inline-flex; align-items: center; gap: var(--oct-space-2); min-height: 24px; padding-inline: var(--oct-space-2); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-pill); background: var(--oct-surface-warm); color: var(--oct-fg-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
/* 14px of paint so the chip stays chip-sized, 24px of target so it can
   be hit — growing the button itself would grow every chip with it */
.chip .chip-x { position: relative; display: grid; place-items: center; width: 14px; height: 14px; border: 0; border-radius: var(--oct-radius-pill); background: none; color: var(--oct-meta); padding: 0; }
.chip .chip-x::before { content: ''; position: absolute; inset: -5px; }
.chip .chip-x:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }

/* ─── terminal ────────────────────────────────────────────────────────
   A surface for real terminal output. It sets only the frame, the face,
   and the ANSI-ish colour roles — the emulator draws the rest. */
.term { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-bg); color: var(--oct-fg-2); font-family: var(--oct-font-mono); font-size: 13px; line-height: var(--oct-leading-code); overflow: hidden; }
.term-bar { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-2) var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.term-body { padding: var(--oct-space-3); overflow: auto; scrollbar-width: thin; white-space: pre-wrap; overflow-wrap: anywhere; }
.term-body .prompt { color: var(--oct-accent-fg); }
.term-body .ok { color: var(--oct-success-fg); }
.term-body .err { color: var(--oct-danger-fg); }
.term-body .dim { color: var(--oct-meta); }
.term-cursor { display: inline-block; width: 7px; height: 1em; background: var(--oct-fg); vertical-align: -2px; animation: oct-blink 1.06s steps(1) infinite; }
@keyframes oct-blink { 50% { opacity: 0; } }

/* ─── workspace tabs ──────────────────────────────────────────────────
   .filetabs with a close affordance and a launcher. */
.filetab-close { display: grid; place-items: center; width: 16px; height: 16px; margin-left: var(--oct-space-1); border: 0; border-radius: 4px; background: none; color: var(--oct-meta); padding: 0; }
.filetab-close:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.filetabs-add { display: grid; place-items: center; width: var(--oct-control-height-sm); border: 0; border-right: 1px solid var(--oct-border); background: none; color: var(--oct-meta); flex: 0 0 auto; }
.filetabs-add:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }

/* ─── drop zone ───────────────────────────────────────────────────────
   Shown over a surface while a drag is in flight. */
.dropzone { position: absolute; inset: var(--oct-space-3); display: grid; place-items: center; gap: var(--oct-space-2); border: 1px dashed var(--oct-accent-fg); border-radius: var(--oct-radius-lg); background: color-mix(in oklab, var(--oct-bg) 82%, transparent); color: var(--oct-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); z-index: var(--oct-z-overlay); pointer-events: none; }

/* ─── settings layout ─────────────────────────────────────────────────
   Nav rail plus a scrolling body. Collapses to stacked on a narrow window. */
.settings { display: grid; grid-template-columns: 200px minmax(0, 1fr); min-height: 0; }
.settings-nav { display: flex; flex-direction: column; gap: 2px; padding: var(--oct-space-3) var(--oct-space-2); border-right: 1px solid var(--oct-border); overflow: auto; scrollbar-width: thin; }
.settings-body { min-width: 0; overflow: auto; padding: var(--oct-space-6); scrollbar-width: thin; }
.settings-group { max-width: 620px; margin-bottom: var(--oct-space-8); }
.settings-group > h3 { margin: 0 0 var(--oct-space-1); font-size: var(--oct-text-lg); }
.settings-group > .settings-note { margin: 0 0 var(--oct-space-5); color: var(--oct-muted); font-size: var(--oct-text-sm); }
/* label left, control right, description under the label */
.settings-item { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--oct-space-4); align-items: center; padding: var(--oct-space-4) 0; border-top: 1px solid var(--oct-border-soft); }
.settings-item:first-of-type { border-top: 0; }
.settings-item .settings-label { font-size: var(--oct-text-base); color: var(--oct-fg); }
.settings-item .settings-hint { margin: 2px 0 0; color: var(--oct-muted); font-size: var(--oct-text-sm); }
@media (max-width: 720px) {
  .settings { grid-template-columns: minmax(0, 1fr); }
  .settings-nav { flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--oct-border); }
  .settings-item { grid-template-columns: minmax(0, 1fr); }
}

/* ─── reference card ──────────────────────────────────────────────────
   A thread, document, or file quoted inside a message. */
.refcard { display: flex; align-items: flex-start; gap: var(--oct-space-3); padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); text-decoration: none; color: inherit; }
.refcard:hover { background: var(--oct-surface-warm); }
.refcard .refcard-title { display: block; color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); }
.refcard .refcard-meta { display: block; margin-top: 2px; color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }

/* ══════════════════════════════════════════════════════════════════════
   CHARTS

   The app already computes chart geometry itself; this layer supplies the
   frame, the axes, and the mark styling, and takes coordinates as given.

   Line, area and scatter are SVG, because they need real geometry. Bar
   and distribution are CSS grid, because they do not — a bar is a box
   with a height, and a box reflows on resize without anyone recomputing
   anything.

   The SVG scales uniformly (width 100%, height auto) rather than being
   stretched to a box. Stretching a viewBox distorts the stroke and turns
   every dot into an ellipse.
   ══════════════════════════════════════════════════════════════════════ */

.chart { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); padding: var(--oct-space-4); }
.chart-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: var(--oct-space-2) var(--oct-space-3); margin-bottom: var(--oct-space-3); }
.chart-title { margin: 0; font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.chart-sub { margin: 0; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.chart-head .chart-gap { flex: 1; }

/* the plot and its axis labels, as one grid: y labels in a fixed column,
   x labels under the plot only. Laying it out this way keeps the plot
   left edge aligned whatever the y labels turn out to be. */
.chart-plot { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; gap: var(--oct-space-1) var(--oct-space-2); }
.chart-y { grid-column: 1; grid-row: 1; display: flex; flex-direction: column; justify-content: space-between; align-items: flex-end; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-meta); }
.chart-x { grid-column: 2; grid-row: 2; display: flex; justify-content: space-between; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.chart-x span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chart-area-box { grid-column: 2; grid-row: 1; position: relative; min-width: 0; min-height: var(--oct-chart-h); border-left: 1px solid var(--oct-chart-axis); border-bottom: 1px solid var(--oct-chart-axis); }

/* horizontal rules behind the marks. A repeating gradient rather than
   elements, so the count is a token and nobody has to emit N divs. */
.chart-rules { position: absolute; inset: 0; pointer-events: none; background-image: repeating-linear-gradient(to top, var(--oct-chart-grid) 0 1px, transparent 1px calc(100% / 4)); }

.chart-svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
.chart-line { fill: none; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.chart-fill { stroke: none; opacity: 0.16; }

/* Points are CSS, not SVG, for the same reason bars are. The line and
   area paths stretch to the box with preserveAspectRatio="none", which is
   fine for a stroke that carries non-scaling-stroke — but it turns every
   circle into an ellipse. A dot placed by percentage stays round at any
   box shape.

   --x and --y are fractions of the domain, 0 to 1, with y measured from
   the bottom so it reads the same way as --v on a bar. */
.chart-dots { position: absolute; inset: 0; }
.chart-dot { position: absolute; left: calc(var(--x, 0) * 100%); bottom: calc(var(--y, 0) * 100%); width: 8px; height: 8px; margin: 0 0 -4px -4px; border-radius: var(--oct-radius-pill); background: var(--oct-ser, var(--oct-series-1)); box-shadow: 0 0 0 1.5px var(--oct-surface); }

/* one class per series sets both the colour and the dash, so a series is
   assigned in one place and can never end up half-styled */
.ser-1 { --oct-ser: var(--oct-series-1); --oct-ser-dash: var(--oct-dash-1); }
.ser-2 { --oct-ser: var(--oct-series-2); --oct-ser-dash: var(--oct-dash-2); }
.ser-3 { --oct-ser: var(--oct-series-3); --oct-ser-dash: var(--oct-dash-3); }
.ser-4 { --oct-ser: var(--oct-series-4); --oct-ser-dash: var(--oct-dash-4); }
.ser-5 { --oct-ser: var(--oct-series-5); --oct-ser-dash: var(--oct-dash-5); }
.ser-6 { --oct-ser: var(--oct-series-6); --oct-ser-dash: var(--oct-dash-6); }
.chart-line[class*="ser-"] { stroke: var(--oct-ser); stroke-dasharray: var(--oct-ser-dash); }
.chart-fill[class*="ser-"] { fill: var(--oct-ser); }

/* bars. --v is the value as a fraction of the domain, 0 to 1. */
.chart-bars { position: absolute; inset: 0; display: flex; align-items: flex-end; gap: 2px; padding-inline: 4px; }
.chart-bar { flex: 1; min-width: 2px; height: calc(var(--v, 0) * 100%); border-radius: 2px 2px 0 0; background: var(--oct-ser, var(--oct-series-1)); }
.chart-bar:hover { filter: brightness(1.12); }
/* a distribution is the same mechanism at higher density, so the bars
   touch and lose their radius — the shape of the whole is the subject */
.chart-bars.is-dense { gap: 1px; }
.chart-bars.is-dense .chart-bar { border-radius: 1px 1px 0 0; min-width: 1px; }
/* grouped bars: one .chart-group per category, bars side by side inside it */
.chart-group { flex: 1; display: flex; align-items: flex-end; gap: 1px; height: 100%; min-width: 0; }

/* the legend. The swatch carries the dash so the key matches the mark. */
.chart-legend { display: flex; flex-wrap: wrap; gap: var(--oct-space-2) var(--oct-space-4); margin-top: var(--oct-space-3); padding: 0; list-style: none; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg-2); }
.chart-legend li { display: inline-flex; align-items: center; gap: var(--oct-space-2); }
.chart-key { width: 18px; height: 0; flex: 0 0 auto; border-top: 2px solid var(--oct-ser, var(--oct-series-1)); }
.chart-key.is-dashed { border-top-style: dashed; }
.chart-key.is-dotted { border-top-style: dotted; }
.chart-key.is-block { height: 10px; border: 0; border-radius: 2px; background: var(--oct-ser, var(--oct-series-1)); }

/* a readout pinned to the pointer. Position comes from the app. */
.chart-tip { position: absolute; z-index: 2; pointer-events: none; padding: var(--oct-space-2) var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; }
.chart-tip dl { display: grid; grid-template-columns: auto auto; gap: 1px var(--oct-space-3); margin: var(--oct-space-1) 0 0; }
.chart-tip dt { color: var(--oct-meta); }
.chart-tip dd { margin: 0; color: var(--oct-fg); font-variant-numeric: tabular-nums; text-align: right; }
.chart-tip .chart-tip-head { color: var(--oct-fg-2); }

/* a chart with nothing in it still holds its height, so a dashboard does
   not jump as series arrive */
.chart-empty { display: grid; place-items: center; min-height: var(--oct-chart-h); color: var(--oct-meta); font-size: var(--oct-text-sm); }

/* a sparkline is a chart with the chrome removed — no axes, no legend,
   sized by its context rather than by the chart height token */
.sparkline { display: inline-block; width: 72px; height: 20px; vertical-align: middle; }
.sparkline .chart-line { stroke: var(--oct-ser, var(--oct-accent-fg)); stroke-dasharray: none; }

/* usage over time, the one chart the app has that is not a canvas block:
   a chart plus the totals that go with it */
.usage { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--oct-space-4); }
.usage-totals { display: flex; flex-wrap: wrap; gap: var(--oct-space-3); }
.usage-total { flex: 1 1 128px; padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); }
.usage-total .usage-label { display: block; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.usage-total .usage-value { display: block; margin-top: 2px; font-family: var(--oct-font-display); font-size: var(--oct-text-xl); font-variant-numeric: tabular-nums; color: var(--oct-fg); }

/* ══════════════════════════════════════════════════════════════════════
   CANVAS

   A canvas is a generated document: a stack of typed blocks with a
   provenance record. The chrome's whole job is to stay out of the way of
   the blocks while making two things always answerable — what kind of
   block is this, and where did it come from.

   The block kinds are the app's own union, and most of them are an
   existing component in a frame: a table block is .datatable, a diff
   block is .diff, a code block is .codeblock. Only the kinds with no
   equivalent elsewhere get their own rules below.
   ══════════════════════════════════════════════════════════════════════ */

.canvas { max-width: calc(var(--oct-canvas-measure) + var(--oct-canvas-gutter)); margin-inline: auto; }
.canvas-head { padding-bottom: var(--oct-space-4); margin-bottom: var(--oct-space-4); border-bottom: 1px solid var(--oct-border); }
.canvas-title { margin: 0 0 var(--oct-space-2); font-size: var(--oct-text-2xl); }
/* the provenance line: who made this, from what, at which version */
.canvas-prov { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.canvas-prov .icon { width: 13px; height: 13px; }
.canvas-body { display: flex; flex-direction: column; }

/* ─── block frame ─────────────────────────────────────────────────────
   The gutter is always reserved, never conjured on hover. Widening the
   block when the pointer arrives shifts every word in it, and a document
   that reflows under the cursor is unreadable. */
.cblock { position: relative; display: grid; grid-template-columns: var(--oct-canvas-gutter) minmax(0, 1fr); align-items: start; padding-block: var(--oct-space-2); border-radius: var(--oct-radius-md); }
.cblock-gutter { display: flex; flex-direction: column; align-items: center; gap: 2px; padding-top: 2px; opacity: 0; }
.cblock:hover .cblock-gutter,
.cblock:focus-within .cblock-gutter { opacity: 1; }
/* keyboard users get the gutter without a pointer, and it stays visible
   while a menu opened from it is still up */
.cblock.is-context-target .cblock-gutter { opacity: 1; }
/* 24px like every other grip in the system: a drag handle is the hardest
   target in its row, and 24 is the WCAG 2.5.8 floor. The icon stays 14. */
.cblock-grip { display: grid; place-items: center; width: 24px; height: 24px; border: 0; border-radius: 5px; background: none; color: var(--oct-meta); cursor: grab; }
.cblock-grip:hover { background: var(--oct-fg-soft); color: var(--oct-fg-2); }
.cblock-grip .icon { width: 14px; height: 14px; }
.cblock-body { min-width: 0; }
/* the kind label is chrome, so it sits above the block in the meta scale
   and disappears into the background until the block is engaged */
.cblock-kind { display: block; margin-bottom: var(--oct-space-1); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: transparent; }
.cblock:hover .cblock-kind,
.cblock:focus-within .cblock-kind { color: var(--oct-meta); }
.cblock[aria-selected="true"] { background: var(--oct-fg-soft); outline: 1px solid var(--oct-accent-fg); outline-offset: -1px; }
.cblock[aria-selected="true"] .cblock-gutter,
.cblock[aria-selected="true"] .cblock-kind { opacity: 1; color: var(--oct-meta); }
/* a block the agent is still writing */
.cblock.is-pending .cblock-body { opacity: 0.6; }

.cblock-body > :first-child { margin-top: 0; }
.cblock-body > :last-child  { margin-bottom: 0; }

/* ─── block kinds without an existing home ────────────────────────── */

/* citation — a quote that has to keep its source attached */
.cite { border-left: 2px solid var(--oct-accent); padding-left: var(--oct-space-4); }
.cite blockquote { margin: 0; font-family: var(--oct-font-transcript); font-size: var(--oct-text-base); line-height: var(--oct-leading-body); color: var(--oct-fg-2); }
.cite-src { display: block; margin-top: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* metric — one number, and how it moved */
.cmetric { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--oct-space-1) var(--oct-space-3); }
.cmetric-label { flex-basis: 100%; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.cmetric-value { font-family: var(--oct-font-display); font-size: var(--oct-text-3xl); line-height: 1; font-variant-numeric: tabular-nums; color: var(--oct-fg); }
.cmetric-unit { font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); color: var(--oct-muted); }
/* the arrow carries the direction, so up and down are not colour alone */
.cmetric-delta { display: inline-flex; align-items: center; gap: 2px; font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); font-variant-numeric: tabular-nums; }
.cmetric-delta.up   { color: var(--oct-success-fg); }
.cmetric-delta.down { color: var(--oct-danger-fg); }
.cmetric-delta.flat { color: var(--oct-meta); }
.cmetric-delta .icon { width: 13px; height: 13px; }

/* key-value — a fact table, aligned on the colon */
.kv { display: grid; grid-template-columns: minmax(0, auto) minmax(0, 1fr); gap: var(--oct-space-2) var(--oct-space-4); margin: 0; font-size: var(--oct-text-sm); }
.kv dt { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.kv dd { margin: 0; color: var(--oct-fg-2); overflow-wrap: anywhere; }
.kv dd.num { font-family: var(--oct-font-mono); font-variant-numeric: tabular-nums; }
@media (max-width: 560px) { .kv { grid-template-columns: minmax(0, 1fr); gap: var(--oct-space-1); } .kv dd { margin-bottom: var(--oct-space-2); } }

/* timeline — items on a rail, each with a status */
.ctimeline { position: relative; margin: 0; padding: 0 0 0 var(--oct-space-5); list-style: none; }
.ctimeline::before { content: ''; position: absolute; inset-block: 6px 6px; left: 3px; width: 1px; background: var(--oct-border); }
.ctimeline li { position: relative; padding-bottom: var(--oct-space-4); }
.ctimeline li:last-child { padding-bottom: 0; }
/* deliberate override: the dot straddles the timeline rule, so the second
   ring is the page colour and punches a hole in the line behind it */
.ctimeline .st { position: absolute; left: calc(-1 * var(--oct-space-5) + 3px); top: 5px; transform: translateX(-50%); box-shadow: inset 0 0 0 1.5px currentColor, 0 0 0 3px var(--oct-bg); }
.ctimeline-title { display: block; font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.ctimeline-when { display: block; margin-top: 1px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-meta); }
.ctimeline-detail { margin: var(--oct-space-1) 0 0; font-size: var(--oct-text-sm); color: var(--oct-muted); }

/* diagram — nodes and edges. The app supplies coordinates when it has
   them; without them the nodes fall into a wrapping row, which is a
   worse diagram but still a readable list of parts. */
.cdiagram { position: relative; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); padding: var(--oct-space-4); }
.cdiagram-svg { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.cdiagram-edge { fill: none; stroke: var(--oct-chart-axis); stroke-width: 1; }
.cdiagram-nodes { position: relative; display: flex; flex-wrap: wrap; gap: var(--oct-space-3); }
.cnode { padding: var(--oct-space-2) var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-bg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg-2); }
.cnode.is-primary { border-color: var(--oct-accent-fg); color: var(--oct-accent-fg); }

/* image — the figure and its caption travel together */
.cfigure { margin: 0; }
.cfigure img, .cfigure .ph-img { display: block; width: 100%; border-radius: var(--oct-radius-md); }
.cfigure figcaption { margin-top: var(--oct-space-2); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* summary — a compacted object from elsewhere in the app */
.csummary { display: flex; align-items: flex-start; gap: var(--oct-space-3); padding: var(--oct-space-3); border: 1px solid var(--oct-border-soft); border-radius: var(--oct-radius-md); background: var(--oct-surface); }
.csummary .csummary-kind { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.csummary .csummary-text { margin: 2px 0 0; font-size: var(--oct-text-sm); color: var(--oct-fg-2); }

/* ─── revisions ───────────────────────────────────────────────────────
   A canvas is revised by prompt, not by hand, so the history is a list of
   prompts. The sequence number is the concurrency check the app actually
   sends, so it is shown rather than hidden. */
.revlist { margin: 0; padding: 0; list-style: none; }
.revlist li { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: var(--oct-space-1) var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.revlist li:first-child { border-top: 0; }
.rev-seq { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-meta); }
.rev-prompt { font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg-2); overflow-wrap: anywhere; }
.rev-meta { grid-column: 2; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.revlist li.is-current .rev-seq { color: var(--oct-accent-fg); }

.canvas-revise { display: flex; flex-direction: column; gap: var(--oct-space-2); }
.canvas-revise .revise-foot { display: flex; align-items: center; gap: var(--oct-space-3); }
.canvas-revise .revise-foot .revise-gap { flex: 1; }

/* ─── skill provenance ────────────────────────────────────────────────
   Read-only by design: showing where a layout came from must never look
   like a control that could change it, so there is nothing clickable in
   here and the whole panel sits at meta weight. */
.provenance { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); padding: var(--oct-space-4); }
.provenance-title { display: flex; align-items: center; gap: var(--oct-space-2); margin: 0 0 var(--oct-space-3); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.provenance-title .icon { width: 15px; height: 15px; color: var(--oct-accent-fg); }
.provenance .kv dd { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.provenance-set { margin-top: var(--oct-space-3); }
.provenance-set .provenance-label { display: block; margin-bottom: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.provenance-set ul { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); margin: 0; padding: 0; list-style: none; }

/* ══════════════════════════════════════════════════════════════════════
   ZEN

   Zen is not another panel layout. It is a spatial workspace: elements
   carry their own x, y, width, height and z-index, and the user arranges
   them. So this layer styles a floating element and gets out of the way
   — the positioning comes from the app, written to custom properties.

   Six element kinds: thread, notes, checklist, timer, reference, recipe.
   Every one of them can be minimised and locked, so those two states
   live on the frame rather than in each widget.
   ══════════════════════════════════════════════════════════════════════ */

.zen { position: relative; min-height: 420px; overflow: hidden; border-radius: var(--oct-radius-lg); background: var(--oct-bg); }
/* the ground. Faint enough to read as texture rather than as a grid the
   elements are supposed to snap to — they do not snap. Mixed from --oct-fg
   rather than reusing --oct-border: a hairline colour is tuned for a
   continuous 1px line, and the same value scattered as dots disappears. */
.zen-ground { position: absolute; inset: 0; background-image: radial-gradient(color-mix(in oklab, var(--oct-fg) 20%, transparent) 1px, transparent 1.2px); background-size: 22px 22px; }

.zen-el { position: absolute; left: calc(var(--x, 0) * 1px); top: calc(var(--y, 0) * 1px); width: calc(var(--w, 240) * 1px); z-index: var(--z, 1); display: flex; flex-direction: column; border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); overflow: hidden; }
.zen-el-head { display: flex; align-items: center; gap: var(--oct-space-2); min-height: 30px; padding-inline: var(--oct-space-3); border-bottom: 1px solid var(--oct-border-soft); background: var(--oct-surface-warm); cursor: grab; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.zen-el-head .icon { width: 13px; height: 13px; }
.zen-el-head .zen-el-gap { flex: 1; }
.zen-el-body { padding: var(--oct-space-3); min-height: 0; overflow: auto; font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
/* the corner is the only resize affordance, and it is decoration until
   the app binds a pointer handler to it */
.zen-el-grip { position: absolute; right: 0; bottom: 0; width: 14px; height: 14px; cursor: nwse-resize; background: linear-gradient(135deg, transparent 50%, var(--oct-border) 50%); }

/* minimised keeps the title bar and drops everything else, so the
   element stays findable and stays where the user put it */
.zen-el[data-minimized="true"] .zen-el-body,
.zen-el[data-minimized="true"] .zen-el-grip { display: none; }
/* locked is stated, not implied: the cursor changes and a mark appears,
   because an element that silently refuses to move reads as broken */
.zen-el[data-locked="true"] .zen-el-head { cursor: default; }
.zen-el[data-locked="true"] .zen-el-grip { display: none; }
.zen-el[data-locked="true"] { border-style: dashed; }

.zen-notes textarea { width: 100%; min-height: 96px; padding: 0; border: 0; background: none; color: inherit; font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); line-height: var(--oct-leading-body); resize: none; }
.zen-notes textarea:focus-visible { box-shadow: none; }

.zen-check { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--oct-space-2); }
.zen-check li { display: flex; align-items: flex-start; gap: var(--oct-space-2); }
.zen-check li.is-done { color: var(--oct-meta); text-decoration: line-through; }

/* the timer is the one widget whose whole point is the number, so it
   gets display type and tabular figures */
.zen-timer { text-align: center; }
.zen-timer-clock { font-family: var(--oct-font-display); font-size: var(--oct-text-3xl); font-variant-numeric: tabular-nums; line-height: 1; color: var(--oct-fg); }
.zen-timer[data-status="running"] .zen-timer-clock { color: var(--oct-accent-fg); }
.zen-timer[data-status="completed"] .zen-timer-clock { color: var(--oct-success-fg); }
.zen-timer-bar { height: 3px; margin: var(--oct-space-3) 0; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); overflow: hidden; }
.zen-timer-bar span { display: block; height: 100%; width: calc(var(--v, 0) * 100%); background: var(--oct-accent); }
.zen-timer-acts { display: flex; justify-content: center; gap: var(--oct-space-2); }

.zen-ref { display: flex; align-items: flex-start; gap: var(--oct-space-2); }
.zen-ref .zen-ref-host { display: block; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* ─── orbit bar ───────────────────────────────────────────────────────
   The one piece of chrome in Zen. It floats over the workspace rather
   than docking, because docking would carve a strip out of a surface
   whose whole point is that the user owns the space. */
.zen-bar { position: absolute; left: 50%; bottom: var(--oct-space-5); transform: translateX(-50%); z-index: var(--oct-z-overlay); display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-2); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-pill); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); }
.zen-bar .btn-icon { border-radius: var(--oct-radius-pill); }
.zen-bar-sep { width: 1px; align-self: stretch; margin-inline: var(--oct-space-1); background: var(--oct-border); }
.zen-bar-ask { min-width: 200px; height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); }
.zen-bar-model { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); padding-inline: var(--oct-space-2); white-space: nowrap; }
/* collapsed to a pill — the bar's own minimised state */
.zen-pill { position: absolute; left: 50%; bottom: var(--oct-space-5); transform: translateX(-50%); z-index: var(--oct-z-overlay); display: flex; align-items: center; gap: var(--oct-space-1); padding: var(--oct-space-1); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-pill); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); }

/* ══════════════════════════════════════════════════════════════════════
   AGENT RUNS

   A run owns nested subagent runs, so the natural shape is a tree. It is
   not .tree: that one is a file browser, where a row is a name. Here a
   row carries a status, a role, a route, and sometimes a demand for
   acknowledgement, and all of those have to be readable at a glance
   down a column.

   Rows are split into two buckets, active and history, because the
   question being asked of this surface is almost always "what is running
   right now".
   ══════════════════════════════════════════════════════════════════════ */

.runtree { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.runtree-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-2) var(--oct-space-3); padding: var(--oct-space-3) var(--oct-space-4); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); }
.runtree-head h3 { font-family: var(--oct-font-display); font-size: var(--oct-text-sm); margin: 0; }
.runtree-head .runtree-gap { flex: 1; }
.runtree-counts { display: flex; gap: var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-meta); white-space: nowrap; }
/* a bucket label rather than a second component — the rows are identical */
.runtree-bucket { padding: var(--oct-space-2) var(--oct-space-4); border-top: 1px solid var(--oct-border-soft); background: var(--oct-bg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.runtree-bucket:first-child { border-top: 0; }

.run-row { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: var(--oct-space-1) var(--oct-space-3); width: 100%; padding: var(--oct-space-3) var(--oct-space-4); padding-left: calc(var(--oct-space-4) + var(--depth, 0) * var(--oct-tree-indent)); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; }
.run-row:hover { background: var(--oct-fg-soft); }
.run-row .st { margin-top: 5px; }
.run-main { min-width: 0; }
.run-task { display: block; font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg); overflow-wrap: anywhere; }
.run-meta { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); margin-top: 2px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
/* the role is what distinguishes two runs doing the same task, so it
   leads the meta line rather than hiding at the end */
.run-role { color: var(--oct-fg-2); }
/* a fallback route is the single most surprising thing that can happen
   to a run — the model that answered was not the model that was asked
   for — so it is marked, not merely recorded */
.run-route.is-fallback { color: var(--oct-warn-fg); }
/* pinned to the first row so a recovery note underneath cannot push the
   status badge out of line with the task it belongs to */
.run-right { grid-column: 3; grid-row: 1; display: flex; align-items: center; gap: var(--oct-space-2); flex: 0 0 auto; }
/* a run whose result nobody has acknowledged. It is a claim on the
   user's attention, so it survives being scrolled past. */
.run-ack { display: inline-flex; align-items: center; gap: var(--oct-space-1); padding: 2px var(--oct-space-2); border-radius: var(--oct-radius-pill); background: var(--oct-warn-soft); color: var(--oct-warn-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.run-recovery { grid-column: 2; margin-top: var(--oct-space-1); padding: var(--oct-space-2); border-radius: 6px; background: var(--oct-danger-soft); color: var(--oct-danger-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
/* the whole panel while the stream is re-establishing. Dashed, because
   the rows may be stale and the user should not trust them yet. */
.runtree.is-reconnecting { outline: 1px dashed var(--oct-warn-fg); outline-offset: -1px; }

/* ══════════════════════════════════════════════════════════════════════
   LINKED THREADS

   One thread spawns others — a review split across three, a task
   continued on another host. Two surfaces: the receipt you get at the
   moment of creation, and the aggregate view over all of them.
   ══════════════════════════════════════════════════════════════════════ */

/* the receipt is a record, not a notification: it says what was created
   and where, and it stays put */
.receipt { border: 1px solid var(--oct-border); border-left: 2px solid var(--oct-success-fg); border-radius: var(--oct-radius-md); background: var(--oct-surface); padding: var(--oct-space-4); }
.receipt-head { display: flex; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-3); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.receipt-head .icon { width: 15px; height: 15px; color: var(--oct-success-fg); }
.receipt-list { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: var(--oct-space-2); }
.receipt-list li { display: flex; align-items: center; gap: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.receipt-list .receipt-name { color: var(--oct-fg-2); }

/* the aggregate. The parent is stated once at the top and the children
   are indented under it, so the fan-out is visible as a shape. */
.linkmap { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.linkmap-parent { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-3) var(--oct-space-4); background: var(--oct-surface-warm); border-bottom: 1px solid var(--oct-border); }
.linkmap-parent .linkmap-title { font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.linkrow { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-3) var(--oct-space-4); padding-left: calc(var(--oct-space-4) + var(--oct-space-5)); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; text-decoration: none; }
.linkrow:first-of-type { border-top: 0; }
.linkrow:hover { background: var(--oct-fg-soft); }
.linkrow .linkrow-main { min-width: 0; flex: 1; }
.linkrow .linkrow-title { display: block; font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.linkrow .linkrow-meta { display: flex; flex-wrap: wrap; gap: var(--oct-space-1) var(--oct-space-3); margin-top: 2px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
/* a thread that lives on another machine. The host is the fact that
   changes what the user can do next, so it is never abbreviated away. */
.linkrow-host { display: inline-flex; align-items: center; gap: var(--oct-space-1); color: var(--oct-accent-fg); }
.linkrow-host .icon { width: 12px; height: 12px; }

/* ══════════════════════════════════════════════════════════════════════
   REMOTE AND COMPUTER USE

   Two surfaces where the app is driving something the user can see but
   is not touching. Both need the same thing above all: an unambiguous
   statement of whether it is live right now.
   ══════════════════════════════════════════════════════════════════════ */

/* pairing. A short code typed into a card, so the card is narrow and
   centred and the code field is the largest thing in it. */
.pair { max-width: 400px; margin-inline: auto; padding: var(--oct-space-6); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); text-align: center; }
.pair-title { margin: 0 0 var(--oct-space-2); font-family: var(--oct-font-display); font-size: var(--oct-text-lg); }
.pair-desc { margin: 0 0 var(--oct-space-5); font-size: var(--oct-text-sm); color: var(--oct-muted); }
.pair-code { width: 100%; height: 56px; text-align: center; font-family: var(--oct-font-mono); font-size: var(--oct-text-2xl); letter-spacing: 0.28em; text-indent: 0.28em; }
/* the host being paired with, shown before the user commits. Fingerprint
   in mono at full length: a truncated fingerprint cannot be compared,
   which is the only reason to show one. */
.pair-host { display: flex; flex-direction: column; gap: var(--oct-space-1); margin-bottom: var(--oct-space-5); padding: var(--oct-space-3); border-radius: var(--oct-radius-md); background: var(--oct-bg); }
.pair-host .pair-host-name { font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.pair-host .pair-host-fp { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); overflow-wrap: anywhere; }

/* the remote screen. Kept at 16:10 so the frame does not jump when a
   session connects and the real dimensions arrive. */
.screen { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.screen-bar { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-3); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.screen-bar .screen-gap { flex: 1; }
/* live is a word plus a pulsing dot, never the dot alone */
.screen-live { display: inline-flex; align-items: center; gap: var(--oct-space-1); color: var(--oct-danger-fg); }
.screen-live::before { content: ''; width: 7px; height: 7px; border-radius: var(--oct-radius-pill); background: currentColor; animation: oct-pulse 1.6s ease-in-out infinite; }
@keyframes oct-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }
.screen-view { aspect-ratio: 16 / 10; display: grid; place-items: center; background: var(--oct-bg); color: var(--oct-meta); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.screen-view img { width: 100%; height: 100%; object-fit: contain; }

/* the activity stream beside it. One line per action the agent took, so
   a user who looked away can reconstruct what happened. */
.actlog { margin: 0; padding: 0; list-style: none; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.actlog li { display: grid; grid-template-columns: auto auto minmax(0, 1fr); gap: var(--oct-space-3); align-items: baseline; padding: var(--oct-space-2) 0; border-top: 1px solid var(--oct-border-soft); color: var(--oct-fg-2); }
.actlog li:first-child { border-top: 0; }
.actlog .actlog-at { color: var(--oct-meta); font-variant-numeric: tabular-nums; }
.actlog .actlog-act { color: var(--oct-accent-fg); }
.actlog .actlog-target { overflow-wrap: anywhere; }
/* an action that is waiting on the user. It stops the log visually,
   which is the point: nothing after it has happened yet. */
.actlog li.is-blocked { color: var(--oct-warn-fg); }
.actlog li.is-blocked .actlog-act { color: inherit; }

/* ══════════════════════════════════════════════════════════════════════
   ENVIRONMENT AND PROVIDERS

   What a thread is bound to: a folder, a repository, a set of providers.
   These are settings, but they are per-thread and consulted mid-task, so
   they sit in a panel beside the work rather than in the settings screen.
   ══════════════════════════════════════════════════════════════════════ */

.envpanel { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.envgroup { border-top: 1px solid var(--oct-border-soft); padding: var(--oct-space-4); }
.envgroup:first-child { border-top: 0; }
.envgroup > .envgroup-label { display: block; margin-bottom: var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.envrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--oct-space-3); padding-block: var(--oct-space-2); }
.envrow .envrow-name { font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.envrow .envrow-value { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); overflow-wrap: anywhere; }

/* the working folder. A path is long and the end of it is the part that
   identifies it, so the field scrolls to its end rather than eliding.

   direction: rtl is what anchors the overflow to the tail. On its own it
   also reorders the neutral characters at each edge, which moves a
   leading slash to the far right and quietly corrupts the path. Adding
   unicode-bidi: plaintext takes the direction from the first strong
   character instead — a letter, so the path renders left to right — while
   the rtl anchoring survives. */
.pathctl { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--oct-space-2); }
.pathctl .input { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); direction: rtl; unicode-bidi: plaintext; text-align: left; }

.gitgroup { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-2) var(--oct-space-4); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.gitgroup .gitgroup-item { display: inline-flex; align-items: center; gap: var(--oct-space-1); }
.gitgroup .gitgroup-item .icon { width: 13px; height: 13px; }
.gitgroup .is-dirty { color: var(--oct-warn-fg); }

/* provider discovery. Each row is a candidate the app found, not one the
   user configured, so the row states how it was found. */
.discovery { margin: 0; padding: 0; list-style: none; }
.discovery li { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.discovery li:first-child { border-top: 0; }
.discovery .discovery-main { min-width: 0; flex: 1; }
.discovery .discovery-name { display: block; font-size: var(--oct-text-sm); color: var(--oct-fg); }
.discovery .discovery-how { display: block; margin-top: 1px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* the pool: which providers a thread may fall back to, in order. The
   order is the contract, so it is numbered. */
.pool { margin: 0; padding: 0; list-style: none; counter-reset: pool; }
.pool li { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-2) 0; }
.pool li::before { counter-increment: pool; content: counter(pool); display: grid; place-items: center; width: 20px; height: 20px; flex: 0 0 auto; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.pool li.is-active::before { background: var(--oct-accent); color: var(--oct-accent-on); }
.pool .pool-name { flex: 1; font-size: var(--oct-text-sm); color: var(--oct-fg-2); }

/* ─── composer autocomplete ───────────────────────────────────────────
   Anchored to the composer, not to the pointer, and it never covers the
   line being typed — it opens upward, because the composer sits at the
   bottom of the window. */
.autocomplete { position: absolute; bottom: calc(100% + var(--oct-space-2)); left: 0; right: 0; max-height: 240px; overflow-y: auto; z-index: var(--oct-z-overlay); padding: var(--oct-space-1); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); box-shadow: var(--oct-elev-raised); }
.ac-item { display: flex; align-items: baseline; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-2) var(--oct-space-3); border: 0; border-radius: 6px; background: none; color: var(--oct-fg-2); text-align: left; font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); }
.ac-item[aria-selected="true"] { background: var(--oct-fg-soft); color: var(--oct-fg); }
.ac-item .ac-hint { margin-left: auto; color: var(--oct-meta); font-size: var(--oct-text-xs); }
/* the characters that matched what was typed */
.ac-item mark { background: none; color: var(--oct-accent-fg); }

/* ══════════════════════════════════════════════════════════════════════
   AGENCY

   The surfaces that make this an agent app rather than an editor with a
   chat pane. They share one rule: an agent acting on the user's machine
   must never be ambiguous about what it is about to do, what it has
   already done, and what it is spending.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── approval ────────────────────────────────────────────────────────
   The most consequential control in the product. It interrupts, because
   the alternative is an agent that acts and apologises.

   Three rules it must not break. The command is shown verbatim and
   selectable — never summarised, because the summary is what an attacker
   would control. Allow is not the primary button: the primary button is
   the safe one, so a reflexive Return does not grant authority. And the
   scope of what is being granted is stated in words next to the choice,
   since "allow" alone does not say for how long. */
.approval { border: 1px solid var(--oct-warn-fg); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.approval-head { display: flex; align-items: center; gap: var(--oct-space-2); padding: var(--oct-space-3) var(--oct-space-4); background: var(--oct-warn-soft); color: var(--oct-warn-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); }
.approval-head .icon { width: 16px; height: 16px; flex: 0 0 auto; }
.approval-body { padding: var(--oct-space-4); }
.approval-what { margin: 0 0 var(--oct-space-3); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
/* verbatim, selectable, and never truncated by ellipsis — it wraps */
.approval-cmd { margin: 0 0 var(--oct-space-4); padding: var(--oct-space-3); border-radius: var(--oct-radius-md); background: var(--oct-bg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); line-height: var(--oct-leading-code); color: var(--oct-fg); user-select: text; overflow-wrap: anywhere; white-space: pre-wrap; }
.approval-scope { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); margin-bottom: var(--oct-space-4); }
.approval-acts { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); }
.approval-acts .approval-gap { flex: 1; }
/* the granting button is secondary on purpose. Making it primary would
   put the accent — the thing the eye lands on — on the irreversible
   choice. */
.approval-allow { border-color: var(--oct-warn-fg); color: var(--oct-warn-fg); }
.approval-allow:hover { background: var(--oct-warn-soft); border-color: var(--oct-warn-fg); color: var(--oct-warn-fg); }
/* what happened, once it is decided */
.approval[data-decision] { border-color: var(--oct-border); }
.approval[data-decision] .approval-head { background: none; color: var(--oct-meta); border-bottom: 1px solid var(--oct-border-soft); }
.approval[data-decision="denied"] .approval-head { color: var(--oct-danger-fg); }

/* ─── context ─────────────────────────────────────────────────────────
   How the window is spent. The app models this properly — fourteen
   categories, five postures, seven states, six health levels — and the
   design's job is to make the one question answerable at a glance:
   is there room, and if not, what is taking it.

   The bar is the whole window, not the used part, so the empty space is
   visible. A meter that fills to 100% of itself cannot show headroom. */
.ctxbar { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-2) var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); color: inherit; text-align: left; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); }
.ctxbar:hover { background: var(--oct-surface-warm); }
.ctxbar .ctxbar-gap { flex: 1; }
.ctxbar .ctxbar-usage { color: var(--oct-fg-2); font-variant-numeric: tabular-nums; }
.ctxbar .ctxbar-health { display: inline-flex; align-items: center; gap: var(--oct-space-1); }
/* health drives colour and a word. The word is the accessible answer;
   the colour is the glanceable one. */
.ctxbar[data-health="healthy"]      .ctxbar-health { color: var(--oct-success-fg); }
.ctxbar[data-health="watch"]        .ctxbar-health,
.ctxbar[data-health="optimizing"]   .ctxbar-health { color: var(--oct-warn-fg); }
.ctxbar[data-health="action-needed"] .ctxbar-health,
.ctxbar[data-health="blocked"]      .ctxbar-health,
.ctxbar[data-health="rate-limited"] .ctxbar-health { color: var(--oct-danger-fg); }

/* the segmented window. Each segment is a category, sized by --v as a
   fraction of the whole budget; whatever is left over is headroom. */
.ctxmeter { display: flex; height: 8px; border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); overflow: hidden; }
.ctxmeter .seg { width: calc(var(--v, 0) * 100%); background: var(--oct-ser, var(--oct-muted)); }
.ctxmeter .seg + .seg { border-left: 1px solid var(--oct-surface); }
/* reserved space is real but not spent, so it is drawn as a hatch rather
   than a fill — the difference between "gone" and "spoken for" */
.ctxmeter .seg.is-reserved { background: repeating-linear-gradient(45deg, var(--oct-meta) 0 2px, transparent 2px 5px); }

.ctxlist { margin: 0; padding: 0; list-style: none; }
.ctxlist li { display: grid; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: baseline; gap: var(--oct-space-3); padding: var(--oct-space-2) 0; border-top: 1px solid var(--oct-border-soft); }
.ctxlist li:first-child { border-top: 0; }
.ctxlist .ctx-key { width: 10px; height: 10px; border-radius: 3px; background: var(--oct-ser, var(--oct-muted)); align-self: center; }
.ctxlist .ctx-name { font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.ctxlist .ctx-sub { display: block; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.ctxlist .ctx-tokens { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-fg-2); }
/* an entry the app could not measure. Shown rather than assumed to be
   zero, because an unmeasured entry is exactly what overruns a budget. */
.ctxlist .ctx-tokens.is-unknown { color: var(--oct-warn-fg); }
/* state is a posture, not an error: summarized and referenced are the
   system working, so they sit at meta weight and do not raise alarm */
.ctxlist li[data-state="omitted"] .ctx-name { color: var(--oct-meta); text-decoration: line-through; }

/* ─── plan ────────────────────────────────────────────────────────────
   The agent's own checklist. It is the clearest signal a user has that
   the agent understood the task, so it is a first-class object in the
   transcript rather than a tool result — the one place the agent states
   intent before spending money on it. */
.plan { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); padding: var(--oct-space-3) var(--oct-space-4); }
.plan-head { display: flex; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.plan-head .plan-gap { flex: 1; }
.plan-list { margin: 0; padding: 0; list-style: none; }
.plan-list li { display: grid; grid-template-columns: 14px minmax(0, 1fr); gap: var(--oct-space-2); padding-block: var(--oct-space-1); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
/* deliberate override: the dot sits in a 14px gutter beside small text, and
   at the standard 8px it reads as a bullet rather than as a state */
.plan-list .st { align-self: center; width: 7px; height: 7px; margin-inline: 3px; }
.plan-list li[data-state="done"]    { color: var(--oct-meta); text-decoration: line-through; text-decoration-color: var(--oct-border); }
/* exactly one item is in progress at a time, and it is the only one that
   gets weight — the plan should answer "where are we" in one glance */
.plan-list li[data-state="active"]  { color: var(--oct-fg); }

/* ─── thinking ────────────────────────────────────────────────────────
   Reasoning, collapsed. It is not prose the user asked for and it is not
   a tool call, so it gets its own quiet register: italic, muted, and shut
   by default. Opening it must never be required to follow the answer. */
.thinking { border-left: 2px solid var(--oct-border); padding-left: var(--oct-space-4); }
.thinking > summary { display: flex; align-items: center; gap: var(--oct-space-2); cursor: pointer; list-style: none; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.thinking > summary::-webkit-details-marker { display: none; }
.thinking > summary .chev { transition: transform var(--oct-motion-fast) ease; }
.thinking[open] > summary .chev { transform: rotate(90deg); }
.thinking-body { margin-top: var(--oct-space-2); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); font-style: italic; line-height: var(--oct-leading-body); color: var(--oct-muted); }

/* ─── subagent ────────────────────────────────────────────────────────
   A run the agent spawned, seen from inside the parent's transcript. It
   is deliberately a card and not a tool row: a tool row is something that
   happened, and this is something still happening somewhere else. */
.subagent { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: var(--oct-space-1) var(--oct-space-3); padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); }
.subagent > .icon { grid-row: span 2; width: 17px; height: 17px; margin-top: 1px; color: var(--oct-accent-fg); }
.subagent-task { font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.subagent-meta { grid-column: 2; display: flex; flex-wrap: wrap; gap: var(--oct-space-1) var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.subagent-right { grid-row: span 2; }

/* ─── checkpoints ─────────────────────────────────────────────────────
   Every turn is a point the workspace can be returned to. The rail makes
   that legible as a line you can walk back along, and the destructive
   truth is stated on the control: rewinding discards what came after. */
.ckpts { margin: 0; padding: 0 0 0 var(--oct-space-5); list-style: none; position: relative; }
.ckpts::before { content: ''; position: absolute; inset-block: 8px; left: 3px; width: 1px; background: var(--oct-border); }
.ckpt { position: relative; display: flex; align-items: baseline; gap: var(--oct-space-3); padding-block: var(--oct-space-2); }
.ckpt::before { content: ''; position: absolute; left: calc(-1 * var(--oct-space-5) + 3px); top: 10px; transform: translateX(-50%); width: 7px; height: 7px; border-radius: var(--oct-radius-pill); background: var(--oct-border); box-shadow: 0 0 0 3px var(--oct-bg); }
.ckpt.is-current::before { background: var(--oct-accent); }
.ckpt-label { flex: 1; min-width: 0; font-size: var(--oct-text-sm); color: var(--oct-fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ckpt-at { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); font-variant-numeric: tabular-nums; color: var(--oct-meta); }
.ckpt .btn { opacity: 0; }
.ckpt:hover .btn, .ckpt:focus-within .btn { opacity: 1; }

/* ─── extensions, skills, MCP ─────────────────────────────────────────
   Third-party code the user is deciding whether to trust, so every card
   leads with where it came from and what it may reach. */
.extcard { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--oct-space-1) var(--oct-space-3); padding: var(--oct-space-4); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); }
.extcard > .icon-mark { grid-row: span 3; }
.extcard-name { font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.extcard-src { grid-column: 2; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.extcard-desc { grid-column: 2 / -1; margin: var(--oct-space-2) 0 0; font-size: var(--oct-text-sm); color: var(--oct-muted); }
/* what it can reach. This is the part a user actually has to read, so it
   is a list of named capabilities rather than a count. */
.extcard-scopes { grid-column: 2 / -1; display: flex; flex-wrap: wrap; gap: var(--oct-space-2); margin-top: var(--oct-space-3); }
.extcard-right { grid-row: span 2; }

/* a connected server, its transport, and whether it is actually up */
.mcplist { margin: 0; padding: 0; list-style: none; }
.mcplist li { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.mcplist li:first-child { border-top: 0; }
.mcplist .mcp-main { flex: 1; min-width: 0; }
.mcplist .mcp-name { display: block; font-size: var(--oct-text-sm); color: var(--oct-fg); }
.mcplist .mcp-transport { display: block; margin-top: 1px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); overflow-wrap: anywhere; }
.mcplist .mcp-tools { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); font-variant-numeric: tabular-nums; }

/* ─── settings ────────────────────────────────────────────────────────
   The settings screen is a pane inside the shell, not a modal over it.
   An agent's settings are grants of authority and spending limits, and
   people arrive at them mid-task to check what a thread is allowed to
   do — a dialog that has to be dismissed to look at the work is the
   wrong shape for that.

   The layout is a nav column and one scrolling pane. Two columns, never
   three: a settings screen deep enough to need a third is a settings
   screen that needs fewer settings. */
.settings { display: grid; grid-template-columns: 196px minmax(0, 1fr); min-height: 0; flex: 1; }
.setnav { display: flex; flex-direction: column; gap: 2px; min-width: 0; padding: var(--oct-space-4) var(--oct-space-2); border-right: 1px solid var(--oct-border); background: var(--oct-surface); overflow: hidden auto; scrollbar-width: thin; }
.setnav-section { margin-top: var(--oct-space-4); padding: var(--oct-space-1) var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); white-space: nowrap; }
.setnav-section:first-child { margin-top: 0; }
.setnav-item { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; min-height: var(--oct-control-height-sm); padding-inline: var(--oct-space-2); border: 0; border-radius: 6px; background: none; color: var(--oct-muted); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); text-align: left; text-decoration: none; white-space: nowrap; }
.setnav-item > .icon { flex: 0 0 auto; }
.setnav-item:hover { background: var(--oct-fg-soft); color: var(--oct-fg); }
.setnav-item[aria-current="page"] { background: var(--oct-fg-soft); color: var(--oct-fg); }

.setpane { min-width: 0; padding: var(--oct-space-6); overflow: hidden auto; scrollbar-width: thin; }
.setpane-title { margin: 0; font-family: var(--oct-font-display); font-size: var(--oct-text-lg); color: var(--oct-fg); }
/* measured, not decorative: settings prose is read once and has to land,
   so it gets a real line length rather than the width of the pane */
.setpane-note { max-width: 64ch; margin: var(--oct-space-2) 0 var(--oct-space-6); font-size: var(--oct-text-sm); color: var(--oct-muted); }

.setgroup { border: 1px solid var(--oct-border); border-radius: var(--oct-radius-lg); background: var(--oct-surface); overflow: hidden; }
.setgroup + .setgroup { margin-top: var(--oct-space-6); }
.setgroup-head { display: flex; align-items: center; gap: var(--oct-space-3); padding: var(--oct-space-3) var(--oct-space-4); border-bottom: 1px solid var(--oct-border); background: var(--oct-surface-warm); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.setgroup-head .setgroup-gap { flex: 1; }
.setgroup-foot { padding: var(--oct-space-3) var(--oct-space-4); border-top: 1px solid var(--oct-border); }

/* label and hint on the left, control on the right, and the control is
   vertically centred against both lines rather than against the label —
   a switch that floats level with the first line of a two-line
   explanation reads as belonging only to that line.

   The two rows are declared rather than left implicit, and that is load
   bearing. grid-row: 1 / -1 counts -1 from the last line of the EXPLICIT
   grid, so with no grid-template-rows the span collapses to row 1 and
   the control silently stops spanning — it still looks plausible,
   because row 1 then stretches to the control's own height. Declaring
   the tracks is what makes the span mean what it reads as.

   The gap between the two lines is the hint's margin rather than a
   row-gap, so a row with no hint gets an empty second track of zero
   height instead of a stray four pixels. */
.setrow { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto auto; align-items: center; column-gap: var(--oct-space-6); padding: var(--oct-space-4); border-top: 1px solid var(--oct-border-soft); }
.setrow:first-child { border-top: 0; }
.setrow-label { font-size: var(--oct-text-sm); color: var(--oct-fg); }
.setrow-hint { grid-column: 1; margin: 2px 0 0; max-width: 58ch; font-size: var(--oct-text-xs); color: var(--oct-muted); }
/* A control may ask for a comfortable width — a key field wants to show
   more than eight characters — but never one wider than the row holding
   it. max-width beats width, so a declared width becomes a preference
   rather than a demand, and the pair of an input and its Test button
   wraps instead of pushing out of the pane. */
.setrow-control { grid-column: 2; grid-row: 1 / -1; justify-self: end; max-width: 100%; }
.setrow-control .input, .setrow-control .select, .setrow-control .textarea { max-width: 100%; }
.setrow-control .row { flex-wrap: wrap; justify-content: flex-end; }

/* ─── providers ───────────────────────────────────────────────────────
   A provider is a place models come from. The row carries two states
   that people constantly conflate, and the design's job is to keep them
   apart:

     the switch  is INTENT — may Octant use this at all
     the badge   is FACT   — is it reachable right now

   Both matter, and neither implies the other. A provider can be on and
   unreachable (the key expired), or off and perfectly healthy. Folding
   them into one green light is how you get a user staring at "Ready"
   wondering why nothing routes to it. So when a provider is off the
   badge is hidden rather than recoloured, because reachability is not a
   question worth answering about something that will not be used — but
   the space is kept, so the rows do not jump as you toggle down a list.

   Order is meaningful: it is the fallback order, the thing the
   "Provider unreachable" callout means by "the next provider in your
   order". That is why every row has a grip and why the list is a list
   rather than a set of cards. */
.provlist { margin: 0; padding: 0; list-style: none; }
.provrow { display: grid; grid-template-columns: auto auto minmax(0, 1fr) auto auto auto; align-items: center; gap: 0 var(--oct-space-3); padding: var(--oct-space-3) var(--oct-space-4); border-top: 1px solid var(--oct-border-soft); }
.provrow:first-child { border-top: 0; }
/* 24px, not the 16px the icon wants: this is a drag handle, the hardest
   target in the row to hit, and 24 is the floor WCAG 2.5.8 sets for a
   pointer target. The icon stays small and the box around it grows. */
.prov-grip { display: grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 5px; background: none; color: var(--oct-meta); cursor: grab; }
.prov-grip:hover { background: var(--oct-fg-soft); color: var(--oct-fg-2); }
.prov-grip:active { cursor: grabbing; }
.prov-main { grid-column: 3; min-width: 0; }
.prov-name { display: block; font-family: var(--oct-font-display); font-size: var(--oct-text-sm); color: var(--oct-fg); }
.prov-meta { display: block; margin-top: 1px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); overflow-wrap: anywhere; }
.prov-models { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); font-variant-numeric: tabular-nums; white-space: nowrap; }
.prov-status { display: grid; place-items: center; }

/* off keeps everything: the key, the endpoint, the place in the order.
   Turning a provider back on must never be a re-setup, so this dims the
   identity and touches nothing else. The switch stays at full strength —
   it is the one thing you came to the row to press. */
.provrow[data-enabled="false"] > .icon-mark,
.provrow[data-enabled="false"] .prov-main,
.provrow[data-enabled="false"] .prov-models { opacity: var(--oct-disabled-opacity); }
.provrow[data-enabled="false"] .prov-status { visibility: hidden; }

/* ── settings, narrow ─────────────────────────────────────────────────
   Below the medium breakpoint the nav and the pane are fighting over a
   few hundred pixels, and the nav wins by being a fixed width — at phone
   width it took 196 of 273 and left a pane too narrow to hold a single
   setting row. Worse, .app clips, so it failed silently rather than
   spilling where anyone would see it.

   So the nav stops being a column and becomes a strip above the pane,
   scrolling sideways. Its group headings go: they are headings for a
   column, and laid out on one line among the items they read as items
   you could press. */
@media (max-width: 720px) {
  .settings { grid-template-columns: minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
  .setnav { flex-direction: row; padding: var(--oct-space-2); border-right: 0; border-bottom: 1px solid var(--oct-border); overflow: auto hidden; overscroll-behavior-x: contain; scrollbar-width: none; }
  .setnav::-webkit-scrollbar { display: none; }
  /* width:100% is right for a column and nonsense in a row — it made
     every item as wide as the whole strip, so eight of them measured
     1990px and the current one could never be scrolled fully into view */
  .setnav-item { flex: 0 0 auto; width: auto; }
  .setnav-section { display: none; }
  .setpane { padding: var(--oct-space-4); }
}

@media (max-width: 560px) {
  /* the control stops being a right-hand column and sits under the text
     it belongs to, rather than squeezing a label into three lines */
  .setrow { grid-template-columns: minmax(0, 1fr); }
  .setrow-control { grid-column: 1; grid-row: auto; justify-self: start; margin-top: var(--oct-space-3); }

  /* Two lines instead of six columns. The switch stays on the first line
     beside the name, because it is the thing the row is for; the count
     and the badge drop underneath, where they still read as a pair.

     Six fields do not fit in 167px, so one has to go, and the choice is
     the mark rather than the grip. At this width the NAME is the
     identity — a 38px mark costs half the space the name needs to be
     read at all — while dropping the grip would remove the only way to
     change the fallback order. Decoration goes before function. */
  .provrow { grid-template-columns: auto minmax(0, 1fr) auto; column-gap: var(--oct-space-2); row-gap: var(--oct-space-2); }
  .provrow > .icon-mark { display: none; }
  .prov-grip { grid-area: 1 / 1; }
  .prov-main { grid-area: 1 / 2; }
  .provrow > input.switch { grid-area: 1 / 3; justify-self: end; }
  /* The badge gets its own line rather than a column. Sharing one with
     the switch let it size that column, so "UNREACHABLE" — the longest
     status and the one that matters most — squeezed the name it was
     reporting on down to 29px. A status must never be able to crowd out
     the thing whose status it is. */
  .prov-models { grid-area: 2 / 2 / 3 / -1; }
  .prov-status { grid-area: 3 / 2 / 4 / -1; justify-self: start; }
}

/* ─── memory ──────────────────────────────────────────────────────────
   What the agent remembers between threads. Scope is the whole point —
   a fact remembered for a project must never look like one remembered
   everywhere — so it leads the row rather than trailing it. */
.memlist { margin: 0; padding: 0; list-style: none; }
.memlist li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: baseline; gap: var(--oct-space-1) var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.memlist li:first-child { border-top: 0; }
/* full weight, not meta: this is the fact the row is about */
.mem-scope { padding: 1px var(--oct-space-2); border-radius: var(--oct-radius-pill); background: var(--oct-fg-soft); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg-2); white-space: nowrap; }
.mem-scope.is-global { background: var(--oct-accent-soft); color: var(--oct-accent-fg); }
.mem-text { font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); color: var(--oct-fg-2); overflow-wrap: anywhere; }
.mem-at { grid-column: 2; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* ─── goal ────────────────────────────────────────────────────────────
   A long-running objective with a budget. Budget-limited is its own
   status, not a variant of paused: the agent did not choose to stop. */
.goal { border: 1px solid var(--oct-border); border-left: 2px solid var(--oct-accent); border-radius: var(--oct-radius-md); background: var(--oct-surface); padding: var(--oct-space-4); }
.goal[data-status="paused"] { border-left-color: var(--oct-muted); }
.goal[data-status="budget-limited"] { border-left-color: var(--oct-warn-fg); }
.goal[data-status="complete"] { border-left-color: var(--oct-success-fg); }
.goal-head { display: flex; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.goal-head .goal-gap { flex: 1; }
.goal-text { margin: 0 0 var(--oct-space-3); font-family: var(--oct-font-transcript); font-size: var(--oct-text-base); color: var(--oct-fg); }
.goal-spend { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-2) var(--oct-space-4); margin-bottom: var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); font-variant-numeric: tabular-nums; }
.goal-acts { display: flex; flex-wrap: wrap; gap: var(--oct-space-2); }

/* ─── automations ─────────────────────────────────────────────────────
   Agents that run without anyone watching, which makes "when does this
   next fire" the load-bearing fact on the row. */
.autolist { margin: 0; padding: 0; list-style: none; }
.autolist li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); padding: var(--oct-space-3) 0; border-top: 1px solid var(--oct-border-soft); }
.autolist li:first-child { border-top: 0; }
.autolist > li > .icon { grid-row: span 2; color: var(--oct-meta); }
.auto-name { font-size: var(--oct-text-sm); color: var(--oct-fg); }
.auto-when { grid-column: 2; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.auto-next { grid-row: span 2; text-align: right; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.autolist li[data-enabled="false"] .auto-name,
.autolist li[data-enabled="false"] .auto-next { color: var(--oct-meta); }

/* ─── spend and limits ────────────────────────────────────────────────
   An agent spends money on the user's behalf while they are not looking,
   so the number is never buried in a settings page. */
.limits { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--oct-space-3); }
.limit { padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); }
.limit-label { display: flex; align-items: center; gap: var(--oct-space-1); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.limit-value { display: block; margin: var(--oct-space-1) 0 var(--oct-space-2); font-family: var(--oct-font-display); font-size: var(--oct-text-xl); font-variant-numeric: tabular-nums; color: var(--oct-fg); }
.limit-of { color: var(--oct-meta); font-size: var(--oct-text-sm); }
.limit[data-health="warn"] .limit-value { color: var(--oct-warn-fg); }
.limit[data-health="over"] .limit-value { color: var(--oct-danger-fg); }
/* when a limit resets. Without it a user cannot tell whether to wait or
   to change plan, which is the only decision the number supports. */
.limit-reset { display: block; margin-top: var(--oct-space-1); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* ─── hosts ───────────────────────────────────────────────────────────
   One user, several machines. Which one a thread is running on changes
   what it can reach, so the host is a first-class switch. */
.hostlist { margin: 0; padding: 0; list-style: none; }
.hostrow { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-3); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; }
.hostrow:first-child { border-top: 0; }
.hostrow:hover { background: var(--oct-fg-soft); }
.hostrow .host-main { flex: 1; min-width: 0; }
.hostrow .host-name { display: block; font-size: var(--oct-text-sm); color: var(--oct-fg); }
.hostrow .host-addr { display: block; margin-top: 1px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.hostrow[aria-current="true"] { background: var(--oct-fg-soft); }
.hostrow[aria-current="true"] .host-name { color: var(--oct-accent-fg); }

/* ─── search across threads ───────────────────────────────────────────
   A hit is only useful with its surroundings, so a row carries the line
   it matched rather than only the thread it lives in. */
.results { margin: 0; padding: 0; list-style: none; }
.result { display: block; width: 100%; padding: var(--oct-space-3) var(--oct-space-4); border: 0; border-top: 1px solid var(--oct-border-soft); background: none; color: inherit; text-align: left; text-decoration: none; }
.result:first-child { border-top: 0; }
.result:hover { background: var(--oct-fg-soft); }
.result-where { display: flex; flex-wrap: wrap; align-items: center; gap: var(--oct-space-1) var(--oct-space-3); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.result-line { display: block; margin-top: var(--oct-space-1); font-size: var(--oct-text-sm); color: var(--oct-fg-2); overflow-wrap: anywhere; }
.result-line mark { background: var(--oct-accent-soft); color: var(--oct-fg); border-radius: 3px; }

/* ─── keyboard reference ──────────────────────────────────────────────
   Two columns of chords. The keys are right-aligned into a rail so the
   eye can run down them without reading the labels. */
.keys { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 0 var(--oct-space-8); }
.keys-group { break-inside: avoid; margin-bottom: var(--oct-space-5); }
.keys-group > h4 { margin: 0 0 var(--oct-space-2); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); font-weight: 400; }
.keyrow { display: flex; align-items: baseline; gap: var(--oct-space-3); padding-block: var(--oct-space-1); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.keyrow .keyrow-gap { flex: 1; border-bottom: 1px dotted var(--oct-border); }
.keyrow .kbd { flex: 0 0 auto; }

/* ─── welcome ─────────────────────────────────────────────────────────
   An empty thread is a prompt, not a void. The suggestions are the
   product telling the user what it is for. */
.welcome { max-width: 46ch; margin: auto; padding-block: var(--oct-space-8); text-align: center; }
.welcome .icon-mark { margin: 0 auto var(--oct-space-5); }
.welcome h3 { margin-bottom: var(--oct-space-2); }
.welcome p { margin: 0 0 var(--oct-space-6); color: var(--oct-muted); }
.welcome-suggest { display: flex; flex-direction: column; gap: var(--oct-space-2); text-align: left; }
.welcome-suggest button { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-3); border: 1px solid var(--oct-border); border-radius: var(--oct-radius-md); background: var(--oct-surface); color: var(--oct-fg-2); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); text-align: left; }
.welcome-suggest button:hover { border-color: color-mix(in oklab, var(--oct-fg) 26%, transparent); background: var(--oct-surface-warm); color: var(--oct-fg); }
.welcome-suggest .icon { color: var(--oct-meta); flex: 0 0 auto; }

/* ─── compaction ──────────────────────────────────────────────────────
   The conversation was rewritten underneath the user. That is a fact
   about their data, so it is stated in the transcript at full width and
   never as a toast that disappears. */
.compaction { display: flex; align-items: center; gap: var(--oct-space-3); margin-block: var(--oct-space-5); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-meta); }
.compaction::before, .compaction::after { content: ''; flex: 1; height: 1px; background: var(--oct-border); }

/* ══════════════════════════════════════════════════════════════════════
   MOBILE

   The phone is a different product surface, not a narrow desktop. One
   thing at a time, one thumb, and a material — translucent glass over an
   atmospheric ground — that the desktop deliberately does not use.

   Everything here is scoped to .phone so the glass layer cannot leak
   into a desktop screen by accident. Drop a component out of .phone and
   it loses its material rather than quietly bringing a blur onto a
   surface that was designed flat.
   ══════════════════════════════════════════════════════════════════════ */

/* ─── device ──────────────────────────────────────────────────────────
   A viewport at real logical points, so a phone layout is judged at the
   size it ships at rather than at whatever width the page happens to
   give it. */
.phone { position: relative; width: var(--oct-phone-w); height: var(--oct-phone-h); border-radius: 44px; border: 1px solid var(--oct-border); background: var(--oct-bg); overflow: hidden; isolation: isolate;

  /* The text scale compresses on glass, and this is where it happens —
     once, as two token overrides, rather than as an exception in each of
     the eighteen mobile rules that reach for a softened role.

     On an opaque surface the system runs four weights: fg, fg-2, muted,
     meta. Over floored glass only three of them survive. Measured across
     every material, both themes and any ground, fg holds 5.2:1 and fg-2
     holds 4.6:1, but muted needs 89% ink to reach 4.5:1 — which is fg-2,
     so the third and fourth steps collapse into one. The quiet step below
     that is re-cut at 68% ink in both themes, where it measures 3.4:1:
     the same 3:1 exemption --oct-meta already carries on the desktop, and
     the same reason for it, that it labels rather than reads.

     Losing a step of hierarchy is the honest cost of putting text on a
     photograph. The alternative is a fourth weight that is legible in the
     styleguide's own atmosphere and not in the user's. */
  --oct-muted: light-dark(rgba(38, 37, 30, 0.90), rgba(242, 241, 237, 0.90));
  --oct-meta:  light-dark(rgba(38, 37, 30, 0.68), rgba(242, 241, 237, 0.68));
}

/* ─── atmosphere ──────────────────────────────────────────────────────
   The ground the glass sits on. The app ships photographs here; this
   stands in for one without pulling a binary into the design system,
   and it is deliberately built to be HOSTILE rather than pretty.

   The first version was three soft pastel washes, and glass measured
   clean over all of it — which proved nothing. A photograph is not a
   soft wash. It has a blown highlight and a black shadow within a few
   points of each other, and blurring it does not lift the shadow or
   pull the highlight down. So this ground carries a near-white specular
   blob, a near-black one, and fine grain: if a material survives here it
   survives a picture.

   The grain is an inline SVG turbulence, not an asset. It is destroyed
   by a 48px blur, which is the point — it is there so the ground looks
   photographic, not so it changes any measurement. */
.phone-atmos { position: absolute; inset: 0; z-index: 0; background:
    radial-gradient(34% 22% at 76% 11%, rgba(255, 255, 255, 0.95), rgba(255, 255, 255, 0) 70%),
    radial-gradient(46% 32% at 18% 74%, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0) 72%),
    radial-gradient(120% 80% at 12% 0%, color-mix(in oklab, var(--oct-accent) 42%, transparent), transparent 60%),
    radial-gradient(100% 70% at 92% 24%, color-mix(in oklab, var(--oct-series-4) 38%, transparent), transparent 62%),
    radial-gradient(120% 90% at 50% 100%, color-mix(in oklab, var(--oct-series-2) 34%, transparent), transparent 58%),
    var(--oct-bg); }
.phone-atmos::before {
  content: ''; position: absolute; inset: 0; opacity: 0.5; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* The floor. This is the layer that makes the material recipes true.

   Measured against an unbounded ground, thin glass carrying secondary
   text bottoms out at 1.32:1 — a photograph can put white under it, and
   blur averages the white rather than removing it. No adjustment to the
   fill or the blur fixes that, because the failure is in the picture,
   not the material.

   So the ground is floored: the theme's own background is laid over
   whatever image is behind it, pinning the backdrop into a band the
   glass can be specified against. 66% is the measured minimum that holds
   every ink-weight role over ANY image, black through white — including
   the roles that sit on no material at all, which is the case that sets
   the number. On glass alone 55% would do; a heading on the bare ground
   needs 66%, and a screen like .mcentre is deliberately bare.

   It costs two thirds of the photograph, and it buys a guarantee that
   does not depend on which photograph. That is the trade, stated
   plainly: this language is glass over an atmosphere, not glass over a
   picture you can still make out. */
.phone-atmos::after { content: ''; position: absolute; inset: 0; background: var(--oct-glass-floor); }
.phone-body { position: relative; z-index: 1; display: flex; flex-direction: column; height: 100%; min-height: 0; }
/* The status bar is the OS, not us. It is drawn only so a screen is
   judged with the space it actually gets. */
.phone-status { display: flex; align-items: center; justify-content: space-between; flex: 0 0 auto; padding: 14px var(--oct-space-6) 2px; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg); font-variant-numeric: tabular-nums; }
.phone-scroll { flex: 1; min-height: 0; overflow-y: auto; overscroll-behavior: contain; scrollbar-width: none; }
.phone-scroll::-webkit-scrollbar { display: none; }

/* ─── glass ───────────────────────────────────────────────────────────
   The material. Fill and blur move together; the stroke is a hairline of
   ink at low alpha, and the highlight is the inner top edge that makes a
   panel read as a physical pane catching light.

   backdrop-filter is the whole point, so where it is unsupported the
   fallback has to be a solid surface rather than a transparent one —
   otherwise the text lands on the atmosphere directly and becomes
   unreadable. @supports handles that, in that direction. */
/* The scope is written :where(.phone) rather than .phone so it contributes
   nothing to specificity. Scoping and weight are different jobs, and
   conflating them is what broke this: as `.phone .glass` the rule carried two
   classes and quietly outranked every one-class component that also wears
   the material, so .bsheet lost its position, its top-only corners and its
   open bottom edge, and .tabbar lost its pill. The sheet still looked right
   in the styleguide purely because the markup happened to nest it in a flex
   column — which is exactly the kind of accident that survives review.
   At zero specificity the material sets the defaults and any component
   defined below overrides the ones it cares about. */
:where(.phone) .glass { position: relative; border: 1px solid var(--oct-glass-stroke); border-radius: var(--oct-radius-card-m); background: var(--oct-surface); }
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
  :where(.phone) .glass { background: var(--oct-glass-regular); backdrop-filter: blur(var(--oct-glass-blur-regular)) saturate(150%); -webkit-backdrop-filter: blur(var(--oct-glass-blur-regular)) saturate(150%); }
  :where(.phone) .glass-thin   { background: var(--oct-glass-thin);   backdrop-filter: blur(var(--oct-glass-blur-thin))   saturate(150%); -webkit-backdrop-filter: blur(var(--oct-glass-blur-thin))   saturate(150%); }
  :where(.phone) .glass-thick  { background: var(--oct-glass-thick);  backdrop-filter: blur(var(--oct-glass-blur-thick))  saturate(150%); -webkit-backdrop-filter: blur(var(--oct-glass-blur-thick))  saturate(150%); }
  :where(.phone) .glass-chrome { background: var(--oct-glass-chrome); backdrop-filter: blur(var(--oct-glass-blur-chrome)) saturate(150%); -webkit-backdrop-filter: blur(var(--oct-glass-blur-chrome)) saturate(150%); }
}
:where(.phone) .glass-thick, :where(.phone) .glass-chrome { border-color: var(--oct-glass-stroke-strong); }
/* the caught-light edge */
:where(.phone) .glass::before { content: ''; position: absolute; inset: 0 0 auto; height: 1px; border-radius: inherit; background: var(--oct-glass-highlight); opacity: .6; pointer-events: none; }

/* The desktop icon button is 34px, which is a comfortable mouse target
   and an uncomfortable thumb one. Inside a phone it takes the touch
   floor instead — the same component, resized by its surface rather than
   forked into a second one. */
:where(.phone) .btn-icon { width: var(--oct-touch-min); height: var(--oct-touch-min); }

/* ─── tab bar ─────────────────────────────────────────────────────────
   Four destinations, floating on chrome glass rather than docked to a
   painted strip — the transcript scrolls under it, which is what makes
   the phone feel like one continuous surface. */
.tabbar { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 2px; flex: 0 0 auto; margin: 0 var(--oct-space-3) var(--oct-space-3); padding: 6px; border-radius: var(--oct-radius-pill); }
.tabitem { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; min-height: var(--oct-touch-min); padding: 4px; border: 0; border-radius: var(--oct-radius-pill); background: none; color: var(--oct-meta); font-family: var(--oct-font-display); font-size: 10px; letter-spacing: var(--oct-tracking-wide); }
.tabitem .icon { width: 20px; height: 20px; }
/* the tint is mixed into the surface, not into transparency, so the
   accent label has an opaque ground under it rather than the photograph */
.tabitem[aria-current="page"] { color: var(--oct-accent-fg); background: color-mix(in oklab, var(--oct-accent) 10%, var(--oct-bg)); }
/* an unread count on a tab, placed on the icon rather than the label so
   it survives the label being two words */
.tabitem .dot-badge { position: absolute; transform: translate(11px, -4px); min-width: 7px; height: 7px; border-radius: var(--oct-radius-pill); background: var(--oct-accent); }

/* ─── home ────────────────────────────────────────────────────────────
   Two status cards over the thread list. The cards are deliberately tall
   and half-width: they are the first thing a thumb reaches and the only
   thing on the screen that is a destination rather than a row. */
.statusgrid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--oct-space-3); }
.statuscard { display: flex; flex-direction: column; justify-content: space-between; min-height: 132px; padding: var(--oct-space-4); border-radius: var(--oct-radius-card-m); text-align: left; color: inherit; }
.statuscard > .icon { width: 22px; height: 22px; color: var(--oct-fg); }
.statuscard-label { display: flex; align-items: baseline; gap: 6px; }
.statuscard-title { font-family: var(--oct-font-display); font-size: var(--oct-text-base); color: var(--oct-fg); }
.statuscard-count { font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); font-variant-numeric: tabular-nums; color: var(--oct-muted); }

/* a thread in the inbox. Status is a dot, because at this width a word
   would take the room the title needs. */
.mrow { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; padding: var(--oct-space-4); border-radius: var(--oct-radius-card-m); text-align: left; color: inherit; }
.mrow-dot { width: 8px; height: 8px; flex: 0 0 auto; border-radius: var(--oct-radius-pill); background: var(--oct-meta); }
.mrow[data-status="waiting"]  .mrow-dot { background: var(--oct-warn-fg); }
.mrow[data-status="running"]  .mrow-dot { background: var(--oct-accent); }
.mrow-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.mrow-title { font-family: var(--oct-font-display); font-size: var(--oct-text-base); color: var(--oct-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mrow-meta { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.mrow > .icon { flex: 0 0 auto; color: var(--oct-meta); }

/* ─── transcript ──────────────────────────────────────────────────────
   Asymmetric on purpose. The user's own words are a solid ink bubble,
   capped at 88% and notched at the corner nearest the sender. The
   agent's are not a bubble at all — full width on thin glass, because
   agent output is long, structured, and often code, and a bubble around
   it would waste the only axis the phone has. */
.bubble-row { display: flex; flex-direction: column; margin-bottom: var(--oct-space-2); }
.bubble-row.is-user { align-items: flex-end; }
.bubble-user { max-width: 88%; padding: 12px var(--oct-space-4); border-radius: var(--oct-radius-bubble); border-bottom-right-radius: 8px; background: var(--oct-fg); color: var(--oct-bg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-base); line-height: var(--oct-leading-body); }
.bubble-agent { width: 100%; padding: 12px var(--oct-space-4); border-radius: var(--oct-radius-card-m); font-family: var(--oct-font-transcript); font-size: var(--oct-text-base); line-height: var(--oct-leading-body); color: var(--oct-fg-2); }
.bubble-agent p { margin: 0 0 var(--oct-space-3); }
.bubble-agent p:last-child { margin-bottom: 0; }
.bubble-stack { display: flex; flex-direction: column; gap: var(--oct-space-2); width: 100%; }

/* reasoning, collapsed — same rule as desktop: never required reading */
.mreason { border-radius: var(--oct-radius-md); }
.mreason > summary { display: flex; align-items: center; gap: var(--oct-space-2); min-height: var(--oct-touch-min); padding: 0 var(--oct-space-3); cursor: pointer; list-style: none; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); }
.mreason > summary::-webkit-details-marker { display: none; }
.mreason .chev { transition: transform var(--oct-motion-fast) ease; }
.mreason[open] .chev { transform: rotate(90deg); }
.mreason-body { padding: 0 var(--oct-space-3) var(--oct-space-3); font-family: var(--oct-font-transcript); font-size: var(--oct-text-sm); font-style: italic; line-height: var(--oct-leading-body); color: var(--oct-muted); }

/* a tool call, summarised. On a phone the arguments are not shown at all
   — only what ran and how it went. */
.toolpart { display: flex; align-items: center; gap: var(--oct-space-3); min-height: var(--oct-touch-min); padding: var(--oct-space-2) var(--oct-space-3); border-radius: var(--oct-radius-md); }
.toolpart .icon { flex: 0 0 auto; color: var(--oct-meta); }
.toolpart-main { flex: 1; min-width: 0; }
.toolpart-name { display: block; font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); color: var(--oct-fg-2); }
.toolpart-sum { display: block; font-size: var(--oct-text-xs); color: var(--oct-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.toolpart[data-status="failed"] .toolpart-name { color: var(--oct-danger-fg); }

/* Soft idle: the attempt chip carries accent only while something is
   actually in flight, and danger only after a real failure. An idle
   thread should not glow. */
/* Opaque, and that is the whole point. A coloured label is the one thing
   that cannot ride on glass: to survive an arbitrary photograph, accent
   and danger would have to be mixed down to 20-35% of themselves, which
   is the hue gone. So the chip brings its own ground and keeps its
   colour, rather than keeping the material and losing the meaning.

   The ground is --oct-bg rather than --oct-surface: accent text measures
   4.33:1 on surface and 4.84:1 on the page ground, and the chip has no
   reason to prefer the lighter of the two. */
.attempt { display: inline-flex; align-items: center; gap: 6px; align-self: flex-start; padding: 4px 10px; margin-bottom: var(--oct-space-2); border: 1px solid var(--oct-border-soft); border-radius: var(--oct-radius-md); background: var(--oct-bg); font-size: var(--oct-text-xs); font-weight: var(--oct-weight-strong); color: var(--oct-muted); }
.attempt[data-state="queued"], .attempt[data-state="streaming"], .attempt[data-state="waiting"] { color: var(--oct-accent-fg); }
.attempt[data-state="failed"], .attempt[data-state="interrupted"] { color: var(--oct-danger-fg); }

/* what the agent is doing, as colour on a surface too small for a word */
.stage { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: var(--oct-radius-pill); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-fg); background: var(--oct-stage-thinking); }
.stage[data-stage="reading"]  { background: var(--oct-stage-reading); }
.stage[data-stage="editing"]  { background: var(--oct-stage-editing); }
.stage[data-stage="grepping"] { background: var(--oct-stage-grepping); }
.stage[data-stage="done"]     { background: var(--oct-stage-done); }

/* ─── composer ────────────────────────────────────────────────────────
   Floats over the transcript on the thickest glass in the system, which
   is what lets text scroll under it and stay legible. Send is a circle,
   thumb-sized, and becomes Stop while a turn is in flight — the same
   control, because the thumb is already there. */
.mcomposer { flex: 0 0 auto; margin: 0 var(--oct-space-3) var(--oct-space-3); padding: var(--oct-space-4) var(--oct-space-4) var(--oct-space-3); border-radius: var(--oct-radius-sheet); }
.mcomposer-input { width: 100%; min-height: 44px; border: 0; padding: 0; background: none; color: var(--oct-fg); font-family: var(--oct-font-transcript); font-size: var(--oct-text-base); line-height: var(--oct-leading-body); resize: none; }
.mcomposer-input::placeholder { color: var(--oct-muted); }
.mcomposer-input:focus { outline: none; }
.mcomposer-bar { display: flex; align-items: center; gap: var(--oct-space-2); margin-top: var(--oct-space-2); }
.mcomposer-bar .spacer { flex: 1; }
.mchip { display: inline-flex; align-items: center; gap: 4px; min-height: 32px; padding: 0 var(--oct-space-3); border: 1px solid var(--oct-glass-stroke); border-radius: var(--oct-radius-pill); background: var(--oct-glass-thin); color: var(--oct-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); white-space: nowrap; }
.msend { display: grid; place-items: center; width: var(--oct-touch-min); height: var(--oct-touch-min); flex: 0 0 auto; border: 0; border-radius: var(--oct-radius-pill); background: var(--oct-accent); color: var(--oct-accent-on); }
.msend[disabled] { opacity: var(--oct-disabled-opacity); }
.msend.is-stop { background: var(--oct-fg); color: var(--oct-bg); }
.mcomposer-hint { margin: var(--oct-space-2) 0 0; text-align: center; font-size: var(--oct-text-xs); color: var(--oct-meta); }

/* ─── bottom sheet ────────────────────────────────────────────────────
   The phone's modal. Grab handle, centred title, close on the left, and
   a ceiling well short of the full screen so the backdrop stays visible
   — on a phone the visible strip of what is behind is the only thing
   telling you this is a layer and not a new page. */
.bsheet-scrim { position: absolute; inset: 0; z-index: 2; background: var(--oct-scrim); }
.bsheet { position: absolute; z-index: 3; inset: auto 0 0; max-height: 78%; display: flex; flex-direction: column; padding-bottom: var(--oct-space-6); border-radius: var(--oct-radius-sheet) var(--oct-radius-sheet) 0 0; border-bottom: 0; }
.bsheet-grip { width: 40px; height: 5px; margin: var(--oct-space-2) auto; border-radius: var(--oct-radius-pill); background: var(--oct-glass-stroke-strong); }
.bsheet-head { display: grid; grid-template-columns: 34px minmax(0, 1fr) 34px; align-items: center; padding: 0 var(--oct-space-3) var(--oct-space-3); }
.bsheet-title { text-align: center; font-family: var(--oct-font-display); font-size: var(--oct-text-base); color: var(--oct-fg); }
.bsheet-body { min-height: 0; overflow-y: auto; padding: 0 var(--oct-space-4); }

/* Open and closed.

   Progressive enhancement, and the direction matters: a sheet with no
   script is OPEN, because that is what the markup says, and a specimen
   should render what it declares. The closed attribute is written by
   octant.js and by nothing else, so a page without the script never
   reaches a state it cannot leave. Reduced motion is handled by the
   global block at the foot of this file. */
/* visibility is toggled, never interpolated, and the delay is asymmetric
   on purpose. Opening applies it immediately, because the sheet has to be
   focusable in the same frame the state changes — an element that is still
   visibility:hidden cannot take focus, and the open handler focuses it.
   Closing holds it until the slide has finished, so the sheet is not
   snatched away mid-animation. */
.bsheet { transition: transform var(--oct-motion-base) var(--oct-ease-standard), visibility 0s; }
.bsheet-scrim { transition: opacity var(--oct-motion-base) var(--oct-ease-standard), visibility 0s; }
.bsheet[data-oct-sheet-state="closed"] { transform: translateY(100%); visibility: hidden; transition: transform var(--oct-motion-base) var(--oct-ease-standard), visibility 0s var(--oct-motion-base); }
.bsheet-scrim[data-oct-sheet-state="closed"] { opacity: 0; visibility: hidden; transition: opacity var(--oct-motion-base) var(--oct-ease-standard), visibility 0s var(--oct-motion-base); }
/* while a drag is in flight the sheet follows the thumb, not the clock */
.bsheet[data-oct-sheet-dragging] { transition: none; }
/* and the closed state octant.js writes at startup is not a dismissal, so
   it must not animate: the sheet was never open to the reader, and a slide
   on load would claim it was. Set for one frame, then removed. */
.bsheet[data-oct-sheet-init], .bsheet-scrim[data-oct-sheet-init] { transition: none; }

/* a tappable option inside a sheet */
.mopt { display: flex; align-items: center; gap: var(--oct-space-3); width: 100%; min-height: var(--oct-touch-min); padding: var(--oct-space-3) 0; border: 0; border-top: 1px solid var(--oct-glass-stroke); background: none; color: inherit; text-align: left; }
.mopt:first-child { border-top: 0; }
.mopt-main { flex: 1; min-width: 0; }
.mopt-name { display: block; font-size: var(--oct-text-base); color: var(--oct-fg); }
.mopt-sub { display: block; font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); color: var(--oct-muted); }
/* Every option carries the check; only the selected one shows it. Drawing
   it on all of them keeps the rows the same width, so selection does not
   reflow the list, and lets the state move without touching the DOM. */
.mopt > .icon { flex: 0 0 auto; visibility: hidden; color: var(--oct-accent-fg); }
.mopt[aria-selected="true"] > .icon { visibility: visible; }
/* selection is the check mark and the weight; it was accent-coloured,
   which measured 3.79:1 on glass over a light ground */
.mopt[aria-selected="true"] .mopt-name { font-weight: var(--oct-weight-strong); }

/* ─── deferral ────────────────────────────────────────────────────────
   The phone cannot mint approval for a high-risk action; that authority
   stays on the paired host. This surface exists to say so honestly,
   which means it must NOT offer an approve control — a disabled button
   would read as "not yet", when the truth is "not here". It states what
   is waiting, where, and why, and offers only the way forward.

   The rule it encodes: an agent client that cannot do a thing should
   remove the control, not grey it out. */
.deferral { padding: var(--oct-space-4); border-radius: var(--oct-radius-card-m); }
/* The heading is ink, not warn. Colour does not survive glass, and a
   heading is the wrong place to spend the one signal that does — the
   icon beside it carries the warning, and it is a shape, not a hue. */
.deferral-head { display: flex; align-items: center; gap: var(--oct-space-2); margin-bottom: var(--oct-space-2); color: var(--oct-fg); font-family: var(--oct-font-display); font-size: var(--oct-text-base); }
.deferral-head > .icon { color: var(--oct-warn-fg); }
.deferral-body { margin: 0 0 var(--oct-space-3); font-size: var(--oct-text-sm); line-height: var(--oct-leading-body); color: var(--oct-fg-2); }
/* the facts, from the host, never invented on the phone */
.deferral-facts { margin: 0; padding: var(--oct-space-3); border-radius: var(--oct-radius-md); background: color-mix(in oklab, var(--oct-fg) 6%, transparent); font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); line-height: var(--oct-leading-code); color: var(--oct-fg-2); }
.deferral-facts dt { color: var(--oct-meta); }
.deferral-facts dd { margin: 0 0 var(--oct-space-2); overflow-wrap: anywhere; }
.deferral-facts dd:last-child { margin-bottom: 0; }

/* Biometric-gated actions the phone CAN take. The distinction is the
   whole point: this button exists because the authority is the device's,
   not the host's. */
.biobtn { display: flex; align-items: center; justify-content: center; gap: var(--oct-space-2); width: 100%; min-height: var(--oct-touch-min); border: 1px solid var(--oct-glass-stroke-strong); border-radius: var(--oct-radius-pill); background: var(--oct-fg); color: var(--oct-bg); font-family: var(--oct-font-display); font-size: var(--oct-text-sm); }
.biobtn[disabled] { background: none; color: var(--oct-meta); }

/* ─── pairing and lock ────────────────────────────────────────────────
   The two screens shown before there is any content: connect this phone
   to a host, and prove you are you. Both are centred and single-purpose,
   which is the one time a phone screen should be. */
.mcentre { display: flex; flex-direction: column; justify-content: center; gap: var(--oct-space-5); min-height: 100%; padding: var(--oct-space-6); text-align: center; }
.mcentre .icon-mark { margin-inline: auto; }
.mcentre h3 { margin: 0; font-size: var(--oct-text-xl); }
.mcentre p { margin: 0; font-size: var(--oct-text-sm); color: var(--oct-muted); }
.mfield { display: flex; flex-direction: column; gap: 6px; text-align: left; }
.mfield label { font-family: var(--oct-font-mono); font-size: var(--oct-text-xs); letter-spacing: var(--oct-tracking-wide); text-transform: uppercase; color: var(--oct-meta); }
.mfield input { min-height: var(--oct-touch-min); padding: 0 var(--oct-space-3); border: 1px solid var(--oct-glass-stroke); border-radius: var(--oct-radius-md); background: var(--oct-glass-thin); color: var(--oct-fg); font-family: var(--oct-font-mono); font-size: var(--oct-text-sm); }
.mfield input::placeholder { color: var(--oct-meta); }

/* Present to a screen reader, absent to the eye. Use it for a label the
   layout makes obvious but the accessibility tree still needs — never to
   hide something a sighted user also should not reach, since focus and
   the reading order both still go through it. */
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

/* The row a context menu was opened on. It reads as held rather than
   selected — an outline, not a fill — because the row's real selected state
   may well be somewhere else, and two fills would compete. */
.is-context-target { outline: 1px solid var(--oct-accent-fg); outline-offset: -1px; border-radius: 5px; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
  /* the spinner keeps turning — a frozen spinner reads as a hung app —
     but slowly enough not to trigger anyone */
  .spinner { animation-duration: 2s !important; }
  .skeleton::after { display: none; }
}
