assets/css/openagents.css

main at 58e6347eeb72 · 191 KB

/*
 * OpenAgents style pack
 *
 * Basecoat ships structure (assets/vendor/basecoat/components/*.css: layout,
 * accessibility selectors, behavior hooks - no color, no radius) separately
 * from visual style packs. We import the structure and write this pack instead
 * of adopting Vega or any other shadcn pack, so OpenAgents identity is a
 * first-class artifact rather than a diff against another product's defaults.
 *
 * This file is imported last. Every rule here must be traceable to DESIGN.md.
 * See docs/decisions/0003-basecoat-component-library.md.
 */

/* ── Typeface ──────────────────────────────────────────────────────────────
 * Geist Sans and Geist Mono, self-hosted from priv/static/fonts. The browser
 * image and font policy is same-origin, so a CDN or Google Fonts link would be
 * blocked; these are the only way the product actually renders in Geist rather
 * than silently falling back to a system face. SIL OFL 1.1, (C) 2023 Vercel -
 * see priv/static/fonts/LICENSE.
 */

@font-face {
  font-family: "Geist Sans";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/geist-sans-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Sans";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/geist-sans-latin-500-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Sans";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/geist-sans-latin-600-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Sans";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/geist-sans-latin-700-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/geist-mono-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("/fonts/geist-mono-latin-500-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/geist-mono-latin-600-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Geist Mono";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/geist-mono-latin-700-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Titillium Web";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/titillium-web-latin-400-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Titillium Web";
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url("/fonts/titillium-web-latin-600-normal.woff2") format("woff2");
}

@font-face {
  font-family: "Titillium Web";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/titillium-web-latin-700-normal.woff2") format("woff2");
}

/* ── Token layer ───────────────────────────────────────────────────────────
 * OpenAgents Aiur tokens (declared in app.css) are the authored source of truth.
 * Basecoat's shadcn-compatible names are derived from them so both vocabularies
 * resolve to one palette. DESIGN.md's token table stays the readable contract.
 */

:root {
  --background: var(--ink-void);
  --foreground: var(--text-primary);

  --card: var(--ink-surface);
  --card-foreground: var(--text-primary);
  --popover: var(--ink-surface);
  --popover-foreground: var(--text-body);

  --primary: var(--accent);
  --primary-foreground: var(--text-primary);
  --secondary: var(--ink-surface);
  --secondary-foreground: var(--text-primary);

  --muted: var(--ink-raised);
  --muted-foreground: var(--text-muted);
  --accent-foreground: var(--text-primary);

  --destructive: var(--danger);
  --border: var(--line);
  --input: var(--line-strong);
  --ring: var(--accent-bright);

  /* Basecoat's default radius. Surfaces are softened rather than square. */
  --radius: 0.625rem;

  --font-sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono: "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* The face Arwes uses. One statement carries it; nothing else does. */
  --font-display: "Titillium Web", var(--font-sans);

  /* Restrained elevation for surfaces that float above the shell, and a soft
     accent halo for the primary action. Both are depth cues, not decoration. */
  --lift: 0 8px 28px -12px oklch(0% 0 0 / 0.72);
  --halo: 0 0 0 1px color-mix(in oklch, var(--accent-bright) 42%, transparent),
    0 6px 22px -10px color-mix(in oklch, var(--accent-bright) 58%, transparent);
  --ease: cubic-bezier(0.22, 1, 0.36, 1);

  /* Motion tokens. One shared easing, two durations: --motion-fast (150ms) is
     the default for every interactive color/background/border/opacity/shadow
     transition and for the sanctioned sidebar layout step; --motion-quick
     (100ms) is reserved for menus and popovers, which should feel closer to
     instant. Reduced motion is handled once, by the global kill switch at the
     end of app.css, so individual rules do not carry their own guards. */
  --motion-fast: 150ms;
  --motion-quick: 100ms;

  /* Composed shadow scale, L1 lowest. Four rungs of the same black, tuned for
     a dark canvas: L1 for cards and hover toolbars, L2 for inner media wells,
     L3 for menus and floating panels, L4 for the rare full lift. Nothing
     consumes them yet; they are the vocabulary later phases reference by
     name, alongside --lift, which stays for today's floating surfaces. */
  --shadow-l1: 0 1px 2px 0 oklch(0% 0 0 / 0.28);
  --shadow-l2: 0 1px 3px 0 oklch(0% 0 0 / 0.32), 0 1px 2px -1px oklch(0% 0 0 / 0.32);
  --shadow-l3: 0 10px 15px -3px oklch(0% 0 0 / 0.38), 0 4px 6px -4px oklch(0% 0 0 / 0.38);
  --shadow-l4: 0 25px 50px -12px oklch(0% 0 0 / 0.55);
}

@theme inline {
  --font-sans: var(--font-sans);
  --font-mono: var(--font-mono);
  --radius-sm: calc(var(--radius) - 4px);
  --radius-md: calc(var(--radius) - 2px);
  --radius-lg: var(--radius);
  --radius-xl: calc(var(--radius) + 4px);
}

/* ── Surface recipes ───────────────────────────────────────────────────────
 * Shared mechanics that later phases compose by name. Each is a pattern with
 * one utility class, not a component: components stay in OpenAgentsWeb.UI.
 */

@layer components {
  /* Inset hairline ring. A 1px edge drawn as an inset box-shadow instead of a
     border: it costs no layout, sits over the element's own background, and —
     because transitions are already sanctioned on box-shadow — focus can
     animate it where a border could not. Compose it with an elevation rung in
     one declaration, e.g.
       box-shadow: inset 0 0 0 1px var(--line-soft), var(--shadow-l1);
     A stronger edge swaps --line-soft for --line; nothing here changes the
     radius, which stays the consumer's decision. */
  .ring-hairline {
    box-shadow: inset 0 0 0 1px var(--line-soft);
  }

  /* Scroll-edge hairline. A sticky header earns its bottom rule only once
     content has actually scrolled beneath it: the rule is a box-shadow on a
     zero-height placeholder, toggled by opacity, so it occupies no layout and
     shifts nothing when it appears. The host needs position: relative (or any
     positioned box). Consumers toggle it with a data attribute —
       <header class="scroll-edge-hairline" data-scroll-edge="stuck">
     — set from whatever already knows the scroll state (a LiveView hook or an
     IntersectionObserver sentinel); the stylesheet itself needs no script. */
  .scroll-edge-hairline::after {
    position: absolute;
    inset-inline: 0;
    bottom: 0;
    height: 0;
    box-shadow: 0 1px 0 var(--line-faint);
    content: "";
    opacity: 0;
    pointer-events: none;
    transition: opacity var(--motion-fast) var(--ease);
  }

  .scroll-edge-hairline[data-scroll-edge="stuck"]::after {
    opacity: 1;
  }

  /* Keyboard-mode focus outline. The same recipe the app shell applies to
     bare controls (app.css), as a class for composed elements that are not
     matched by that selector list. :focus-visible keeps it to keyboard and
     assistive navigation; pointer clicks stay quiet. The outline follows the
     element's own border-radius — the recipe never reshapes the element, so a
     circular control keeps a circular ring, offset by the 2px it already has. */
  .focus-ring:focus-visible {
    outline: 2px solid var(--accent-bright);
    outline-offset: 2px;
  }
}

/* ── Computers surface ─────────────────────────────────────────────────────
 * The device manager is a command-bar shell with one bounded scrolling
 * column. Cards separate durable credential state from ephemeral presence;
 * neither status is communicated by color alone.
 */

@layer components {
  .computers-shell {
    grid-template-rows: auto minmax(0, 1fr);
  }

  .computers {
    min-height: 0;
    padding: clamp(24px, 4vw, 44px) clamp(16px, 4vw, 36px) 56px;
    overflow-x: hidden;
    overflow-y: auto;
    /* No fill. `--ink-surface` is the panel rung, and painting it on a
       page-level element drew a lighter slab that stopped wherever the content
       stopped. The page takes the main area's background like every other
       surface; the indigo radial wash that used to sit in this corner is gone
       for the same reason -- it was the only place a page tinted itself. */
    scrollbar-color: var(--icon-faint) transparent;
    scrollbar-width: thin;
  }

  .computers__inner {
    display: grid;
    width: min(100%, 880px);
    margin: 0 auto;
    gap: 22px;
  }

  .computers-heading {
    display: grid;
    max-width: 680px;
    gap: 7px;
  }

  .computers-heading__kicker {
    color: var(--accent-bright);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 700;
    letter-spacing: 0.14em;
  }

  .computers-heading h1,
  .computers-heading p,
  .computers-pairing h2,
  .computers-pairing p,
  .computers-list-heading h2,
  .computers-list-heading p,
  .computer-card h3,
  .computer-card__actions p {
    margin: 0;
  }

  .computers-heading h1 {
    color: var(--text-primary);
    font-size: clamp(1.75rem, 4vw, 2.35rem);
    font-weight: 650;
    letter-spacing: -0.035em;
    line-height: 1.08;
  }

  .computers-heading p,
  .computers-pairing p,
  .computers-list-heading p,
  .computer-card__actions p {
    color: var(--text-muted);
  }

  .computers-pairing {
    display: grid;
    grid-template-columns: minmax(0, 1.2fr) minmax(280px, 0.8fr);
    align-items: end;
    gap: clamp(22px, 4vw, 38px);
    padding: clamp(20px, 3vw, 28px);
    border-color: var(--line-strong);
    background: color-mix(in oklab, var(--ink-raised) 56%, var(--ink-surface));
  }

  .computers-pairing__intro {
    display: flex;
    align-items: flex-start;
    gap: 14px;
  }

  .computers-pairing__intro > div {
    display: grid;
    gap: 6px;
  }

  .computers-pairing__intro h2,
  .computers-list-heading h2 {
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 650;
    letter-spacing: -0.015em;
  }

  .computers-pairing__icon,
  .computer-card__glyph {
    display: grid;
    flex: 0 0 auto;
    place-items: center;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    background: var(--wash-hover);
    color: var(--accent-bright);
  }

  .computers-pairing__icon {
    width: 42px;
    height: 42px;
    font-size: 1.25rem;
  }

  /* One row per control.
   *
   * This was `minmax(0, 1fr) auto` — two columns for three children, so the
   * checkbox landed beside the code label and the button wrapped underneath
   * into the same cell, rendering "Approve pairing" through the checkbox text.
   * The form was hard to fill and the button hard to hit, on the one page
   * where a person approves a computer. */
  .computers-pairing__form {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    align-items: start;
    gap: 14px;
  }

  .computers-pairing__form .btn {
    justify-self: start;
  }

  /* The checkbox and its words on one line, aligned to each other rather than
     stretched across the row. */
  .computers-pairing__field label:has(input[type="checkbox"]) {
    display: flex;
    align-items: center;
    gap: 8px;
    line-height: 1.35;
  }

  .computers-pairing__field input[type="checkbox"] {
    flex: 0 0 auto;
    margin: 0;
  }

  .computers-pairing__field {
    min-width: 0;
  }

  .computers-pairing__field .input {
    width: 100%;
    font-family: var(--font-mono);
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .computers-notices:empty {
    display: none;
  }

  .computers-notices {
    display: grid;
    gap: 8px;
  }

  .computers-list-section {
    display: grid;
    gap: 12px;
  }

  .computers-list-heading {
    display: flex;
    align-items: end;
    justify-content: space-between;
    gap: 18px;
    padding-top: 6px;
  }

  .computers-list-heading > div {
    display: grid;
    gap: 3px;
  }

  .computers-list {
    display: grid;
    gap: 10px;
  }

  .computer-card {
    display: grid;
    gap: 18px;
    padding: clamp(18px, 3vw, 24px);
    transition:
      border-color var(--motion-fast) var(--ease),
      background-color var(--motion-fast) var(--ease),
      box-shadow var(--motion-fast) var(--ease);
  }

  .computer-card:hover,
  .computer-card:focus-within {
    border-color: var(--line-strong);
    background: color-mix(in oklab, var(--ink-raised) 34%, var(--ink-surface));
    box-shadow: var(--shadow-l1);
  }

  .computer-card[data-state="revoked"] {
    opacity: 0.74;
  }

  .computer-card__top,
  .computer-card__identity,
  .computer-card__badges,
  .computer-card__presence,
  .computer-card__actions {
    display: flex;
    align-items: center;
  }

  .computer-card__top {
    justify-content: space-between;
    gap: 18px;
  }

  .computer-card__identity {
    min-width: 0;
    gap: 12px;
  }

  .computer-card__identity > div {
    display: grid;
    min-width: 0;
    gap: 4px;
  }

  .computer-card__glyph {
    width: 38px;
    height: 38px;
    color: var(--icon-secondary);
    font-size: 1.125rem;
  }

  .computer-card h3 {
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 650;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .computer-card__presence {
    gap: 8px;
    color: var(--text-muted);
    font-size: 0.75rem;
  }

  .computer-card__badges {
    flex: 0 0 auto;
    flex-wrap: wrap;
    justify-content: flex-end;
    gap: 6px;
  }

  .computer-card__facts {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 10px;
    margin: 0;
  }

  .computer-card__repositories {
    display: grid;
    gap: 10px;
    padding: 12px;
    border: 1px solid var(--line-faint);
    border-radius: var(--radius-sm);
    background: var(--wash-hover);
  }

  .computer-card__repositories h4 {
    margin: 0;
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
  }

  .computer-card__repositories p {
    margin: 0;
    color: var(--text-muted);
    font-size: 0.75rem;
  }

  .computer-card__grants {
    display: grid;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .computer-card__grants li {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 8px;
  }

  .computer-card__grant-name {
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .computer-card__grant-form {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) auto;
    align-items: end;
    gap: 10px;
  }

  @media (max-width: 720px) {
    .computer-card__grant-form {
      grid-template-columns: minmax(0, 1fr);
    }
  }

  .computer-card__facts > div {
    display: grid;
    min-width: 0;
    gap: 3px;
    padding: 10px 12px;
    border: 1px solid var(--line-faint);
    border-radius: var(--radius-sm);
    background: var(--wash-hover);
  }

  .computer-card__facts dt {
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.08em;
    text-transform: uppercase;
  }

  .computer-card__facts dd {
    min-width: 0;
    margin: 0;
    overflow: hidden;
    color: var(--text-body);
    font-size: 0.75rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .computer-card__actions {
    justify-content: space-between;
    gap: 18px;
    padding-top: 14px;
    border-top: 1px solid var(--line-faint);
  }

  .computer-card__actions p {
    font-size: 0.75rem;
  }

  @media (max-width: 760px) {
    .computers-pairing {
      grid-template-columns: minmax(0, 1fr);
      align-items: stretch;
    }

    .computer-card__facts {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  @media (max-width: 520px) {
    .computers {
      padding-inline: 13px;
    }

    .computers-pairing__form,
    .computer-card__top,
    .computer-card__actions {
      display: grid;
      grid-template-columns: minmax(0, 1fr);
      align-items: stretch;
    }

    .computers-pairing__form .btn,
    .computer-card__actions .btn {
      width: 100%;
    }

    .computer-card__badges {
      justify-content: flex-start;
    }

    .computer-card__facts {
      grid-template-columns: minmax(0, 1fr);
    }
  }
}

/* ── Button ───────────────────────────────────────────────────────────────── */

@layer components {
  .btn {
    /* Basecoat's structure layer sets no gap, so multi-child controls such as
       the account trigger would otherwise run together. */
    gap: 8px;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    color: var(--text-body);
    cursor: pointer;
    font-family: var(--font-sans);
    font-weight: 600;
    /* Short, eased, and confined to properties that cannot move layout. A
       state change still reads as immediate; it just does not snap. */
    transition-property: background-color, border-color, color, box-shadow, opacity;
    transition-duration: var(--motion-fast);
    transition-timing-function: var(--ease);
  }

  .btn:disabled {
    cursor: not-allowed;
  }

  /* Sizes */
  .btn[data-size="xs"] {
    min-height: 32px;
    padding: 5px 11px;
    font-size: 0.75rem;
    letter-spacing: 0.02em;
  }

  .btn[data-size="sm"] {
    min-height: 38px;
    padding: 8px 15px;
    font-size: 0.8125rem;
    letter-spacing: 0.01em;
  }

  .btn:not([data-size]),
  .btn[data-size="default"] {
    min-height: 44px;
    padding: 11px 20px;
    font-size: 0.875rem;
    letter-spacing: 0.01em;
  }

  .btn[data-size="lg"] {
    min-height: 60px;
    padding: 18px 22px;
    border-radius: var(--radius-lg);
    font-size: 1rem;
    letter-spacing: 0;
  }

  /* Variants */
  .btn:not([data-variant]),
  /* Inverted, not accented. Measured from the reference: its primary control
     is `#e5e5e6` with `#08090a` text -- the page's own background colour used
     as ink -- and no hue at all. `--control-inverse-bg` and
     `--control-inverse-fg` already resolve to that pair, so this is the token
     ladder finally being used where it was always meant to apply. The indigo
     fill was the last saturated element on a monochrome surface. */
  /* A press the finger can feel. Taken from the reference, which scales its
     buttons to .97 on :active. */
  .btn:active:not(:disabled) {
    transform: scale(0.97);
  }

  @media (prefers-reduced-motion: reduce) {
    .btn:active:not(:disabled) {
      transform: none;
    }
  }

  .btn[data-variant="primary"] {
    border-color: var(--control-inverse-bg);
    background: var(--control-inverse-bg);
    color: var(--control-inverse-fg);
  }

  /* Hover states are gated behind (hover: hover) throughout this pack: on
     touch, a tap would otherwise leave the hover treatment stuck on the last
     control touched. Nothing is revealed by hover — every affordance below is
     visible at rest — so the fallback is simply the resting state, and
     :focus-visible and :active stay ungated. */
  @media (hover: hover) {
    /* Hover brightens toward white rather than shifting hue, which is what the
       reference does (`#e5e5e6` to `#fff`). No halo: a glow around a already
       high-contrast control reads as a focus ring that is not one. */
    .btn:is(:not([data-variant]), [data-variant="primary"]):hover:not(:disabled) {
      border-color: var(--text-ink);
      background: var(--text-ink);
      color: var(--control-inverse-fg);
    }
  }

  .btn:is(:not([data-variant]), [data-variant="primary"]):active:not(:disabled) {
    box-shadow: none;
  }

  .btn[data-variant="secondary"] {
    border-color: var(--line-strong);
    background: var(--ink-surface);
    color: var(--text-primary);
  }

  @media (hover: hover) {
    .btn[data-variant="secondary"]:hover:not(:disabled) {
      border-color: var(--accent-bright);
      background: var(--ink-raised);
    }
  }

  .btn[data-variant="outline"] {
    border-color: var(--line-strong);
    background: transparent;
    color: var(--text-body);
  }

  @media (hover: hover) {
    .btn[data-variant="outline"]:hover:not(:disabled) {
      border-color: var(--accent-bright);
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .btn[data-variant="ghost"] {
    justify-content: flex-start;
    border-color: transparent;
    background: transparent;
    color: var(--text-muted);
    text-align: left;
  }

  /* Ghost hover is the canonical wash-ladder gesture: one text promotion plus
     the 6% wash; pressed steps one rung darker to the 8% wash. The wash reads
     the same on every surface rung, unlike the former opaque --ink-raised
     fill, which vanished on raised surfaces. */
  @media (hover: hover) {
    .btn[data-variant="ghost"]:hover:not(:disabled) {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .btn[data-variant="ghost"]:active:not(:disabled) {
    background: var(--wash-active);
    color: var(--text-primary);
  }

  /* The keycap treatment as a control: a quiet raised object in the chrome,
     rather than a link pretending to be one. Shares its look with `.kbd`. */
  .btn[data-variant="chip"] {
    border-color: var(--line-strong);
    border-radius: calc(var(--radius) - 5px);
    background: var(--ink-raised);
    color: var(--text-muted);
    font-size: 0.75rem;
    letter-spacing: 0.02em;
  }

  @media (hover: hover) {
    .btn[data-variant="chip"]:hover:not(:disabled) {
      border-color: var(--accent-bright);
      color: var(--text-primary);
    }
  }

  .btn[data-variant="chip"] .icon {
    font-size: 1.15em;
  }

  .btn[data-variant="destructive"] {
    border-color: color-mix(in oklch, var(--danger) 58%, transparent);
    background: transparent;
    color: var(--danger);
  }

  @media (hover: hover) {
    .btn[data-variant="destructive"]:hover:not(:disabled) {
      border-color: var(--danger);
      background: color-mix(in oklch, var(--danger) 14%, transparent);
    }
  }

  /* Arwes' octagon button. Its clip-path generator is a pure function there
     too, so the shape ports exactly; what does not port is the SVG outline
     behind it and the pointer-following illuminator. The edge is instead the
     element's own background showing through a 1px-inset pseudo-element that
     carries the fill and repeats the clip.

     `clip-path` also clips box-shadow and outline, so the hover glow is a
     `drop-shadow` filter (which follows the cut shape) and focus thickens the
     edge rather than drawing a ring that would be cut off. */
  .btn[data-variant="notched"] {
    --notch: 13px;
    --notch-clip: polygon(
      0 var(--notch),
      var(--notch) 0,
      100% 0,
      100% calc(100% - var(--notch)),
      calc(100% - var(--notch)) 100%,
      0 100%
    );

    position: relative;
    isolation: isolate;
    border: 0;
    border-radius: 0;
    background: var(--accent-bright);
    clip-path: var(--notch-clip);
    color: var(--text-primary);
    font-weight: 650;
    letter-spacing: 0.06em;
    transition-property: background-color, color, filter;
  }

  .btn[data-variant="notched"]::before {
    position: absolute;
    z-index: -1;
    inset: 1px;
    /* Pulled toward the canvas so the element's own background reads as a
       distinct edge rather than blending into the fill. */
    background: color-mix(in oklch, var(--accent) 86%, var(--ink-void));
    clip-path: var(--notch-clip);
    content: "";
  }

  @media (hover: hover) {
    .btn[data-variant="notched"]:hover:not(:disabled) {
      filter: drop-shadow(0 0 12px color-mix(in oklch, var(--accent-bright) 45%, transparent));
    }

    .btn[data-variant="notched"]:hover:not(:disabled)::before {
      background: var(--accent-bright);
    }
  }

  .btn[data-variant="notched"]:active:not(:disabled)::before {
    background: var(--text-primary);
  }

  .btn[data-variant="notched"]:focus-visible {
    outline: 0;
  }

  .btn[data-variant="notched"]:focus-visible::before {
    inset: 3px;
  }

  /* Link buttons are OpenAgents inline action: underlined text, not a boxed
     control. Size tokens must not apply. */
  .btn[data-variant="link"] {
    min-height: 0;
    padding: 0;
    border: 0;
    border-radius: 0;
    background: transparent;
    color: var(--accent-bright);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
    text-decoration: underline;
    text-decoration-color: color-mix(in oklch, currentColor 45%, transparent);
    text-underline-offset: 5px;
  }

  @media (hover: hover) {
    .btn[data-variant="link"]:hover:not(:disabled) {
      color: var(--text-primary);
      text-decoration-color: currentColor;
    }
  }

  .btn[data-variant="link"][data-tone="danger"] {
    color: var(--danger);
  }

  @media (hover: hover) {
    .btn[data-variant="link"][data-tone="danger"]:hover:not(:disabled) {
      color: var(--text-primary);
    }
  }

  /* Touch sizing: on a coarse pointer every control reaches a 40px effective
     target. Boxed controls grow their real box — their layouts absorb it —
     while the link variant keeps its visual size and extends an invisible tap
     pad instead, so inline actions stay typographic. */
  @media (pointer: coarse) {
    .btn[data-size="xs"],
    .btn[data-size="sm"] {
      min-height: 40px;
    }

    .btn[data-variant="link"] {
      position: relative;
      min-height: 0;
    }

    .btn[data-variant="link"]::after {
      position: absolute;
      inset: -12px -8px;
      content: "";
    }
  }
}

/* ── Input, textarea, label, field ────────────────────────────────────────── */

@layer components {
  .input,
  .textarea {
    min-width: 0;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-md);
    background: var(--ink-void);
    color: var(--text-primary);
    caret-color: var(--accent-bright);
    font-family: var(--font-sans);
    font-size: 0.9375rem;
    transition: border-color var(--motion-fast) var(--ease), box-shadow var(--motion-fast) var(--ease);
  }

  .input {
    min-height: 40px;
    padding: 8px 12px;
  }

  .textarea {
    display: block;
    min-height: 48px;
    padding: 13px 15px;
    background: var(--ink-surface);
    line-height: 1.5;
    resize: none;
  }

  .input::placeholder,
  .textarea::placeholder {
    color: var(--text-dim);
  }

  @media (hover: hover) {
    .input:hover:not(:disabled),
    .textarea:hover:not(:disabled) {
      border-color: color-mix(in oklch, var(--accent) 70%, var(--line-strong));
    }
  }

  /* Deliberately :focus, not :focus-visible: a text control earns its halo on
     pointer focus too, because focus is where typing goes — this is the one
     sanctioned focus treatment that is not keyboard-gated. It is a border and
     shadow, never an outline. */
  .input:focus,
  .textarea:focus {
    position: relative;
    z-index: 1;
    border-color: var(--accent-bright);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--accent-bright) 22%, transparent);
  }

  .field > label,
  .field > section > label,
  .label {
    margin-bottom: 7px;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: 0.8125rem;
    font-weight: 500;
    letter-spacing: 0;
    line-height: 1.5;
  }

  .field {
    display: block;
    min-width: 0;
  }
}

/* ── Alert ────────────────────────────────────────────────────────────────── */

@layer components {
  .alert {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    gap: 12px;
    padding: 13px 15px;
    border: 1px solid color-mix(in oklch, var(--info) 55%, transparent);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    box-shadow: var(--lift);
    color: var(--text-body);
    font-family: var(--font-sans);
    font-size: 0.8125rem;
  }

  .alert > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
    color: var(--info);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.02em;
  }

  .alert > section :is(p, ul, ol) {
    margin: 0;
  }

  .alert[data-variant="success"] {
    border-color: color-mix(in oklch, var(--success) 55%, transparent);
  }

  .alert[data-variant="success"] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
    color: var(--success);
  }

  .alert[data-variant="warning"] {
    border-color: color-mix(in oklch, var(--warning) 55%, transparent);
  }

  .alert[data-variant="warning"] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
    color: var(--warning);
  }

  .alert[data-variant="danger"] {
    border-color: color-mix(in oklch, var(--danger) 55%, transparent);
  }

  .alert[data-variant="danger"] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
    color: var(--danger);
  }

  /* In-flow variant: a full-bleed rule inside the app shell rather than a
     floating panel. Used by the composer error and memory status rows, which
     stay inline and never become a toast. */
  .alert[data-appearance="row"] {
    display: flex;
    min-height: 38px;
    align-items: center;
    justify-content: flex-start;
    gap: 12px;
    padding: 9px 18px;
    border: 0;
    border-bottom: 1px solid var(--line);
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    font-size: 0.8125rem;
  }

  .alert[data-appearance="row"] > :is(h2, h3, h4, h5, h6, strong, [data-title]) {
    flex: none;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.08em;
  }

  .alert[data-appearance="row"] > section {
    color: var(--text-body);
  }

  /* Bordered panel carrying prose, as on the authentication boundary. */
  .alert[data-appearance="notice"] {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: 5px;
    padding: 14px 16px;
    font-size: 0.875rem;
  }
}

/* ── Badge ────────────────────────────────────────────────────────────────── */

@layer components {
  /* Color reinforces but never replaces a text label, and the product states
     status in words rather than in chips. Badges are typographic. */
  .badge {
    gap: 8px;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.01em;
  }

  .badge[data-variant="info"] {
    color: var(--info);
  }

  .badge[data-variant="success"] {
    color: var(--success);
  }

  .badge[data-variant="warning"] {
    color: var(--warning);
  }

  .badge[data-variant="danger"] {
    color: var(--danger);
  }

  /* Merged pull requests and issues closed as completed. */
  .badge[data-variant="done"] {
    color: var(--done);
  }

  /* Technical metadata keeps the monospace accent. */
  .badge[data-variant="dim"] {
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
  }
}

/* ── Card ─────────────────────────────────────────────────────────────────── */

@layer components {
  /* A raised panel with room around it, rather than a full-bleed band. Still
     not a bubble: one hairline border and a restrained lift. */
  .card {
    gap: 0;
    margin: 14px 18px;
    padding: 20px 22px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    box-shadow: var(--lift);
  }

  .card > header {
    gap: 8px;
  }

  .card > header > :is(h1, h2, h3, [data-title], .card-title) {
    margin: 0;
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  .card > header > p,
  .card .card-description {
    max-width: 72ch;
    margin: 8px 0 0;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: 0.875rem;
    line-height: 1.6;
  }

  .card > footer {
    gap: 14px;
  }

  /* Arwes' `corners` frame, reconciled with the softened radius: the accent
     traces the existing corner arc instead of drawing a hard bracket across it.
     Diagonal rather than four-cornered, so it reads as an accent. */
  .card[data-frame="corners"] {
    position: relative;
  }

  .card[data-frame="corners"]::before,
  .card[data-frame="corners"]::after {
    position: absolute;
    width: 15px;
    height: 15px;
    border-color: color-mix(in oklch, var(--accent-bright) 60%, transparent);
    border-style: solid;
    content: "";
    pointer-events: none;
  }

  .card[data-frame="corners"]::before {
    top: -1px;
    left: -1px;
    border-width: 1px 0 0 1px;
    border-top-left-radius: var(--radius-lg);
  }

  .card[data-frame="corners"]::after {
    right: -1px;
    bottom: -1px;
    border-width: 0 1px 1px 0;
    border-bottom-right-radius: var(--radius-lg);
  }

  .card[data-state="active"] {
    border-color: color-mix(in oklch, var(--accent) 34%, var(--line));
    background: var(--ink-raised);
  }

  .card[data-variant="danger"] {
    border-color: color-mix(in oklch, var(--danger) 46%, transparent);
    background: var(--ink-raised);
  }

  .card[data-variant="danger"] > header > :is(h1, h2, h3, [data-title], .card-title) {
    color: var(--danger);
  }
}

/* ── Avatar ───────────────────────────────────────────────────────────────── */

@layer components {
  .avatar {
    width: 28px;
    height: 28px;
    border-radius: 9999px;
    background: var(--ink-raised);
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 0.6875rem;
    font-weight: 600;
  }

  .avatar::after {
    border-color: var(--line-strong);
    mix-blend-mode: normal;
  }

  .avatar[data-size="sm"] {
    width: 26px;
    height: 26px;
  }

  .avatar[data-size="lg"] {
    width: 36px;
    height: 36px;
  }

  .avatar[data-tone="accent"]::after {
    border-color: color-mix(in oklch, var(--accent-bright) 70%, transparent);
  }
}

/* ── Item ─────────────────────────────────────────────────────────────────── */

@layer components {
  /* Tool-activity rows: indicator, label, executor disclosure. */
  .item {
    min-height: 34px;
    flex-wrap: nowrap;
    gap: 10px;
    margin: 4px 0;
    padding: 6px 11px;
    border: 1px solid transparent;
    border-radius: var(--radius-md);
    background: color-mix(in oklch, var(--ink-raised) 62%, transparent);
    color: var(--info);
    font-family: var(--font-sans);
    font-size: 0.8125rem;
    transition: none;
  }

  .item > section {
    min-width: 0;
    flex-direction: row;
    color: var(--text-body);
  }

  .item > aside {
    flex: none;
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    text-align: right;
  }

  .item[data-status="succeeded"] {
    color: var(--success);
  }

  .item[data-status="failed"],
  .item[data-status="cancelled"],
  .item[data-status="interrupted"] {
    color: var(--danger);
  }

  .item[data-status="refused"],
  .item[data-status="unavailable"] {
    color: var(--warning);
  }
}

/* ── Event header ─────────────────────────────────────────────────────────── */

@layer components {
  /* One durable event as a disclosure row: chevron, status dot, quiet 13px
     one-line title, optional short status note in text, hover-revealed
     timestamp. Devin's event-header primitive over OpenAgents tokens; the
     expansion carries the bounded durable details. */
  .event-header {
    min-width: 0;
  }

  .event-header__row {
    display: flex;
    width: 100%;
    min-height: 30px;
    align-items: center;
    gap: 8px;
    padding: 5px 8px;
    border: 0;
    border-radius: calc(var(--radius) - 4px);
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    font-family: var(--font-sans);
    font-size: 0.8125rem;
    text-align: left;
    transition:
      background-color 140ms var(--ease),
      color 140ms var(--ease);
  }

  /* Whole-row brightening on hover — the `hover-unless-child` behavior is
     approximated by the row being the only control; child chips are inert. */
  .event-header__row:hover {
    background: var(--wash-hover);
    color: var(--text-primary);
  }

  .event-header__chevron {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    transition: transform 200ms var(--ease);
  }

  .event-header[data-expanded="true"] .event-header__chevron {
    transform: rotate(90deg);
  }

  .event-header__title {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* The outcome stays text, never color alone. Hue reinforces it. */
  .event-header__note {
    flex: none;
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
  }

  .event-header[data-status="failed"] .event-header__note,
  .event-header[data-status="cancelled"] .event-header__note,
  .event-header[data-status="interrupted"] .event-header__note {
    color: var(--danger);
  }

  .event-header[data-status="refused"] .event-header__note,
  .event-header[data-status="unavailable"] .event-header__note {
    color: var(--warning);
  }

  .event-header[data-status="requested"] .event-header__note,
  .event-header[data-status="running"] .event-header__note {
    color: var(--info);
  }

  .event-header__chips {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 4px;
  }

  .event-header__time {
    margin-left: auto;
    flex: none;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    opacity: 0;
    transition: opacity 140ms var(--ease);
  }

  .event-header__row:hover .event-header__time,
  .event-header__row:focus-visible .event-header__time {
    opacity: 1;
  }

  @media (hover: none) {
    .event-header__time {
      opacity: 1;
    }
  }

  /* Client-toggled expansion; a stream re-insert collapses honestly. */
  .event-header__details {
    display: none;
    padding: 2px 8px 10px 32px;
  }

  .event-header[data-expanded="true"] .event-header__details {
    display: block;
  }

  .event-detail {
    display: grid;
    gap: 8px;
    margin: 0;
  }

  .event-detail dt {
    color: var(--text-dim);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.06em;
  }

  .event-detail dd {
    margin: 4px 0 0;
    color: var(--text-muted);
    font-size: 0.8125rem;
    overflow-wrap: anywhere;
  }

  .event-detail pre {
    margin: 0;
    overflow-x: auto;
    padding: 8px 10px;
    border-radius: calc(var(--radius) - 2px);
    background: var(--ink-raised);
    box-shadow: inset 0 0 0 1px var(--line-soft);
    color: var(--text-body);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1.55;
    white-space: pre-wrap;
  }

  .event-detail__executor-id {
    display: block;
    color: var(--text-dim);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
  }
}

/* ── Empty ────────────────────────────────────────────────────────────────── */

@layer components {
  .empty {
    align-items: flex-start;
    margin: 14px 18px;
    padding: 40px 22px;
    border: 1px dashed var(--line);
    border-radius: var(--radius-lg);
    text-align: left;
  }

  .empty > header {
    max-width: none;
    align-items: flex-start;
  }

  .empty > header > :is(h1, h2, h3) {
    margin: 0;
    color: var(--text-primary);
    font-family: var(--font-sans);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  .empty > header > p {
    max-width: 72ch;
    margin: 8px 0 0;
    color: var(--text-muted);
    font-family: var(--font-sans);
    font-size: 0.875rem;
    line-height: 1.6;
    text-align: left;
  }
}

/* ── Kbd ──────────────────────────────────────────────────────────────────── */

@layer components {
  .kbd {
    padding: 2px 6px;
    border: 1px solid var(--line-strong);
    border-radius: calc(var(--radius) - 5px);
    background: var(--ink-raised);
    color: var(--text-muted);
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 500;
    letter-spacing: 0.02em;
  }
}

/* ── Icon ────────────────────────────────────────────────────────────────── */

@layer components {
  /* Sized at 1em and painted in currentColor, so a glyph inherits the type
     scale and semantic colour of whatever it sits in. The optical nudge lines
     the box up with the text baseline when an icon runs inline in a sentence;
     inside a control, flex centring already handles it. */
  .icon {
    display: inline-block;
    flex: none;
    vertical-align: -0.125em;
  }

  /* Inline actions set a small type size for their text. A glyph at that exact
     size reads as a speck, so it is scaled against its own label rather than
     given a fixed pixel size. */
  .btn[data-variant="link"] .icon {
    font-size: 1.25em;
  }
}

/* ── Frame ────────────────────────────────────────────────────────────────── */

@layer components {
  /* Arwes' `corners` frame. There it is eight SVG line segments whose endpoints
     are percentage expressions evaluated against the measured element; here the
     same eight strokes are background gradients, pinned to the four corners.
     No measurement, no runtime, and the stroke length stays fixed however the
     region resizes - which is the property the percentage DSL exists to
     preserve. */
  .frame[data-variant="corners"] {
    --frame-length: 18px;
    --frame-stroke: 1px;
    --frame-color: color-mix(in oklch, var(--accent-bright) 45%, transparent);

    position: relative;
    padding: clamp(24px, 4vw, 36px);
    background-image:
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color)),
      linear-gradient(var(--frame-color), var(--frame-color));
    background-repeat: no-repeat;
    background-position:
      left top,
      left top,
      right top,
      right top,
      left bottom,
      left bottom,
      right bottom,
      right bottom;
    background-size:
      var(--frame-length) var(--frame-stroke),
      var(--frame-stroke) var(--frame-length),
      var(--frame-length) var(--frame-stroke),
      var(--frame-stroke) var(--frame-length),
      var(--frame-length) var(--frame-stroke),
      var(--frame-stroke) var(--frame-length),
      var(--frame-length) var(--frame-stroke),
      var(--frame-stroke) var(--frame-length);
  }
}

/* ── Status indicator ─────────────────────────────────────────────────────── */

@layer components {
  /* OpenAgents semantic state marker: a small lit dot that reinforces a text
     label and never replaces it. Reserved meanings: green completed and
     connected, blue active and streaming, gold required attention, red
     failed. The halo is the state made legible at a glance, not decoration. */
  .status-indicator {
    display: inline-block;
    width: 8px;
    height: 8px;
    flex: none;
    border-radius: 9999px;
    background: currentColor;
    box-shadow: 0 0 0 3px color-mix(in oklch, currentColor 18%, transparent);
  }

  /* "ended", "live", and "attention" are the sidebar status rows' lifecycle
     vocabulary (DESIGN.md, OpenAgents shell): an ordinarily ended call is a muted
     fact, a live call is blue activity, and budget/interrupted outcomes ask
     for gold attention. Each joins the existing hue group; no new color. */
  .status-indicator[data-state="idle"],
  .status-indicator[data-state="ended"] {
    background: var(--text-dim);
    box-shadow: none;
  }

  .status-indicator[data-state="connected"],
  .status-indicator[data-state="listening"],
  .status-indicator[data-state="succeeded"] {
    background: var(--success);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--success) 18%, transparent);
  }

  .status-indicator[data-state="requested"],
  .status-indicator[data-state="running"],
  .status-indicator[data-state="streaming"],
  .status-indicator[data-state="responding"],
  .status-indicator[data-state="speaking"],
  .status-indicator[data-state="live"],
  .status-indicator[data-state="interrupted"] {
    background: var(--info);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--info) 20%, transparent);
  }

  .status-indicator[data-state="required"],
  .status-indicator[data-state="requesting"],
  .status-indicator[data-state="connecting"],
  .status-indicator[data-state="reconnecting"],
  .status-indicator[data-state="ending"],
  .status-indicator[data-state="refused"],
  .status-indicator[data-state="attention"],
  .status-indicator[data-state="unavailable"] {
    background: var(--warning);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--warning) 20%, transparent);
  }

  /* Recording is a fact about the call, not a fault or a success, and it reads
     as the one state the person is most entitled to notice. */
  .status-indicator[data-state="recording"] {
    background: var(--danger);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 28%, transparent);
  }

  .status-indicator[data-state="muted"],
  .status-indicator[data-state="blocked"],
  .status-indicator[data-state="failed"],
  .status-indicator[data-state="cancelled"] {
    background: var(--danger);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--danger) 20%, transparent);
  }

  /* Inside an activity row the row already carries the semantic color. */
  .item > .status-indicator {
    background: currentColor;
  }

  /* LiveView drops the socket: the connection marker tells the truth even
     though the server can no longer re-render it. */
  .phx-disconnected .status-indicator[data-state="connected"] {
    background: var(--warning);
    box-shadow: 0 0 0 3px color-mix(in oklch, var(--warning) 20%, transparent);
  }
}

/* ── Audio player ─────────────────────────────────────────────────────────── */

@layer components {
  /* Native controls, deliberately: the browser's transport is already keyboard
   * operable and already announced, and a hand-rolled one would have to re-earn
   * both. Only the box the transport sits in is OpenAgents. */
  .audio-player {
    display: block;
    width: 100%;
    height: 34px;
    border-radius: var(--radius);
    background: var(--ink-raised);
    /* Follow the document theme so the native control chrome matches the
       surface it sits on. Forcing dark here left light mode with a dark
       scrubber on a white card. */
    color-scheme: inherit;
  }

  .audio-player:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }
}

/* ── Popover / menu ───────────────────────────────────────────────────────── */

@layer components {
  /* OpenAgents keeps the native `popover` API: no Basecoat JavaScript, and the
   * control works without client-side script.
   *
   * Basecoat's own popover CSS is deliberately not imported. There, `.popover`
   * is the anchor and `[data-popover]` is the panel its JavaScript positions;
   * applying either to a native popover element gives it an author-origin
   * `display`, which outranks `[popover]:not(:popover-open) { display: none }`
   * and pins the menu open. Declare no `display` here so the user agent keeps
   * control of open and closed state. Position is app-shell layout. */
  .menu {
    /* Sized by its widest row rather than by its narrowest. */
    min-width: max-content;
    /* A native popover is centred by the user agent's `margin: auto`. Setting
       `margin: 0` defeated that and pinned every menu to the viewport's
       top-left corner -- the account menu included, not just this demo. Keep
       the UA's centring as the fallback and anchor to the trigger where the
       browser can. */
    margin: auto;
    padding: 0;
    overflow: hidden;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    box-shadow: var(--lift);
    color: var(--text-body);
  }

  /* Chrome makes a `popovertarget` invoker the popover's implicit anchor, so
     the panel can sit under the control that opened it. Progressive: browsers
     without this keep the centred fallback above. */
  @supports (position-area: bottom) {
    .menu {
      position-area: bottom span-right;
      margin: 6px 0 0;
    }
  }

  .menu::backdrop {
    background: transparent;
  }

  /* Menu rows are sidebar rows. Both are a 32px line with an icon, muted until
     hovered, and there is no reason for the account menu to invent a second
     set of metrics for the same shape. */
  .menu__identity {
    display: flex;
    align-items: center;
    gap: 10px;
    border-bottom: 1px solid var(--line);
    padding: 12px;
    /* Long names still have a ceiling: the menu is a menu, not a card. */
    max-width: 20rem;
  }

  .menu__identity span {
    display: flex;
    min-width: 0;
    flex-direction: column;
  }

  /* One line. The panel sizes to its content, and its other rows are short, so
     a two-word name wrapped mid-name and made the identity look like two
     fields. The name is what sets the panel's width now; an unusually long one
     truncates rather than wrapping, since the handle beneath it already
     identifies the account. */
  .menu__identity strong {
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.8125rem;
    font-weight: 600;
    line-height: 1.25rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .menu__identity small {
    color: var(--text-muted);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  .menu__form {
    margin: 0;
  }

  .menu__item {
    display: flex;
    width: 100%;
    align-items: center;
    gap: 8px;
    min-height: 32px;
    margin: 4px;
    padding-inline: 8px;
    border: 0;
    border-radius: 6px;
    background: transparent;
    color: var(--text-muted);
    cursor: pointer;
    font-size: 0.8125rem;
    font-weight: 500;
    line-height: 1.25rem;
    text-align: start;
    text-decoration: none;
    transition: color 150ms var(--ease), background 150ms var(--ease);
  }

  /* The margin above would double between adjacent rows. */
  .menu__form + .menu__form .menu__item,
  .menu__item + .menu__item {
    margin-block-start: 0;
  }

  .menu__item .icon {
    width: 16px;
    height: 16px;
    flex: none;
    font-size: 16px;
  }

  @media (hover: hover) {
    .menu__item:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .menu__item:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: -2px;
  }

  /* Menus ride the quick token (100ms), not the fast one: a picker should feel
     closer to instant than a state change. Progressive enhancement — guarded
     so a browser without :popover-open never inherits a stranded opacity: 0,
     and one without allow-discrete/@starting-style simply snaps, which was the
     previous behavior. The state is true the instant the popover toggles;
     nothing waits on the fade, and the global reduced-motion kill switch makes
     it instant. */
  @supports selector(:popover-open) {
    .menu:where([popover]) {
      opacity: 0;
      transition:
        opacity var(--motion-quick) var(--ease),
        display var(--motion-quick) var(--ease) allow-discrete,
        overlay var(--motion-quick) var(--ease) allow-discrete;
    }

    .menu:where([popover]):popover-open {
      opacity: 1;

      @starting-style {
        opacity: 0;
      }
    }
  }
}

/* ── Graph surface (SCV swarm) ────────────────────────────────────────────── */

/* Unit's visual language, expressed in the sanctioned palette. Status is
 * carried by ring STYLE as well as hue -- hairline, heavy, dashed, doubled --
 * so the taxonomy survives a colour-blind reader and a greyscale screenshot.
 * See docs/2026-08-20-scv-swarm-visualization-unit-audit.md. */

@layer components {
  .graph-surface {
    width: 100%;
    height: auto;
    overflow: visible;
    font-family: var(--font-sans);
  }

  .graph-node__body {
    fill: var(--ink-raised);
    stroke: var(--line-strong);
    stroke-width: 1;
    transition:
      fill var(--motion-fast) var(--ease),
      stroke var(--motion-fast) var(--ease),
      stroke-width var(--motion-fast) var(--ease);
  }

  .graph-node__label {
    fill: var(--text-muted);
    font-size: 9px;
    text-anchor: middle;
    pointer-events: none;
  }

  .graph-node[data-shape="circle"] {
    cursor: pointer;
  }

  /* SCV lifecycle. */
  .graph-node[data-status="idle"] .graph-node__body {
    stroke: var(--line);
  }

  .graph-node[data-status="running"] .graph-node__body {
    stroke: var(--accent);
    stroke-width: 2;
    fill: color-mix(in oklab, var(--accent) 12%, var(--ink-raised));
  }

  .graph-node[data-status="paused"] .graph-node__body {
    stroke: var(--warning);
    stroke-dasharray: 3 3;
  }

  .graph-node[data-status="circuit_open"] .graph-node__body {
    stroke: var(--danger);
    stroke-width: 2;
    fill: color-mix(in oklab, var(--danger) 10%, var(--ink-raised));
  }

  .graph-node[data-status="disabled"] .graph-node__body {
    stroke: var(--line-faint);
    fill: transparent;
  }

  .graph-node[data-status="disabled"] .graph-node__label {
    fill: var(--text-dim);
  }

  /* Work-item lifecycle, on rect nodes. */
  .graph-node[data-status="discovered"] .graph-node__body {
    stroke: var(--line-soft);
    stroke-dasharray: 2 2;
    fill: transparent;
  }

  .graph-node[data-status="admitted"] .graph-node__body {
    stroke: var(--line-strong);
  }

  .graph-node[data-status="completed"] .graph-node__body {
    stroke: var(--success);
    fill: color-mix(in oklab, var(--success) 12%, var(--ink-raised));
  }

  .graph-node[data-status="deferred"] .graph-node__body {
    stroke: var(--text-dim);
    stroke-dasharray: 4 2;
  }

  .graph-node[data-status="refused"] .graph-node__body {
    stroke: var(--warning);
  }

  .graph-node[data-status="failed"] .graph-node__body {
    stroke: var(--danger);
  }

  .graph-node--selected .graph-node__body {
    stroke: var(--text-primary);
    stroke-width: 2;
  }

  /* Links. Three coincident paths: hit area, visible stroke, text anchor. */
  .graph-link__hit {
    stroke: transparent;
    stroke-width: 12;
    fill: none;
    cursor: pointer;
  }

  .graph-link__base {
    stroke: var(--line-strong);
    stroke-width: 1;
    fill: none;
  }

  .graph-link__text-path {
    fill: none;
    stroke: none;
  }

  .graph-link__label {
    fill: var(--text-dim);
    font-size: 8px;
    text-anchor: middle;
  }

  .graph-link[data-kind="error"] .graph-link__base {
    stroke: var(--danger);
  }

  .graph-link[data-kind="data"] .graph-link__base {
    stroke: var(--line);
    stroke-dasharray: 2 2;
  }

  .graph-link[data-kind="type"] .graph-link__base {
    stroke: var(--line-soft);
  }

  .graph-link[data-kind="exposed"] .graph-link__base {
    stroke: var(--accent-bright);
  }

  .graph-link--active .graph-link__base {
    stroke: var(--accent);
  }

  .graph-link__pulse {
    fill: var(--accent);
  }

  .graph-arrow {
    fill: none;
    stroke: var(--line-strong);
    stroke-width: 1.5;
  }

  /* A step pulse is motion, and motion is the one channel that should mean
     something on this surface. Honour the reader's preference to stop it. */
  @media (prefers-reduced-motion: reduce) {
    .graph-link__pulse {
      display: none;
    }
  }
}

/* ── Breadcrumb ───────────────────────────────────────────────────────────── */

/* Basecoat supplies the list structure; this supplies the voice. The trail is
 * quiet by default and only the hovered ancestor brightens -- a breadcrumb is
 * orientation, not a call to action, so it should not compete with the page. */

@layer components {
  .breadcrumb {
    font-size: 0.8125rem;
    line-height: 1.25rem;
    color: var(--text-dim);
  }

  .breadcrumb > ol {
    gap: 6px;
  }

  .breadcrumb li {
    gap: 6px;
  }

  .breadcrumb__separator {
    color: var(--line-strong);
    user-select: none;
  }

  .breadcrumb__link {
    color: var(--text-muted);
    text-decoration: none;
    transition: color var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .breadcrumb__link:hover {
      color: var(--text-primary);
    }
  }

  .breadcrumb__link:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
  }

  .breadcrumb__current {
    color: var(--text-body);
  }
}

/* ── Docs header ──────────────────────────────────────────────────────────── */

/* Breadcrumb on the left, controls pushed right by the trail's own growth
 * rather than a fixed width, so a long component name never collides with the
 * controls -- it truncates instead. */

@layer components {
  .docs-column {
    display: flex;
    flex: 1;
    flex-direction: column;
    min-width: 0;
    overflow: hidden;
  }

  .docs-header {
    display: flex;
    flex: none;
    align-items: center;
    gap: 12px;
    min-height: 52px;
    padding-inline: 32px;
    border-bottom: 1px solid var(--line);
    background: var(--ink-void);
  }

  .docs-header .breadcrumb {
    min-width: 0;
    margin-right: auto;
  }

  .docs-header .breadcrumb__current {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .docs-header__controls {
    display: flex;
    flex: none;
    align-items: center;
    gap: 8px;
  }
}

/* ── Copy button ──────────────────────────────────────────────────────────── */

@layer components {
  .copy-button {
    gap: 6px;
  }

  /* One glyph is shown at a time; both occupy the flow so the control does not
     resize when it flips, which would nudge everything beside it. */
  .copy-button__done {
    display: none;
  }

  .copy-button[data-copied="true"] .copy-button__idle {
    display: none;
  }

  .copy-button[data-copied="true"] .copy-button__done {
    display: inline-flex;
    color: var(--success);
  }
}

/* ── SCV streams ──────────────────────────────────────────────────────────── */

/* One row per agent: glyph, identity, and the tail of its output. Rows are a
 * fixed height because a chatty agent must not push the others off screen --
 * fifteen rows that stay put are readable, fifteen that reflow are not. */

@layer components {
  .scv-streams {
    display: flex;
    flex-direction: column;
    gap: 1px;
    margin: 0;
    padding: 0;
    list-style: none;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--line-faint);
    overflow: hidden;
  }

  .scv-stream {
    display: flex;
    align-items: center;
    gap: 10px;
    height: 44px;
    padding-inline: 8px;
    background: var(--ink-surface);
  }

  .scv-stream[data-selected="true"] {
    background: var(--ink-hover);
  }

  .scv-stream__glyph {
    flex: none;
    width: 30px;
    height: 30px;
  }

  .scv-stream__body {
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: 1px;
    min-width: 0;
    flex: 1;
  }

  .scv-stream__meta {
    display: flex;
    align-items: baseline;
    gap: 8px;
    font-size: 0.6875rem;
    line-height: 1;
  }

  .scv-stream__id {
    color: var(--text-body);
    font-family: var(--font-mono);
  }

  .scv-stream__tool {
    color: var(--text-dim);
    font-family: var(--font-mono);
  }

  .scv-stream__status {
    margin-left: auto;
    color: var(--text-dim);
    text-transform: uppercase;
    letter-spacing: 0.06em;
  }

  .scv-stream[data-status="circuit_open"] .scv-stream__status {
    color: var(--danger);
  }

  .scv-stream[data-status="running"] .scv-stream__status {
    color: var(--accent-bright);
  }

  /* The tail is one line and clips from the left, so the newest text -- the end
     of the line -- is the part that stays visible as it grows. */
  .scv-stream__text {
    margin: 0;
    overflow: hidden;
    color: var(--text-muted);
    direction: rtl;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    line-height: 1.25;
    text-align: left;
    text-overflow: ellipsis;
    white-space: nowrap;
    unicode-bidi: plaintext;
  }

  .scv-stream__caret {
    display: inline-block;
    width: 6px;
    height: 12px;
    margin-left: 2px;
    background: var(--accent);
    vertical-align: text-bottom;
    animation: scv-stream-caret 1.1s steps(2, start) infinite;
  }

  @keyframes scv-stream-caret {
    0%,
    50% {
      opacity: 1;
    }
    50.01%,
    100% {
      opacity: 0;
    }
  }

  /* A blinking caret is motion for its own sake if the reader asked for less. */
  @media (prefers-reduced-motion: reduce) {
    .scv-stream__caret {
      animation: none;
    }
  }
}

/* ── Collapsible sidebar sections ─────────────────────────────────────────── */

/* Native <details>, so no JavaScript and no ARIA to keep in sync. The caret
 * rotates rather than swapping glyphs, which keeps one icon in the set. */

@layer components {
  .sidebar-section {
    display: flex;
    flex-direction: column;
    gap: 1px;
  }

  .sidebar-section__summary {
    display: flex;
    align-items: center;
    gap: 4px;
    cursor: pointer;
    list-style: none;
    user-select: none;
    border-radius: 6px;
    transition: color 150ms var(--ease);
  }

  @media (hover: hover) {
    .sidebar-section__summary:hover {
      color: var(--text-primary);
    }
  }

  /* The caret is a disclosure indicator, so it sits on the trailing edge where
     the eye lands after reading the label, not before it. */
  .sidebar-section__caret {
    order: 2;
    margin-left: auto;
  }

  /* Safari draws its own triangle unless this is removed. */
  .sidebar-section__summary::-webkit-details-marker {
    display: none;
  }

  .sidebar-section__summary:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
    border-radius: 6px;
  }

  .sidebar-section__caret {
    width: 12px;
    height: 12px;
    flex: none;
    font-size: 12px;
  }

  /* The caret animates only when a reader turned it. Navigation re-renders
     `open` from the server, and an unconditional transition made every move
     between pages play a rotation nobody asked for. The hook sets this
     attribute for the length of one click and no longer. */
  .sidebar-section[data-animate] .sidebar-section__caret {
    transition: transform 150ms var(--ease);
  }

  .sidebar-section[open] .sidebar-section__caret {
    transform: rotate(90deg);
  }

  .sidebar-section__items {
    display: flex;
    flex-direction: column;
    gap: 1px;
    /* Nested rows are one step shorter than the heading above them, which is
       what makes the hierarchy readable without indenting everything. */
    padding-block: 1px 6px;
  }

  .sidebar-section__items .sidebar-row {
    min-height: 30px;
  }

  @media (prefers-reduced-motion: reduce) {
    .sidebar-section__caret {
      transition: none;
    }
  }
}

/* ── Docs page: body and table of contents ────────────────────────────────── */

/* Two columns: the prose, and the page's own headings. The rail is fixed-width
 * and the prose is capped, because a measure much past ~70 characters costs
 * more in return sweeps than it gains in fewer lines. */

@layer components {
  .docs-page {
    display: flex;
    align-items: flex-start;
    gap: 48px;
  }

  .docs-prose {
    min-width: 0;
    /* Fills its column. The measure was capped at 68ch, which left a wide
       screen with a narrow ribbon of text beside a large empty margin; the
       column itself is what bounds the line now. */
    max-width: none;
    flex: 1;
    color: var(--text-body);
  }

  .docs-toc {
    position: sticky;
    top: 32px;
    flex: none;
    width: 200px;
    border-left: 1px solid var(--line);
    padding-left: 16px;
  }

  .docs-toc__title {
    margin: 0 0 8px;
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  .docs-toc ul {
    display: flex;
    flex-direction: column;
    gap: 6px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .docs-toc a {
    color: var(--text-muted);
    font-size: 0.8125rem;
    line-height: 1.25;
    text-decoration: none;
    transition: color 150ms var(--ease);
  }

  @media (hover: hover) {
    .docs-toc a:hover {
      color: var(--text-primary);
    }
  }

  /* Depth by indent rather than by size: a nested heading is subordinate, not
     less important, and shrinking it would say the wrong thing. */
  .docs-toc li[data-level="3"] {
    padding-left: 12px;
  }

  /* The rail costs more than it returns once it would crowd the prose. */
  @media (max-width: 1100px) {
    .docs-toc {
      display: none;
    }
  }
}

/* ── Rendered Markdown ────────────────────────────────────────────────────── */

/* One ruleset for every surface that renders Markdown to HTML: the docs pages,
 * a repository's README, and a Markdown file in the source browser. The class
 * used to be `.docs-prose`, which is why the README rendered as unstyled text
 * for as long as it did -- the markup asked for `.code-markdown`, a class that
 * was never written. Naming the ruleset after what it renders rather than
 * where it renders keeps the next surface from repeating that. Chat keeps its
 * own `.message-markdown`, tuned for a bubble rather than a document. */

@layer components {
  .markdown {
    min-width: 0;
    color: var(--text-body);
    /* A pasted URL or an unbroken token must wrap inside its card rather than
       widen it; forum posts carry these routinely. Inherited, so one
       declaration covers every descendant. */
    overflow-wrap: break-word;
  }

  .markdown h1 {
    margin: 0 0 8px;
    color: var(--text-primary);
    font-size: 2rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.2;
  }

  .markdown h2 {
    margin: 40px 0 12px;
    color: var(--text-primary);
    font-size: 1.375rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    scroll-margin-top: 24px;
  }

  .markdown h3 {
    margin: 28px 0 8px;
    color: var(--text-primary);
    font-size: 1.0625rem;
    font-weight: 600;
    scroll-margin-top: 24px;
  }

  .markdown p {
    margin: 0 0 16px;
    line-height: 1.7;
  }

  /* A README goes deeper than a docs page does. Below h3 the difference is
     carried by weight rather than size, because a fourth size in a document
     this shallow reads as noise. */
  .markdown :is(h4, h5, h6) {
    margin: 20px 0 8px;
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 600;
    scroll-margin-top: 24px;
  }

  .markdown ul,
  .markdown ol {
    margin: 0 0 16px;
    padding-left: 22px;
    line-height: 1.7;
  }

  .markdown ul {
    list-style: disc;
  }

  .markdown ol {
    list-style: decimal;
  }

  .markdown li {
    margin-bottom: 6px;
  }

  .markdown a {
    color: var(--accent-bright);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  @media (hover: hover) {
    .markdown a:hover {
      text-decoration: none;
    }
  }

  .markdown code {
    padding: 1px 5px;
    border: 1px solid var(--line);
    border-radius: 4px;
    background: var(--ink-raised);
    color: var(--text-primary);
    font-family: var(--font-mono);
    font-size: 0.8125em;
  }

  /* A fenced block is already a panel; the inline chrome would double it. */
  .markdown pre {
    margin: 0 0 16px;
    padding: 14px 16px;
    overflow-x: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--ink-raised);
  }

  .markdown pre code {
    padding: 0;
    border: 0;
    background: transparent;
    font-size: 0.8125rem;
    line-height: 1.6;
  }

  .markdown blockquote {
    margin: 0 0 16px;
    padding-left: 16px;
    border-left: 2px solid var(--line-strong);
    color: var(--text-muted);
  }

  .markdown hr {
    margin: 32px 0;
    border: 0;
    border-top: 1px solid var(--line);
  }

  .markdown strong {
    color: var(--text-primary);
    font-weight: 600;
  }

  .markdown table {
    width: 100%;
    margin: 0 0 16px;
    border-collapse: collapse;
    font-size: 0.875rem;
  }

  .markdown th,
  .markdown td {
    padding: 8px 10px;
    border-bottom: 1px solid var(--line);
    text-align: left;
  }

  .markdown th {
    color: var(--text-primary);
    font-weight: 600;
  }
}

/* ── Sidebar brand ────────────────────────────────────────────────────────── */

/* Wordmark, rule, section name. Two targets because they answer two questions:
 * the wordmark leaves for the application, the section name returns to this
 * section's index. The rule between them says they are separate controls --
 * without it, adjacent links at the same weight read as one. */

@layer components {
  .sidebar-brand {
    display: flex;
    flex: none;
    align-items: center;
    /* No gap here: the divider owns the spacing on both of its sides, because
       the two sides are not equal -- see `.sidebar-brand__divider`. */
    gap: 0;
    min-height: 52px;
    padding-inline: 20px;
  }

  .sidebar-brand__mark {
    flex: none;
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 700;
    letter-spacing: -0.01em;
    text-decoration: none;
    transition: color 150ms var(--ease);
  }

  .sidebar-brand__divider {
    width: 1px;
    height: 16px;
    flex: none;
    /* Not a symmetric gap. The mark is a PNG carrying about 16% transparent
       padding, so a gap measured from the image's box reads ~4px wider on that
       side than on the word's -- the eye measures from the glyph, not the box.
       6px plus that padding is the 10px on the right. */
    margin-inline: 6px 10px;
    background: var(--line-strong);
  }

  .sidebar-brand__title {
    min-width: 0;
    overflow: hidden;
    color: var(--text-body);
    font-size: 0.9375rem;
    font-weight: 500;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
    transition: color 150ms var(--ease);
  }

  @media (hover: hover) {
    .sidebar-brand__mark:hover,
    .sidebar-brand__title:hover {
      color: var(--text-primary);
    }
  }
}

/* ── Sidebar footer ───────────────────────────────────────────────────────── */

/* Secondary destinations, so they are quieter than the nav above: a rule
 * separates them, and hover moves colour only. Giving them the nav's background
 * wash would make a footer link look as important as a section you are working
 * in. */

@layer components {
  .sidebar-footer {
    display: flex;
    flex: none;
    flex-direction: column;
    align-items: stretch;
    margin-top: auto;
    border-top: 1px solid var(--line);
    /* Block padding only -- the inline inset comes from the link, so a footer
       link lines up with the nav rows above it. The 2px matches
       `.sidebar-nav`'s padding-block so both columns breathe identically. */
    padding: 8px 0 10px;
  }

  /* A footer link is a sidebar row. Same height, same insets, same gap, same
     type -- the only difference is that these are secondary destinations, so
     they sit under a rule. Matching .sidebar-row by hand had drifted: 36px
     against the row's 32px, which read as looser spacing in the footer than in
     the nav above it. */
  .sidebar-footer__link {
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: 0;
    min-height: 32px;
    margin-inline: 12px;
    padding-inline: 8px;
    border-radius: 6px;
    color: var(--text-muted);
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
    text-decoration: none;
    transition: color 150ms var(--ease), background 150ms var(--ease);
  }

  .sidebar-footer__link .icon {
    width: 16px;
    height: 16px;
    flex: none;
    /* The same 10px the nav rows use, so the two columns read as one set. */
    margin-right: 10px;
    font-size: 16px;
  }

  @media (hover: hover) {
    .sidebar-footer__link:hover {
      color: var(--text-primary);
    }
  }

  .sidebar-footer__link:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: 2px;
    border-radius: 6px;
  }
}

/* ── Chat ─────────────────────────────────────────────────────────────────── */

/* The conversation surface had no stylesheet at all. It was ported from a
 * codebase where it was the whole application, and its layout, message,
 * composer, activity and memory rules did not come with it -- seventy-five
 * classes resolved to nothing, which is why an unstyled column of text sat
 * under a floating rail. What follows is that stylesheet, written against the
 * tokens rather than reconstructed from the original.
 *
 * The surface owns the full height of the main area: the transcript is the
 * only thing that scrolls, and the composer is pinned under it rather than
 * riding at the end of a scrolling page. */

@layer components {
  .chat-shell {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
  }

  .app-main {
    display: flex;
    /* Both axes: `min-height` for the column it is, and `min-width` so a long
       unbroken line in the transcript cannot widen the column past its
       parent. */
    min-width: 0;
    min-height: 0;
    flex: 1;
    flex-direction: column;
  }

  /* ── Transcript ─────────────────────────────────────────────────────────── */

  /* Not a scroller any more: `conversation/1` is, and it clips at its own
     box so the return-to-newest control can hold still against the bottom
     edge. What is left here is the column the conversation fills. */
  .transcript {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
  }

  /* A reading column, centred, so a wide window does not stretch prose to the
     full width of a desktop display. The composer below uses the same measure,
     so the two share one axis. The display, direction, gap, and padding are
     `conversation_content/1`'s. */
  .message-list {
    width: 100%;
    max-width: 46rem;
    margin-inline: auto;
  }

  .history-control {
    display: flex;
    justify-content: center;
    padding-block: 4px;
  }

  /* Colour and size come from `message_content/1`'s own utilities, which
     outrank this layer; what is left is the prose rhythm inside a turn. */
  .message-markdown {
    line-height: 1.65;
    word-break: break-word;
  }

  .message-markdown > * + * {
    margin-block-start: 1em;
  }

  .message-markdown p + p {
    margin-block-start: 1.25em;
  }

  .message-markdown :is(p, ul, ol, pre, blockquote) + :is(h1, h2, h3, h4) {
    margin-block-start: 1.5em;
  }

  .message-markdown :is(h1, h2, h3, h4) + :is(p, ul, ol) {
    margin-block-start: 0.5em;
  }

  .message-markdown :is(h1, h2, h3, h4) {
    color: var(--text-primary);
    font-weight: 600;
    line-height: 1.3;
  }

  .message-markdown :is(ul, ol) {
    list-style-position: outside;
  }

  .message-markdown ul {
    padding-inline-start: 1.5em;
    list-style-type: disc;
  }

  .message-markdown ol {
    padding-inline-start: 2em;
    list-style-type: decimal;
  }

  .message-markdown li + li {
    margin-block-start: 0.25em;
  }

  .message-markdown a {
    color: var(--text-primary);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  .message-markdown code {
    border-radius: 4px;
    background: var(--wash-hover);
    padding: 0.125em 0.375em;
    font-size: 0.875em;
  }

  .message-markdown pre {
    position: relative;
    overflow-x: auto;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    padding: 12px 14px;
  }

  .message-markdown pre code {
    background: transparent;
    padding: 0;
  }

  /* Revealed on hover of the block it belongs to, and always present for
     keyboard reach -- an action that only exists on hover cannot be tabbed to. */
  .code-copy {
    position: absolute;
    top: 6px;
    right: 6px;
    opacity: 0;
    transition: opacity 150ms var(--ease);
  }

  .message-markdown pre:hover .code-copy,
  .code-copy:focus-visible {
    opacity: 1;
  }

  /* ── Message chrome ─────────────────────────────────────────────────────── */

  /* `message_actions/1` supplies the row; this supplies its voice. */
  .message-toolbar {
    color: var(--text-dim);
  }

  .message-toolbar__time {
    font-size: 0.75rem;
    line-height: 1rem;
  }

  /* The row's actions appear on hover of the message, not permanently: they
     are available on every message, and drawing them all would turn the
     transcript into a column of buttons. */
  .message-toolbar__button {
    opacity: 0;
    transition: opacity 150ms var(--ease);
  }

  [class*="message"]:hover .message-toolbar__button,
  .message-toolbar__button:focus-visible {
    opacity: 1;
  }

  .message-status,
  .message-provenance {
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  /* `queue/1` and its parts draw the queue itself. All this states is that
     the queue shares the composer's measure and sits above it. */
  .message-queue {
    width: 100%;
    max-width: 46rem;
    margin-inline: auto;
    margin-block-end: 8px;
  }

  /* ── Composer ───────────────────────────────────────────────────────────── */

  .composer-region {
    flex: none;
    background: var(--ink-void);
    /* No rule and no space above it. The composer already reads as its own
       control against the transcript, and a border plus 16px stacked a second
       edge under the one the card draws. */
    padding: 0 24px 20px;
  }

  /* `prompt_input/1`'s form is `w-full`, so the reading measure the composer
     shares with the transcript is stated here rather than by the component. */
  .composer {
    width: 100%;
    max-width: 46rem;
    margin-inline: auto;
  }

  /* Inside the input group's leading strip now, so it no longer carries a
     measure or a margin of its own — only its voice, and the full width of
     the strip it fills. */
  .composer-eyebrow {
    width: 100%;
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  .control-row {
    display: flex;
    max-width: 46rem;
    align-items: flex-end;
    gap: 8px;
    margin-inline: auto;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    padding: 6px 6px 6px 12px;
    transition: border-color 150ms var(--ease);
  }

  .control-row:focus-within {
    border-color: var(--text-dim);
  }

  .control-row__input {
    min-height: 32px;
    max-height: 40vh;
    flex: 1;
    resize: none;
    border: 0;
    background: transparent;
    padding-block: 6px;
    color: var(--text-primary);
    font-family: inherit;
    font-size: 0.9375rem;
    line-height: 1.5;
    outline: none;
  }

  .control-row__input::placeholder {
    color: var(--text-dim);
  }

  .voice-controller {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .voice-output {
    display: none;
  }

  /* ── Activity ───────────────────────────────────────────────────────────── */

  /* Each step is a `tool/1` block, which carries its own bottom margin, so
     this states the measure and nothing about the spacing between them. */
  .tool-activity {
    width: 100%;
    max-width: 46rem;
    margin-inline: auto;
  }
}

/* Screen-reader-only text. Not `display: none`, which removes it from the
 * accessibility tree along with the visual layout. */
@layer components {
  .visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ── Memory manager ───────────────────────────────────────────────────────── */

/* Opened from the sidebar, this replaces the transcript rather than covering
 * it: it is a place you go, not a dialog over where you were, and the row that
 * opens it becomes the row that returns you. */

@layer components {
  .memory-manager {
    display: flex;
    min-height: 0;
    flex: 1;
    flex-direction: column;
    gap: 16px;
    overflow-y: auto;
    padding: 8px 24px 24px;
  }

  .memory-header,
  .memory-records,
  .memory-correction,
  .memory-confirmation,
  .privacy-delete-form {
    width: 100%;
    max-width: 46rem;
    margin-inline: auto;
  }

  /* The prose column needs a basis. Without one it shrinks to fit whatever the
     action row demands, and that row holds four text buttons -- so the heading
     and its paragraph were squeezed into a ten-character ribbon while the
     controls took the width. */
  .memory-header {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: space-between;
    gap: 12px 16px;
  }

  .memory-header > :first-child {
    flex: 1 1 22rem;
    min-width: 0;
  }

  .memory-header h1 {
    color: var(--text-primary);
    font-size: 1.125rem;
    font-weight: 600;
    line-height: 1.4;
  }

  .memory-header p {
    max-width: 60ch;
    padding-block-start: 4px;
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.5;
  }

  .memory-header__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
  }

  .memory-records {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .memory-record__claim {
    color: var(--text-body);
    font-size: 0.9375rem;
    line-height: 1.5;
  }

  .memory-record__meta,
  .memory-record__sources {
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  .memory-record__controls {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
  }

  /* Deletion is stated in the control's colour, not only in its label. */
  .memory-record__destructive {
    color: var(--danger);
  }

  .memory-correction,
  .memory-confirmation {
    display: flex;
    flex-direction: column;
    gap: 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    padding: 12px;
  }

  .memory-confirmation__actions {
    display: flex;
    align-items: center;
    gap: 8px;
  }
}

/* ── Composer and transcript tails ────────────────────────────────────────── */

/* Send, stop, and end-a-call are `prompt_input_submit/1` and
 * `prompt_input_button/1` now, and each states its job through the button
 * variant it is given. What is left here is the two boxes those controls sit
 * in. */

@layer components {
  .voice-controller--in-composer {
    width: 100%;
  }

  .tool-activity--live {
    padding-block-start: 4px;
  }
}

/* ── Shimmer ──────────────────────────────────────────────────────────────── */

/* The sweep behind `OpenAgentsWeb.AI.Conversation.shimmer/1`. AI Elements
 * animates `background-position` with `motion/react` across a gradient 250% as
 * wide as the box, clipped to the glyphs: a bright band travels through muted
 * text and the text reads as still arriving. The component carries the whole
 * gradient and marks itself `data-shimmer`; the movement is this rule.
 *
 * The declaration it overrides — `[background-position:50%_center]` — is a
 * Tailwind utility, and utilities outrank `@layer components`. An animation
 * wins anyway: animated values sit above all normal declarations in the
 * cascade whatever layer they came from. That is also why the component had to
 * drop `animate-pulse` rather than have it overridden — two animations on one
 * element, and the later `animation` shorthand simply replaces the earlier. */

@keyframes shimmer-sweep {
  from {
    background-position: 100% center;
  }

  to {
    background-position: 0% center;
  }
}

@layer components {
  [data-shimmer="true"] {
    animation: shimmer-sweep 2s linear infinite;
  }

  /* Stated twice on purpose: the component also carries
     `motion-reduce:animate-none`, and whichever of the two lands first, the
     band holds still. */
  @media (prefers-reduced-motion: reduce) {
    [data-shimmer="true"] {
      animation: none;
    }
  }
}

/* ── Landing ──────────────────────────────────────────────────────────────── */

/* Marketing composition, adapted from Launch UI (MIT, © 2024 Mikolaj
 * Dobrucki). The source is React and Tailwind and paints with its own `brand`
 * palette and layered `glass-*` gradients; every value here resolves to an
 * existing OpenAgents token instead, because a landing page that does not look
 * like the product it advertises is worse than a plainer one that does.
 *
 * Sections are divided by a hairline rather than by fill, so the page reads as
 * one surface in parts rather than a stack of coloured panels. */

@layer components {
  .landing-section {
    position: relative;
    /* Glows are scaled well past their own box -- 2.5x on the wash -- and a
       scaled element paints outside it. Unclipped, the hero's glow widened the
       page and left a band of empty scroll to the right of every section.
       `clip` rather than `hidden`: it does not make the section a scroll
       container, so sticky and anchor behaviour inside it are unaffected. */
    overflow-x: clip;
    border-bottom: 1px solid var(--line-faint);
    padding: 64px 24px;
  }

  .landing-section--flush {
    border-bottom: 0;
  }

  @media (min-width: 640px) {
    .landing-section {
      padding-block: 96px;
    }
  }

  @media (min-width: 1024px) {
    .landing-section {
      padding-block: 128px;
    }
  }

  .landing-section__inner {
    position: relative;
    z-index: 1;
    display: flex;
    width: 100%;
    max-width: 1104px;
    flex-direction: column;
    align-items: center;
    margin-inline: auto;
    gap: 32px;
  }

  .landing-heading {
    max-width: 36ch;
    color: var(--text-primary);
    font-size: clamp(1.75rem, 1.2rem + 2.4vw, 3rem);
    font-weight: 600;
    letter-spacing: -0.02em;
    line-height: 1.15;
    text-align: center;
    text-wrap: balance;
  }

  /* ── Glow ───────────────────────────────────────────────────────────────── */

  /* Two ellipses, not one: a wide faint wash under a narrower dense core. A
     single gradient reads as a flat smudge; the pair reads as light. Both are
     ellipses via a 50% radius on a wide, short box, which is cheaper than a
     blur filter and does not cost a compositor layer. */
  .glow {
    position: absolute;
    left: 0;
    width: 100%;
    pointer-events: none;
    /* Two independent dials, multiplied, so neither has to know the other's
       value: `--glow-base` is what the theme allows, `--glow-scale` is what a
       given placement asks for. A single `opacity` cannot do this -- the light
       theme's rule outranks a placement's, so dimming one glow would silently
       do nothing in light mode. */
    opacity: calc(var(--glow-base, 1) * var(--glow-scale, 1));
  }

  .glow--top { top: 0; }
  .glow--above { top: -128px; }
  .glow--bottom { bottom: 0; }
  .glow--below { bottom: -128px; }
  .glow--center { top: 50%; }

  .glow__wash,
  .glow__core {
    position: absolute;
    left: 50%;
    border-radius: 50%;
    translate: -50% 0;
  }

  .glow--center .glow__wash,
  .glow--center .glow__core {
    translate: -50% -50%;
  }

  .glow__wash {
    width: 60%;
    height: 256px;
    background: radial-gradient(
      closest-side,
      color-mix(in oklab, var(--text-primary) 22%, transparent) 10%,
      transparent 70%
    );
    scale: 2.5;
  }

  .glow__core {
    width: 40%;
    height: 128px;
    background: radial-gradient(
      closest-side,
      color-mix(in oklab, var(--text-primary) 30%, transparent) 10%,
      transparent 70%
    );
    scale: 2;
  }

  /* Light mode carries none of it. A glow is light spilling from behind
     something, which a dark surface can show and a white one cannot: there is
     nothing brighter than the page for it to be. Dimmed to a third it did not
     read as light, it read as a grey smudge behind the words.

     Removed rather than made transparent, so it is not painted or composited
     at all in the theme that never shows it. */
  :root[data-theme="light"] .glow,
  :root:not([data-theme="dark"]) .glow {
    display: none;
  }

  @media (min-width: 640px) {
    .glow__wash { height: 512px; }
    .glow__core { height: 256px; }
  }

  /* ── Beam ───────────────────────────────────────────────────────────────── */

  .beam {
    position: relative;
  }

  .beam::after {
    content: "";
    position: absolute;
    z-index: -1;
    inset: 0;
    border-radius: 50%;
    scale: 2;
  }

  .beam--default::after {
    background: radial-gradient(
      closest-side,
      color-mix(in oklab, var(--text-primary) 26%, transparent) 10%,
      transparent 65%
    );
  }

  .beam--bright::after {
    background: radial-gradient(
      closest-side,
      color-mix(in oklab, var(--text-ink) 34%, transparent) 10%,
      transparent 65%
    );
  }

  /* ── Mockup ─────────────────────────────────────────────────────────────── */

  /* A second, wider border in a lighter fill is what reads as a device edge.
     Drawing a literal laptop dates the page and fixes the aspect ratio of
     whatever goes inside it. */
  .mockup-frame {
    position: relative;
    z-index: 1;
    display: flex;
    overflow: hidden;
    border-radius: 20px;
    background: color-mix(in oklab, var(--line) 45%, transparent);
  }

  .mockup-frame--small { padding: 8px; }
  .mockup-frame--large { padding: 16px; }

  .mockup {
    position: relative;
    z-index: 1;
    display: flex;
    width: 100%;
    overflow: hidden;
    border: 1px solid var(--line);
    background: var(--ink-surface);
    box-shadow: var(--lift);
  }

  .mockup--window { border-radius: 12px; }

  .mockup--phone {
    max-width: 350px;
    border-radius: 44px;
  }

  .mockup > * {
    width: 100%;
    height: auto;
  }

  /* ── Hero ───────────────────────────────────────────────────────────────── */

  /* Weighted to the top. A landing page opens on nothing but the claim, and
     equal padding above and below sets it mid-band, where it reads as one item
     in a list rather than as the first thing on the page. */
  .hero {
    padding-block: 128px 64px;
  }

  @media (min-width: 640px) {
    .hero {
      padding-block: 160px 96px;
    }
  }

  @media (min-width: 1024px) {
    .hero {
      padding-block-start: 192px;
    }
  }

  .hero__lede {
    display: flex;
    max-width: 48rem;
    flex-direction: column;
    align-items: center;
    gap: 24px;
    text-align: center;
  }

  .hero__title {
    color: var(--text-primary);
    font-size: clamp(2.25rem, 1.45rem + 2.6vw, 3.75rem);
    font-weight: 500;
    letter-spacing: -0.025em;
    line-height: 1.1;
    text-wrap: balance;
  }

  /* Inline and quiet, ahead of the name it introduces. The word space is a
     margin rather than a literal space in the template: HEEx collapses
     whitespace between an interpolation and its neighbour, so a space typed
     there is not one you can rely on. */
  .hero__title-lead {
    margin-inline-end: 0.25em;
    color: var(--text-muted);
  }

  .hero__description {
    max-width: 36rem;
    color: var(--text-muted);
    font-size: clamp(1rem, 0.94rem + 0.3vw, 1.125rem);
    line-height: 1.625;
    text-wrap: balance;
  }

  .hero__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
  }

  /* The frame sets its own width from the command inside it, so it ends where
     the command does rather than spanning the measure. It still has to be able
     to shrink: on a narrow viewport the command scrolls inside the frame. */
  .hero__command {
    max-width: 100%;
  }

  /* Tight to the command it qualifies rather than on the lede's 24px rhythm:
     it is a footnote to the frame above it, not the next thing in the band. */
  .hero__command + .hero__note {
    margin-block-start: -12px;
  }

  .hero__note {
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  .hero__links {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px;
  }

  /* Quieter than a secondary action and quieter than body text: these are for
     a reader the band did not answer, and at full contrast they compete with
     the command, which is the one thing the band is asking for. */
  .hero__link.btn {
    gap: 2px;
    border-radius: 999px;
    color: var(--text-muted);
    font-weight: 500;
  }

  .hero__link.btn:hover {
    color: var(--text-primary);
  }

  .hero__link .icon {
    color: inherit;
    opacity: 0.7;
  }

  /* ── Announcement pill ──────────────────────────────────────────────────── */

  .announce {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    border: 1px solid var(--line-soft);
    border-radius: 999px;
    padding: 5px 5px 5px 10px;
    background: color-mix(in oklab, var(--ink-surface) 60%, transparent);
    backdrop-filter: blur(6px);
    text-decoration: none;
    transition:
      border-color var(--motion-fast) var(--ease),
      background-color var(--motion-fast) var(--ease);
  }

  .announce:hover {
    border-color: var(--line-strong);
    background: var(--ink-surface);
  }

  /* The band's only accent. It is what makes the pill findable before it is
     read, which is the whole reason an announcement sits above a headline. */
  .announce__tag {
    display: inline-flex;
    align-items: center;
    border: 1px solid color-mix(in oklab, var(--accent-bright) 45%, transparent);
    border-radius: 999px;
    padding: 2px 7px;
    color: var(--accent-bright);
    font-size: 0.625rem;
    font-weight: 600;
    letter-spacing: 0.06em;
    line-height: 1;
    text-transform: uppercase;
  }

  .announce__text {
    font-size: 0.8125rem;
    letter-spacing: -0.01em;
    white-space: nowrap;
  }

  .announce__lead {
    color: var(--text-primary);
    font-weight: 600;
  }

  .announce__separator {
    color: var(--text-dim);
  }

  .announce__detail {
    color: var(--text-muted);
    transition: color var(--motion-fast) var(--ease);
  }

  .announce:hover .announce__detail {
    color: var(--text-body);
  }

  .announce__glyph {
    display: inline-flex;
    width: 24px;
    height: 24px;
    flex-shrink: 0;
    align-items: center;
    justify-content: center;
    border-radius: 999px;
    background: var(--wash-hover);
    color: var(--icon-tertiary);
    font-size: 0.625rem;
    transition:
      background-color var(--motion-fast) var(--ease),
      color var(--motion-fast) var(--ease);
  }

  .announce:hover .announce__glyph {
    background: var(--wash-selected);
    color: var(--icon-primary);
  }

  /* On a narrow viewport the detail is the half that goes: the tag and the
     claim are the announcement, and the rest is elaboration that would
     otherwise push the pill wider than the screen. */
  @media (max-width: 480px) {
    .announce__separator,
    .announce__detail {
      display: none;
    }
  }

  /* ── Install command ────────────────────────────────────────────────────── */

  .install-command {
    max-width: 100%;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-xl);
    transition: border-color var(--motion-fast) var(--ease);
  }

  .install-command:hover {
    border-color: var(--line-strong);
  }

  .install-command__button {
    position: relative;
    display: flex;
    width: 100%;
    min-width: 0;
    align-items: center;
    padding: 12px 16px;
    cursor: pointer;
    font-family: var(--font-mono);
    font-size: 0.8125rem;
    text-align: left;
  }

  @media (min-width: 640px) {
    .install-command__button {
      font-size: 0.875rem;
    }
  }

  /* Scrolls rather than wraps, and fades out under the glyph, so a command too
     long for the viewport reads as continuing rather than as truncated. */
  .install-command__text {
    width: 100%;
    min-width: 0;
    overflow-x: auto;
    /* Wider than the fade, so a command that fits ends before the gradient
       starts. At 28px the tail of every command was dimmed as though it had
       been truncated, when nothing was being cut off at all. */
    padding-inline-end: 56px;
    color: var(--text-body);
    scrollbar-width: none;
    white-space: nowrap;
    mask-image: linear-gradient(to right, black calc(100% - 48px), transparent calc(100% - 32px));
  }

  .install-command__text::-webkit-scrollbar {
    display: none;
  }

  .install-command__prompt {
    margin-inline-end: 0.5ch;
    color: var(--text-dim);
  }

  .install-command__glyph {
    position: absolute;
    inset-inline-end: 16px;
    display: inline-flex;
    color: var(--icon-tertiary);
    transition: color var(--motion-fast) var(--ease);
  }

  .install-command__button:hover .install-command__glyph {
    color: var(--icon-primary);
  }

  .install-command__button[data-copied="true"] .install-command__glyph {
    color: var(--success);
  }

  .install-command__done,
  .install-command__button[data-copied="true"] .install-command__idle {
    display: none;
  }

  .install-command__button[data-copied="true"] .install-command__done {
    display: inline-flex;
  }

  .hero__figure {
    position: relative;
    width: 100%;
    padding-top: 32px;
  }

  .hero__figure-inner {
    position: relative;
    z-index: 1;
  }

  /* ── Features ───────────────────────────────────────────────────────────── */

  /* No borders and no fills on the cells. A grid of boxes competes with itself
     for attention, and the alignment already separates them. */
  .feature-grid {
    display: grid;
    width: 100%;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
  }

  @media (min-width: 640px) {
    .feature-grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 16px;
    }
  }

  @media (min-width: 1024px) {
    .feature-grid {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
  }

  .feature {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 16px;
  }

  .feature__title {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 600;
    letter-spacing: -0.01em;
    line-height: 1.3;
  }

  .feature__icon {
    width: 18px;
    height: 18px;
    flex: none;
    color: var(--icon-secondary);
    font-size: 18px;
  }

  .feature__description {
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.5;
    text-wrap: pretty;
  }

  /* ── Stats ──────────────────────────────────────────────────────────────── */

  .stat-row {
    display: grid;
    width: 100%;
    max-width: 960px;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 40px;
  }

  @media (min-width: 640px) {
    .stat-row {
      grid-template-columns: repeat(4, minmax(0, 1fr));
    }
  }

  .stat {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }

  .stat__label {
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-weight: 600;
  }

  .stat__value {
    display: flex;
    align-items: baseline;
    gap: 4px;
    color: var(--text-primary);
    font-size: clamp(2.25rem, 1.5rem + 3vw, 3.75rem);
    font-weight: 500;
    font-variant-numeric: tabular-nums;
    letter-spacing: -0.03em;
    line-height: 1;
  }

  .stat__suffix {
    color: var(--text-muted);
    font-size: 0.4em;
    font-weight: 600;
  }

  .stat__description {
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-weight: 500;
    text-wrap: pretty;
  }

  /* ── Pricing ────────────────────────────────────────────────────────────── */

  .pricing-column {
    position: relative;
    display: flex;
    flex: 1;
    flex-direction: column;
    gap: 24px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    padding: 28px;
  }

  /* A hairline that fades at both ends, so the top of the column is lit rather
     than boxed. On the featured column it is brighter, which is the whole of
     the emphasis -- no accent fill, no scaling. */
  .pricing-column__rule {
    position: absolute;
    top: 0;
    left: 10%;
    width: 80%;
    height: 1px;
    border: 0;
    background: linear-gradient(
      to right,
      transparent,
      color-mix(in oklab, var(--text-primary) 45%, transparent),
      transparent
    );
  }

  .pricing-column--featured {
    border-color: var(--line-strong);
    box-shadow: var(--lift);
  }

  .pricing-column--featured .pricing-column__rule {
    background: linear-gradient(to right, transparent, var(--text-primary), transparent);
  }

  .pricing-column__header {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  .pricing-column__name {
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 700;
  }

  .pricing-column__description {
    max-width: 26ch;
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.5;
  }

  .pricing-column__price {
    display: flex;
    align-items: baseline;
    gap: 6px;
    color: var(--text-primary);
    font-size: 2.5rem;
    font-weight: 500;
    letter-spacing: -0.03em;
    line-height: 1;
  }

  .pricing-column__note {
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-weight: 500;
  }

  .pricing-column__features {
    display: flex;
    flex-direction: column;
    gap: 10px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .pricing-column__features li {
    display: flex;
    align-items: flex-start;
    gap: 8px;
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.4;
  }

  .pricing-column__features .icon {
    width: 16px;
    height: 16px;
    flex: none;
    margin-top: 2px;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  /* ── FAQ ────────────────────────────────────────────────────────────────── */

  .faq-list {
    display: flex;
    width: 100%;
    max-width: 800px;
    flex-direction: column;
  }

  .faq-item {
    border-bottom: 1px solid var(--line-faint);
  }

  .faq-item__question {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    cursor: pointer;
    list-style: none;
    padding-block: 20px;
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 500;
    line-height: 1.4;
    text-align: start;
  }

  .faq-item__question::-webkit-details-marker {
    display: none;
  }

  .faq-item__question:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: -2px;
  }

  .faq-item__caret {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
    transition: transform var(--motion-fast, 150ms) var(--ease);
  }

  .faq-item[open] .faq-item__caret {
    transform: rotate(90deg);
  }

  .faq-item__answer {
    padding-block-end: 20px;
    color: var(--text-muted);
    font-size: 0.9375rem;
    line-height: 1.6;
    text-wrap: pretty;
  }

  .faq-item__answer > * + * {
    margin-block-start: 0.75em;
  }

  /* ── CTA ────────────────────────────────────────────────────────────────── */

  .cta {
    overflow: hidden;
  }

  .cta__description {
    max-width: 44ch;
    color: var(--text-muted);
    font-size: 1rem;
    line-height: 1.55;
    text-align: center;
    text-wrap: balance;
  }

  .cta__actions {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 12px;
  }

  /* The light rises as the reader arrives at the last thing on the page. */
  .cta__glow {
    translate: 0 16px;
    opacity: 0.8;
    transition:
      translate var(--motion-slow, 500ms) var(--ease),
      opacity var(--motion-slow, 500ms) var(--ease);
  }

  .cta:hover .cta__glow {
    translate: 0 -32px;
    opacity: 1;
  }

  /* ── Logo wall ──────────────────────────────────────────────────────────── */

  .logos__title {
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-weight: 500;
    text-align: center;
  }

  .logo-wall {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 24px 40px;
  }

  /* One weight for every mark, so a heavier logo cannot dominate the row. */
  .logo-wall__item {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--text-dim);
    font-size: 1rem;
    font-weight: 600;
    letter-spacing: -0.01em;
  }

  .logo-wall__item .icon {
    width: 20px;
    height: 20px;
    font-size: 20px;
  }

  /* ── Footer ─────────────────────────────────────────────────────────────── */

  .landing-footer {
    border-top: 1px solid var(--line);
    padding: 48px 24px 32px;
  }

  .landing-footer__inner {
    display: grid;
    width: 100%;
    max-width: 1104px;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 32px;
    margin-inline: auto;
  }

  @media (min-width: 768px) {
    .landing-footer__inner {
      grid-template-columns: 2fr repeat(3, minmax(0, 1fr));
    }
  }

  .landing-footer__name {
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 700;
  }

  .landing-footer__tagline {
    max-width: 32ch;
    padding-block-start: 6px;
    color: var(--text-muted);
    font-size: 0.875rem;
    line-height: 1.5;
  }

  .landing-footer__column {
    display: flex;
    flex-direction: column;
    gap: 8px;
  }

  .landing-footer__column-title {
    color: var(--text-primary);
    font-size: 0.8125rem;
    font-weight: 600;
  }

  .landing-footer__column a {
    color: var(--text-muted);
    font-size: 0.875rem;
    text-decoration: none;
    transition: color 150ms var(--ease);
  }

  @media (hover: hover) {
    .landing-footer__column a:hover {
      color: var(--text-primary);
    }
  }

  .landing-footer__note {
    max-width: 1104px;
    margin-inline: auto;
    padding-block-start: 32px;
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  /* ── Layout lines ───────────────────────────────────────────────────────── */

  .layout-lines {
    position: fixed;
    z-index: 0;
    inset: 0;
    pointer-events: none;
  }

  .layout-lines__column {
    width: 100%;
    max-width: 1104px;
    height: 100%;
    margin-inline: auto;
    border-inline: 1px dashed var(--line-faint);
  }

  /* ── Appear ─────────────────────────────────────────────────────────────── */

  /* Decorative only: the page is complete without it, which is why the
     reduced-motion rule can simply drop the animation and leave the element at
     its final state rather than substituting a shorter one. */
  .appear,
  .appear-zoom {
    animation-duration: 600ms;
    animation-fill-mode: forwards;
    animation-timing-function: var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
    opacity: 0;
    /* These animate opacity, translate and scale, which a compositor can run
       on its own. Saying so gets each one its own layer up front, so the hero
       glow -- a very large gradient being scaled -- composites instead of
       repainting its whole area every frame. Four elements, all above the
       fold, so the layers are cheap and short-lived. */
    will-change: opacity, translate, scale;
  }

  .appear { animation-name: landing-appear; }
  .appear-zoom { animation-name: landing-appear-zoom; }

  .appear--delay-1 { animation-delay: 100ms; }
  .appear--delay-2 { animation-delay: 300ms; }
  .appear--delay-3 { animation-delay: 500ms; }
  .appear--delay-4 { animation-delay: 700ms; }

  /* Opacity and translate only. This also animated `filter: blur()`, which no
     compositor can do: every frame repainted the element at its full size, and
     the largest element here is a 5.5rem headline spanning the viewport. The
     dropped frames read as the animation stuttering and starting over. */
  @keyframes landing-appear {
    from {
      opacity: 0;
      translate: 0 1rem;
    }
    to {
      opacity: 1;
      translate: 0 0;
    }
  }

  @keyframes landing-appear-zoom {
    from {
      opacity: 0;
      scale: 0.9;
    }
    to {
      opacity: 1;
      scale: 1;
    }
  }

  @media (prefers-reduced-motion: reduce) {
    .appear,
    .appear-zoom {
      animation: none;
      opacity: 1;
      will-change: auto;
    }

    .cta__glow,
    .faq-item__caret {
      transition: none;
    }
  }
}

/* ── Landing demos ────────────────────────────────────────────────────────── */

/* Landing blocks are built to own a viewport, so the component library shows
 * them contained. The frame is the honest part: it says "this is a scaled-down
 * view", rather than pretending the documentation column is a page. */

@layer components {
  .demo-frame {
    position: relative;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-void);
  }

  /* Landing sections set their own generous rhythm against a viewport; inside
     a demo frame that rhythm is the only thing on screen. */
  .demo-frame .landing-section {
    padding-block: 32px;
    padding-inline: 20px;
  }

  .demo-frame .hero__title {
    font-size: clamp(1.75rem, 1.2rem + 2vw, 2.75rem);
  }

  .demo-frame .landing-footer {
    padding: 28px 20px 20px;
  }

  .demo-frame--lines {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 160px;
  }

  .demo-screenshot {
    display: flex;
    width: 100%;
    min-height: 120px;
    align-items: center;
    justify-content: center;
    background: var(--ink-raised);
    color: var(--text-dim);
    font-size: 0.8125rem;
  }

  .demo-screenshot--tall {
    min-height: 220px;
  }

  .demo-glow-row {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
  }

  .demo-glow {
    position: relative;
    display: flex;
    min-width: 160px;
    min-height: 120px;
    flex: 1;
    align-items: center;
    justify-content: center;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-void);
  }

  .demo-glow__label {
    position: relative;
    z-index: 1;
    color: var(--text-muted);
    font-size: 0.75rem;
  }

  .demo-beam {
    display: flex;
    min-width: 160px;
    min-height: 120px;
    flex: 1;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-void);
  }

  .demo-mockup-row,
  .demo-pricing-row {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    gap: 16px;
  }

  .demo-mockup-row > *,
  .demo-pricing-row > * {
    min-width: 260px;
    flex: 1;
  }
}

/* The landing page is the one surface that scrolls itself: its bands set
 * vertical rhythm against the viewport, so it owns the main area rather than
 * sitting inside a padded, centred column. */

@layer components {
  .landing-page {
    position: relative;
    min-height: 0;
    flex: 1;
    overflow-y: auto;
    /* The scroll container is the last place horizontal overflow can surface,
       so it refuses it outright: a decorative element must never be able to
       give the page a horizontal scrollbar. */
    overflow-x: hidden;
    overscroll-behavior: none;
  }

  .landing-figure {
    display: flex;
    width: 100%;
    min-height: 220px;
    flex-direction: column;
    background: var(--ink-raised);
  }

  .landing-figure__bar {
    display: flex;
    flex: none;
    align-items: center;
    gap: 6px;
    border-bottom: 1px solid var(--line);
    padding: 10px 12px;
  }

  .landing-figure__bar span {
    width: 10px;
    height: 10px;
    border-radius: 50%;
    background: var(--line-strong);
  }

  .landing-figure__caption {
    display: flex;
    flex: 1;
    align-items: center;
    justify-content: center;
    color: var(--text-dim);
    font-size: 0.8125rem;
  }
}

/* ── Login ────────────────────────────────────────────────────────────────── */

/* Signing in leaves the page, so between the click and the redirect there is a
 * window where an idle-looking button invites a second click and a second
 * OAuth attempt. The pending state closes it.
 *
 * Both `[data-pending]` and `:disabled` select it: the hook sets the first, but
 * a browser restoring the page on back-navigation, or one running no script at
 * all, should still not show a spinner-less disabled button. */

@layer components {
  .login-form {
    margin: 0;
  }

  .login-button__spinner {
    display: none;
    animation: login-spin 900ms linear infinite;
  }

  .login-button[data-pending] .login-button__mark,
  .login-button:disabled .login-button__mark {
    display: none;
  }

  .login-button[data-pending] .login-button__spinner,
  .login-button:disabled .login-button__spinner {
    display: inline-flex;
  }

  .login-button:disabled {
    cursor: not-allowed;
    opacity: 0.6;
  }

  @keyframes login-spin {
    to {
      rotate: 360deg;
    }
  }

  /* The spinner is the only thing reporting that the round-trip started, so it
     keeps turning; the disabled state and the swapped glyph already carry the
     meaning without it, which is what a reduced-motion reader gets. */
  @media (prefers-reduced-motion: reduce) {
    .login-button__spinner {
      animation: none;
    }
  }
}

/* ── Dashboard ────────────────────────────────────────────────────────────── */

/* The signed-in home. A marketing pitch is the wrong thing to show someone who
 * has already signed in, so this is the state of the work instead: a feed
 * column beside a rail of standing facts, the shape a code host's home takes.
 *
 * Panels are bordered rather than filled. A page of filled cards on a dark
 * surface reads as a stack of separate applications; a hairline says "these
 * are parts of one page" while still separating them. */

@layer components {
  .dashboard {
    display: grid;
    align-items: start;
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
  }

  @media (min-width: 1024px) {
    .dashboard {
      grid-template-columns: minmax(0, 1fr) 320px;
    }
  }

  .dashboard__main,
  .dashboard__rail {
    display: flex;
    min-width: 0;
    flex-direction: column;
    gap: 16px;
  }

  .panel {
    display: flex;
    flex-direction: column;
    gap: 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
    padding: 16px;
  }

  .panel__header {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: 12px;
  }

  .panel__title {
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 600;
    line-height: 1.4;
  }

  .panel__more {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    color: var(--text-muted);
    font-size: 0.8125rem;
    text-decoration: none;
    transition: color 150ms var(--ease);
  }

  .panel__more .icon {
    width: 14px;
    height: 14px;
    font-size: 14px;
  }

  @media (hover: hover) {
    .panel__more:hover {
      color: var(--text-primary);
    }
  }

  /* An empty panel says so in the same voice as a full one, rather than being
     padded out with a placeholder that pretends there is something there. */
  .panel__empty {
    color: var(--text-dim);
    font-size: 0.875rem;
    line-height: 1.5;
  }

  .panel__empty a {
    color: var(--text-muted);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  /* ── Issue feed ─────────────────────────────────────────────────────────── */

  .issue-feed {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .issue-feed__row {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    padding-block: 10px;
  }

  .issue-feed__row + .issue-feed__row {
    border-top: 1px solid var(--line-faint);
  }

  .issue-feed__state {
    width: 16px;
    height: 16px;
    flex: none;
    margin-top: 2px;
    color: var(--success);
    font-size: 16px;
  }

  .issue-feed__body {
    display: flex;
    min-width: 0;
    flex: 1;
    flex-direction: column;
    gap: 2px;
  }

  .issue-feed__title {
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.4;
    text-decoration: none;
  }

  @media (hover: hover) {
    .issue-feed__title:hover {
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }

  .issue-feed__meta {
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  .issue-feed__comments {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 4px;
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  .issue-feed__comments .icon {
    width: 14px;
    height: 14px;
    font-size: 14px;
  }

  /* ── Project list ───────────────────────────────────────────────────────── */

  .project-list {
    display: flex;
    flex-direction: column;
    gap: 2px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .project-list__row {
    display: flex;
    align-items: center;
    gap: 8px;
    min-height: 34px;
    margin-inline: -8px;
    padding-inline: 8px;
    border-radius: 6px;
    color: var(--text-muted);
    font-size: 0.875rem;
    text-decoration: none;
    transition: background 150ms var(--ease), color 150ms var(--ease);
  }

  .project-list__row .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  @media (hover: hover) {
    .project-list__row:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .project-list__title {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* A cross-repository list is unreadable without the repository, so the row
     carries it between the title and the state. */
  .project-list__repository {
    min-width: 0;
    flex: none;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.75rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .project-list__state {
    flex: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 8px;
    color: var(--text-dim);
    font-size: 0.6875rem;
    text-transform: lowercase;
  }

  /* ── Rail ───────────────────────────────────────────────────────────────── */

  .repo-card {
    display: flex;
    align-items: center;
    gap: 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md, 8px);
    padding: 10px 12px;
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
    transition: border-color 150ms var(--ease);
  }

  .repo-card .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .repo-card__name {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  @media (hover: hover) {
    .repo-card:hover {
      border-color: var(--line-strong);
    }
  }

  /* Two, because the pair now sits inside the panel whose rows it counts --
     open and closed of that one thing. It used to be three (open, closed,
     projects) in the rail, where a number with no panel attached read as a
     total over the whole account. */
  .stat-pairs {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    margin: 0;
  }

  .stat-pairs dt {
    color: var(--text-dim);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .stat-pairs dd {
    margin: 0;
    color: var(--text-primary);
    font-size: 1.25rem;
    font-variant-numeric: tabular-nums;
    font-weight: 500;
    line-height: 1.2;
  }

  .changelog-rail {
    display: flex;
    flex-direction: column;
    gap: 12px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .changelog-rail__when {
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  .changelog-rail__summary {
    color: var(--text-muted);
    font-size: 0.8125rem;
    line-height: 1.45;
    text-wrap: pretty;
  }

  /* One row per topic, so the rows are a list of conversations rather than a
     transcript of the busiest one. Same rhythm as the issue feed, at rail
     size: a title that leads, and a line under it saying who and when. */
  .post-rail {
    display: flex;
    flex-direction: column;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .post-rail__row {
    display: flex;
    min-width: 0;
    flex-direction: column;
    gap: 2px;
    padding-block: 8px;
  }

  .post-rail__row + .post-rail__row {
    border-top: 1px solid var(--line-faint);
  }

  .post-rail__title {
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.4;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  @media (hover: hover) {
    .post-rail__title:hover {
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }

  .post-rail__meta {
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}

/* ── Operator ─────────────────────────────────────────────────────────────── */

/* A table, because the question this surface answers is comparative: who
 * signed up, when, and how much have they done. Cards would put each account
 * in its own box and make the columns impossible to scan down. */

@layer components {
  .admin-count {
    color: var(--text-dim);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
  }

  /* The table scrolls inside its own container rather than widening the page:
     a horizontal scrollbar on the document is a layout failure, on a table it
     is the design. */
  .admin-table-scroll {
    overflow-x: auto;
    margin-inline: -16px;
    padding-inline: 16px;
  }

  .admin-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
  }

  .admin-table th {
    padding: 8px 12px 8px 0;
    border-bottom: 1px solid var(--line);
    color: var(--text-dim);
    font-size: 0.75rem;
    font-weight: 500;
    letter-spacing: 0.04em;
    text-align: start;
    text-transform: uppercase;
    white-space: nowrap;
  }

  .admin-table td {
    padding: 10px 12px 10px 0;
    border-bottom: 1px solid var(--line-faint);
    color: var(--text-muted);
    vertical-align: middle;
    white-space: nowrap;
  }

  .admin-table tr:last-child td {
    border-bottom: 0;
  }

  /* Counts line up on their digits so a column can be compared by eye. */
  .admin-table__number {
    text-align: end;
    font-variant-numeric: tabular-nums;
  }

  .admin-identity {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .admin-identity__names {
    display: flex;
    min-width: 0;
    flex-direction: column;
  }

  .admin-identity__names strong {
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    line-height: 1.25rem;
  }

  .admin-identity__names span {
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1rem;
  }

  .admin-paging {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding-block-start: 4px;
  }

  .admin-paging__position {
    color: var(--text-dim);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
  }
}

/* ── Diff ─────────────────────────────────────────────────────────────────── */

/* One file's diff. Adapted from Pierre's `FileDiff` (@pierre/diffs, Apache
 * 2.0, pierrecomputer/pierre); the layout decisions carried over, the values
 * are ours. See docs/2026-08-20-pierre-code-surfaces-port.md.
 *
 * A table, not a stack of flex rows: the gutters must align down the whole
 * file regardless of how many digits a line number has, and that is what a
 * table does without measuring anything. */

@layer components {
  .diff-file {
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
  }

  .diff-file + .diff-file {
    margin-block-start: 12px;
  }

  .diff-file__header {
    display: flex;
    align-items: center;
    gap: 8px;
    cursor: pointer;
    list-style: none;
    padding: 10px 12px;
    background: var(--ink-raised);
    font-size: 0.8125rem;
  }

  .diff-file__header::-webkit-details-marker {
    display: none;
  }

  .diff-file__header:focus-visible {
    outline: 2px solid var(--ring);
    outline-offset: -2px;
  }

  .diff-file__caret {
    width: 14px;
    height: 14px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 14px;
    transition: transform var(--motion-fast, 150ms) var(--ease);
  }

  .diff-file[open] .diff-file__caret {
    transform: rotate(90deg);
  }

  .diff-file__path {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    color: var(--text-primary);
    font-family: var(--font-mono, ui-monospace, monospace);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .diff-file__from {
    color: var(--text-dim);
  }

  /* The status is a word, not a colour: "renamed" and "modified" are not
     distinguishable as tints, and a reader should not have to learn a legend. */
  .diff-file__status {
    flex: none;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 1px 8px;
    color: var(--text-dim);
    font-size: 0.6875rem;
    text-transform: lowercase;
  }

  .diff-file__count {
    flex: none;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  .diff-file__count[data-kind="insert"] { color: var(--success); }
  .diff-file__count[data-kind="delete"] { color: var(--danger); }

  .diff-file__note {
    padding: 12px;
    color: var(--text-dim);
    font-size: 0.8125rem;
  }

  /* ── Hunks ──────────────────────────────────────────────────────────────── */

  .diff-hunk + .diff-hunk {
    border-top: 1px solid var(--line);
  }

  .diff-hunk__header {
    display: flex;
    align-items: baseline;
    gap: 12px;
    border-block: 1px solid var(--line-faint);
    background: var(--wash-hover);
    padding: 4px 12px;
    font-size: 0.75rem;
  }

  .diff-hunk__range {
    flex: none;
    color: var(--text-dim);
    font-family: var(--font-mono, ui-monospace, monospace);
  }

  /* Git puts the enclosing function here, which is the cheapest orientation a
     reader gets in a long file. */
  .diff-hunk__heading {
    min-width: 0;
    overflow: hidden;
    color: var(--text-muted);
    font-family: var(--font-mono, ui-monospace, monospace);
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  /* ── Lines ──────────────────────────────────────────────────────────────── */

  .diff-lines {
    width: 100%;
    border-collapse: collapse;
    font-family: var(--font-mono, ui-monospace, monospace);
    font-size: 0.8125rem;
    line-height: 1.5;
  }

  .diff-line td {
    padding: 0;
    vertical-align: top;
  }

  /* Tints are held well back. A diff is read for its text, and a saturated row
     fights the code sitting on it; the marker column carries the same
     information without colour. */
  .diff-line[data-kind="insert"] {
    background: color-mix(in oklab, var(--success) 12%, transparent);
  }

  .diff-line[data-kind="delete"] {
    background: color-mix(in oklab, var(--danger) 12%, transparent);
  }

  .diff-line[data-kind="meta"] {
    background: var(--wash-hover);
    color: var(--text-dim);
  }

  /* The addressed line, after following an anchor. */
  .diff-line:target {
    background: color-mix(in oklab, var(--info) 22%, transparent);
  }

  .diff-line__number {
    width: 1%;
    min-width: 44px;
    padding-inline: 8px !important;
    border-right: 1px solid var(--line-faint);
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    text-align: end;
    user-select: none;
    white-space: nowrap;
  }

  .diff-line__number a {
    color: inherit;
    text-decoration: none;
  }

  @media (hover: hover) {
    .diff-line__number a:hover {
      color: var(--text-primary);
      text-decoration: underline;
    }
  }

  .diff-line__marker {
    width: 1%;
    padding-inline: 6px !important;
    color: var(--text-dim);
    text-align: center;
    user-select: none;
  }

  .diff-line[data-kind="insert"] .diff-line__marker { color: var(--success); }
  .diff-line[data-kind="delete"] .diff-line__marker { color: var(--danger); }

  /* The code scrolls inside the cell rather than widening the page: a long
     line is normal in a diff, and a horizontal scrollbar on the document is
     not. `pre` keeps the leading whitespace that indentation depends on. */
  .diff-line__text {
    width: 100%;
    overflow-x: auto;
    padding-inline: 10px !important;
  }

  .diff-line__text pre {
    margin: 0;
    color: var(--text-body);
    font: inherit;
    white-space: pre;
  }

  @media (prefers-reduced-motion: reduce) {
    .diff-file__caret {
      transition: none;
    }
  }
}

@layer components {
  /* The count under a diff describes the diff above it, including when the
     input was truncated -- it is derived from the parsed lines, not from git's
     own summary of the whole commit. */
  .diff-totals {
    display: flex;
    align-items: baseline;
    gap: 8px;
    padding-block: 8px;
    color: var(--text-muted);
    font-size: 0.8125rem;
  }

  .diff-totals [data-kind="insert"] {
    color: var(--success);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-variant-numeric: tabular-nums;
  }

  .diff-totals [data-kind="delete"] {
    color: var(--danger);
    font-family: var(--font-mono, ui-monospace, monospace);
    font-variant-numeric: tabular-nums;
  }
}

@layer components {
  /* Two edges of one line. The copyright says who owns this and the note says
     how it is licensed; centring them together reads as one run-on sentence. */
  .landing-footer__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    justify-content: space-between;
    gap: 8px 24px;
    max-width: 1104px;
    margin-inline: auto;
    padding-block-start: 32px;
  }

  .landing-footer__bar .landing-footer__note {
    padding-block-start: 0;
    margin: 0;
  }
}

/* ── Repository view ──────────────────────────────────────────────────────── */

/* The GitHub-shaped repo home: a ref bar, the latest commit, the tree, and a
 * rail describing the repository. Adapted from the clones catalogued in
 * docs/audits/2026-08-19-github-clone-harvest-candidates.md -- gh-next (MIT)
 * for the Tailwind repo home, Gitea's view_list.tmpl for what a tree row
 * carries. Values are ours. */

@layer components {
  .repo-view {
    display: grid;
    align-items: start;
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
  }

  @media (min-width: 1024px) {
    .repo-view {
      grid-template-columns: minmax(0, 1fr) 296px;
    }
  }

  /* A grid child will not shrink below its content unless told to, and a long
     unbroken commit message inside the tree is exactly that content. */
  .repo-view__main,
  .repo-view__rail {
    min-width: 0;
  }

  /* ── Repository page ────────────────────────────────────────────────────── */

  .repo-page {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  .repo-page__identity {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 12px;
  }

  /* The trail is the page title here, so it is set at title size rather than
     at the small size a trail carries when it sits above one. */
  .repo-page__trail .breadcrumb__link,
  .repo-page__trail .breadcrumb__current,
  .repo-page__trail .breadcrumb__separator {
    font-size: 1.25rem;
  }

  .repo-page__trail .breadcrumb__current {
    color: var(--text-primary);
    font-weight: 600;
  }

  /* ── Section bar ────────────────────────────────────────────────────────── */

  /* Links that change the URL, so the selected one is marked by aria-current
     and the underline follows that attribute rather than a second class. */
  .repo-tabs {
    display: flex;
    gap: 4px;
    overflow-x: auto;
    border-block-end: 1px solid var(--line);
  }

  .repo-tabs__tab {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    padding: 8px 12px;
    border-block-end: 2px solid transparent;
    color: var(--text-muted);
    font-size: 0.875rem;
    text-decoration: none;
    white-space: nowrap;
    transition:
      color var(--motion-fast) var(--ease),
      border-color var(--motion-fast) var(--ease);
  }

  .repo-tabs__tab .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  @media (hover: hover) {
    .repo-tabs__tab:hover {
      border-color: var(--line-strong);
      color: var(--text-primary);
    }
  }

  .repo-tabs__tab[aria-current="page"] {
    border-color: var(--accent-bright);
    color: var(--text-primary);
    font-weight: 600;
  }

  .repo-tabs__tab[aria-current="page"] .icon {
    color: var(--icon-primary);
  }

  .repo-tabs__count {
    border-radius: 999px;
    background: var(--wash-selected);
    padding: 1px 7px;
    color: var(--text-muted);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    font-weight: 500;
  }

  /* ── Stack map ──────────────────────────────────────────────────────────── */

  /* One pull request stack as an ordered rail: layers top-first, the trunk at
     the bottom, and a hairline connector threading through the state glyphs so
     the chain reads as one object. The current layer is washed rather than
     underlined -- the reader is inside it, not travelling to it. */
  .stack-map {
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-surface);
  }

  .stack-map__header {
    display: flex;
    align-items: center;
    gap: 10px;
    border-block-end: 1px solid var(--line);
    padding: 12px 16px;
  }

  .stack-map__title {
    color: var(--text-primary);
    font-weight: 600;
    font-size: 0.9375rem;
  }

  .stack-map__count {
    color: var(--text-muted);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  .stack-map__actions {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    margin-inline-start: auto;
  }

  .stack-map__layers {
    position: relative;
    margin: 0;
    padding: 8px 0;
    list-style: none;
  }

  /* The connector rail. It threads behind the state glyphs, which sit on the
     surface colour so the line appears to pass through them. */
  .stack-map__layers::before {
    position: absolute;
    inset-block: 14px;
    inset-inline-start: 23px;
    border-inline-start: 1px solid var(--line-strong);
    content: "";
  }

  .stack-map__add,
  .stack-map__trunk {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 8px 16px;
  }

  .stack-map__add-link {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    color: var(--text-muted);
    font-size: 0.875rem;
    text-decoration: none;
    transition: color var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .stack-map__add-link:hover {
      color: var(--text-primary);
    }
  }

  .stack-map__add-icon {
    z-index: 1;
    box-sizing: content-box;
    width: 16px;
    height: 16px;
    flex: none;
    padding: 0;
    background: var(--ink-surface);
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .stack-map__layer {
    position: relative;
    display: grid;
    grid-template-columns: 16px 1fr;
    align-items: center;
    gap: 4px 10px;
    padding: 8px 16px;
  }

  .stack-map__layer[aria-current="page"] {
    background: var(--wash-selected);
  }

  .stack-map__layer[aria-current="page"]::before {
    position: absolute;
    inset-block: 0;
    inset-inline-start: 0;
    border-inline-start: 2px solid var(--accent-bright);
    content: "";
  }

  .stack-map__state {
    z-index: 1;
    grid-row: span 2;
    align-self: start;
    box-sizing: content-box;
    width: 16px;
    height: 16px;
    flex: none;
    padding-block: 2px;
    background: var(--ink-surface);
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .stack-map__layer[aria-current="page"] .stack-map__state {
    background: transparent;
  }

  .stack-map__layer[data-state="open"] .stack-map__state {
    color: var(--success);
  }

  .stack-map__layer[data-state="merged"] .stack-map__state {
    color: var(--accent-bright);
  }

  .stack-map__layer[data-state="closed"] .stack-map__state {
    color: var(--danger);
  }

  .stack-map__layer-link {
    color: var(--text-primary);
    font-weight: 600;
    font-size: 0.875rem;
    text-decoration: none;
  }

  @media (hover: hover) {
    a.stack-map__layer-link:hover {
      color: var(--accent-bright);
    }
  }

  .stack-map__layer-meta {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    grid-column: 2;
    min-width: 0;
  }

  .stack-map__layer-number {
    color: var(--text-muted);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
  }

  .stack-map__branch {
    overflow: hidden;
    border-radius: 6px;
    background: var(--wash-selected);
    padding: 2px 8px;
    color: var(--text-body);
    font-family: var(--font-mono);
    font-size: 0.75rem;
    text-decoration: none;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  @media (hover: hover) {
    a.stack-map__branch:hover {
      color: var(--accent-bright);
    }
  }

  .stack-map__trunk .stack-map__state {
    grid-row: auto;
    padding-block: 0;
  }

  /* ── File table ─────────────────────────────────────────────────────────── */

  .file-table {
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
  }

  .file-table__bar {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 16px;
    padding: 12px;
    background: var(--ink-surface);
    font-size: 0.8125rem;
  }

  .file-table__ref {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    border: 1px solid var(--line-strong);
    border-radius: 6px;
    padding: 4px 10px;
    color: var(--text-primary);
    font-weight: 500;
  }

  .file-table__count {
    color: var(--text-muted);
  }

  .file-table__count strong {
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
  }

  .file-table__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    margin-inline-start: auto;
  }

  /* Above the table, not inside it. This row describes the whole tree, and a
     row that describes the table while looking like a row in it is the part of
     GitHub's own version that reads oddly at first glance. */
  .file-table__commit {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px 12px;
    border-block: 1px solid var(--line);
    background: var(--ink-raised);
    padding: 10px 12px;
    color: var(--text-muted);
    font-size: 0.8125rem;
  }

  /* How many commits are behind the one named to its left. It belongs at the
     far edge because it is the only thing in this row that is not about that
     single commit. */
  .file-table__commits {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    margin-inline-start: auto;
    color: var(--text-muted);
  }

  .file-table__commits .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .file-table__commits strong {
    color: var(--text-primary);
    font-variant-numeric: tabular-nums;
  }

  .file-table__list {
    width: 100%;
    border-collapse: collapse;
    font-size: 0.875rem;
  }

  .file-row + .file-row {
    border-top: 1px solid var(--line-faint);
  }

  .file-row td {
    padding: 8px 12px;
  }

  .file-row__link {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    color: var(--text-body);
    text-decoration: none;
  }

  /* A folder leads, so it is tinted; a file is the default and is not. Tinting
     both would mean tinting nothing. */
  .file-row[data-kind="tree"] .file-row__link .icon {
    color: var(--info);
  }

  .file-row .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  @media (hover: hover) {
    .file-row__link:hover {
      color: var(--text-primary);
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }

  /* The one column that gives up its width. `width: 100%` claims the slack a
     table would otherwise hand to the name, and `max-width: 0` lets the cell
     shrink back under it so a long subject truncates instead of widening the
     table past its container. */
  .file-row__message {
    width: 100%;
    max-width: 0;
    overflow: hidden;
    color: var(--text-muted);
    font-size: 0.8125rem;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .file-row__size {
    width: 1%;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    text-align: end;
    white-space: nowrap;
  }

  .file-row__age {
    width: 1%;
    color: var(--text-dim);
    font-size: 0.75rem;
    text-align: end;
    white-space: nowrap;
  }

  .file-table__empty {
    padding: 16px 12px;
    color: var(--text-dim);
    font-size: 0.875rem;
  }

  /* ── About rail ─────────────────────────────────────────────────────────── */

  .repo-about {
    display: flex;
    flex-direction: column;
    gap: 16px;
  }

  .repo-about__title {
    color: var(--text-primary);
    font-size: 1rem;
    font-weight: 600;
  }

  .repo-about__description {
    color: var(--text-muted);
    font-size: 0.9375rem;
    line-height: 1.5;
    text-wrap: pretty;
  }

  .repo-about__links,
  .repo-about__stats,
  .repo-about__language-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .repo-about__links li,
  .repo-about__stats li,
  .repo-about__language-list li {
    display: flex;
    align-items: center;
    gap: 8px;
    color: var(--text-muted);
    font-size: 0.875rem;
  }

  .repo-about__links a {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    color: var(--text-muted);
    text-decoration: none;
  }

  @media (hover: hover) {
    .repo-about__links a:hover {
      color: var(--text-primary);
    }
  }

  .repo-about .icon {
    width: 16px;
    height: 16px;
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .repo-about__languages h3,
  .repo-about__contributors h3 {
    padding-block-end: 8px;
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 600;
  }

  .repo-about__count {
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
    font-weight: 400;
  }

  .repo-import-provenance {
    display: flex;
    flex-direction: column;
    gap: 12px;
    padding-block-start: 18px;
    border-block-start: 1px solid var(--line);
  }

  .repo-import-provenance h2 {
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 600;
  }

  .repo-import-provenance dl {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin: 0;
  }

  .repo-import-provenance dl > div {
    display: grid;
    grid-template-columns: minmax(76px, auto) minmax(0, 1fr);
    gap: 10px;
    color: var(--text-muted);
    font-size: 0.75rem;
  }

  .repo-import-provenance dt {
    color: var(--text-dim);
  }

  .repo-import-provenance dd {
    min-width: 0;
    overflow-wrap: anywhere;
  }

  .repo-import-provenance p {
    margin: 0;
    color: var(--text-dim);
    font-size: 0.75rem;
    line-height: 1.5;
  }

  /* A wrapped cluster rather than a column: the group reads as one fact about
     the repository, not as a list to be gone through name by name. Each face
     still carries its own accessible name. */
  .contributor-cluster {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    margin: 0;
    padding: 0;
    list-style: none;
  }

  .contributor-cluster > li {
    display: inline-flex;
  }

  .contributor-cluster__count {
    color: var(--text-muted);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  /* A hairline in the page's own fill, so adjacent faces of similar tone still
     read as separate people. */
  .contributor-cluster .avatar {
    border: 1px solid var(--ink-void);
  }

  /* Wide enough to type a path into and no wider: the ref bar is a row of small
     controls, and a search field that fills it reads as the subject of the bar
     rather than one control in it. */
  .file-table__search {
    width: 100%;
    max-width: 220px;
  }

  /* One bar rather than a stack: the proportions are the point, and they only
     read as proportions when the segments share a length. */
  .language-bar {
    display: flex;
    height: 8px;
    overflow: hidden;
    border-radius: 999px;
    background: var(--line);
  }

  .language-bar__segment {
    height: 100%;
  }

  .language-dot {
    width: 10px;
    height: 10px;
    flex: none;
    border-radius: 50%;
  }

  /* A fixed rotation rather than a hash of the name: six steps off the ink
     ladder, so the chart stays in the palette and two adjacent languages are
     always distinguishable. */
  .language-bar__segment[data-index="0"], .language-dot[data-index="0"] { background: var(--text-primary); }
  .language-bar__segment[data-index="1"], .language-dot[data-index="1"] { background: var(--info); }
  .language-bar__segment[data-index="2"], .language-dot[data-index="2"] { background: var(--success); }
  .language-bar__segment[data-index="3"], .language-dot[data-index="3"] { background: var(--warning); }
  .language-bar__segment[data-index="4"], .language-dot[data-index="4"] { background: var(--text-muted); }
  .language-bar__segment[data-index="5"], .language-dot[data-index="5"] { background: var(--text-dim); }

  .repo-about__percent {
    color: var(--text-dim);
    font-variant-numeric: tabular-nums;
  }
}

@layer components {
  /* The mark, not the word. The wordmark repeated the page title beside it
     ("OpenAgents | Docs") and the section name is the useful half. */
  .sidebar-brand__mark img {
    display: block;
    width: 24px;
    height: 24px;
    border-radius: 5px;
  }
}

/* ── Issues ───────────────────────────────────────────────────────────────── */

/* Issue, project, and team surfaces, adapted from Circle (MIT, © 2025
 * lndev-ui). The source assigns a hand-picked hex value to each of thirteen
 * statuses and eleven labels; those colours are Linear's, and adopting them
 * would put a second palette beside the one every other surface uses. Colour
 * here is assigned per status CATEGORY off the same token ladder as
 * .status-indicator: activity is --info, completion is --success, anything
 * awaiting a decision is --warning, and both resting states are grey.
 * See docs/2026-08-20-circle-ui-port.md. */

@layer components {
  /* One custom property carries the category colour, so every element that
     tints itself from a status -- the glyph, the group header wash -- reads
     the same value instead of repeating the mapping. */
  .issue-status,
  .issue-group {
    --issue-tint: var(--text-muted);
  }

  .issue-status[data-category="triage"],
  .issue-group[data-category="triage"] {
    --issue-tint: var(--warning);
  }

  .issue-status[data-category="backlog"],
  .issue-group[data-category="backlog"],
  .issue-status[data-category="canceled"],
  .issue-group[data-category="canceled"] {
    --issue-tint: var(--text-dim);
  }

  .issue-status[data-category="started"],
  .issue-group[data-category="started"] {
    --issue-tint: var(--info);
  }

  .issue-status[data-category="open"],
  .issue-group[data-category="open"] {
    --issue-tint: var(--success);
  }

  .issue-status[data-category="completed"],
  .issue-group[data-category="completed"] {
    --issue-tint: var(--done);
  }

  .issue-status {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    color: var(--issue-tint);
  }

  .issue-status__glyph {
    width: 14px;
    height: 14px;
    font-size: 14px;
  }

  /* The one shape an icon set cannot carry: the fill is a number. A conic
     gradient inside a ring is the whole drawing -- no SVG, no second glyph per
     fraction, and it stays correct at any percentage the caller has. */
  .issue-status__arc {
    position: relative;
    width: 14px;
    height: 14px;
    flex: none;
    border: 1.5px solid currentColor;
    border-radius: 50%;
  }

  .issue-status__arc::after {
    content: "";
    position: absolute;
    inset: 1.5px;
    border-radius: 50%;
    background: conic-gradient(currentColor calc(var(--issue-arc, 0) * 1%), transparent 0);
  }

  .issue-status__label {
    color: var(--text-body);
    font-size: 0.8125rem;
  }

  .issue-priority {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    color: var(--icon-tertiary);
  }

  .issue-priority[data-level="urgent"] {
    color: var(--danger);
  }

  .issue-priority__alarm {
    width: 14px;
    height: 14px;
    font-size: 14px;
  }

  /* Bars rather than five pictures: one shape read at four levels, so the
     ordering is visible without learning a legend. The unlit bars stay drawn
     -- removing them would make "low" and "no priority" the same silhouette. */
  .issue-priority__bars {
    display: inline-flex;
    align-items: flex-end;
    gap: 1.5px;
    width: 14px;
    height: 14px;
    padding-block: 1px;
  }

  .issue-priority__bar {
    width: 3px;
    border-radius: 1px;
    background: currentColor;
    opacity: 0.28;
  }

  .issue-priority__bar:nth-child(1) {
    height: 50%;
  }

  .issue-priority__bar:nth-child(2) {
    height: 75%;
  }

  .issue-priority__bar:nth-child(3) {
    height: 100%;
  }

  /* No priority is three equal dashes at the midline: a shape of its own,
     rather than the low end of the same ramp, because "nobody has decided" is
     not a degree of urgency. */
  .issue-priority[data-level="none"] .issue-priority__bar {
    height: 2px;
    opacity: 0.5;
  }

  .issue-priority[data-level="low"] .issue-priority__bar:nth-child(1),
  .issue-priority[data-level="medium"] .issue-priority__bar:nth-child(-n + 2),
  .issue-priority[data-level="high"] .issue-priority__bar {
    opacity: 1;
  }

  .issue-priority__label {
    color: var(--text-body);
    font-size: 0.8125rem;
  }

  .issue-label {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    flex: none;
    max-width: 14rem;
    padding: 1px 8px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.75rem;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .issue-label__dot {
    width: 6px;
    height: 6px;
    flex: none;
    border-radius: 50%;
    background: var(--text-dim);
  }

  .issue-label__glyph {
    width: 12px;
    height: 12px;
    flex: none;
    font-size: 12px;
    color: var(--icon-tertiary);
  }

  .issue-label[data-tone="primary"] .issue-label__dot { background: var(--text-primary); }
  .issue-label[data-tone="info"] .issue-label__dot { background: var(--info); }
  .issue-label[data-tone="success"] .issue-label__dot { background: var(--success); }
  .issue-label[data-tone="warning"] .issue-label__dot { background: var(--warning); }
  .issue-label[data-tone="danger"] .issue-label__dot { background: var(--danger); }

  .assignee {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: none;
  }

  .assignee__figure {
    position: relative;
    display: inline-flex;
  }

  /* Unassigned is drawn, not left blank. A gap in a column of faces reads as a
     failure to render rather than as an unclaimed issue. */
  .assignee__empty {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    border: 1px dashed var(--line-strong);
    border-radius: 50%;
    color: var(--icon-faint);
    font-size: 13px;
  }

  .assignee[data-size="sm"] .assignee__empty {
    width: 20px;
    height: 20px;
    font-size: 11px;
  }

  .assignee[data-size="lg"] .assignee__empty {
    width: 32px;
    height: 32px;
    font-size: 16px;
  }

  .assignee__presence {
    position: absolute;
    right: -1px;
    bottom: -1px;
    width: 9px;
    height: 9px;
    border: 2px solid var(--ink-surface);
    border-radius: 50%;
    background: var(--text-dim);
  }

  .assignee__presence[data-presence="online"] { background: var(--success); }
  .assignee__presence[data-presence="away"] { background: var(--warning); }

  .assignee__name {
    color: var(--text-body);
    font-size: 0.8125rem;
  }

  /* Faces overlap until hovered, then separate. Overlapping is how a stack
     says "a group" rather than "these five people"; separating on hover is how
     the individuals stay reachable. */
  .assignee-stack {
    display: inline-flex;
    align-items: center;
    gap: 6px;
  }

  .assignee-stack__faces {
    display: inline-flex;
  }

  .assignee-stack__faces .avatar {
    margin-inline-start: -6px;
    box-shadow: 0 0 0 2px var(--ink-surface);
    transition: margin var(--motion-fast) var(--ease);
  }

  .assignee-stack__faces .avatar:first-child {
    margin-inline-start: 0;
  }

  @media (hover: hover) {
    .assignee-stack:hover .assignee-stack__faces .avatar {
      margin-inline-start: 2px;
    }

    .assignee-stack:hover .assignee-stack__faces .avatar:first-child {
      margin-inline-start: 0;
    }
  }

  .assignee-stack__count {
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  /* ── rows ──────────────────────────────────────────────────────────────── */

  .issue-row {
    display: flex;
    align-items: center;
    gap: 10px;
    min-height: 44px;
    padding-inline: 16px;
    border-block-end: 1px solid var(--line-faint);
  }

  @media (hover: hover) {
    .issue-row:hover {
      background: var(--wash-hover);
    }
  }

  .issue-row[data-selected="true"] {
    background: var(--wash-selected);
  }

  /* The three fields a person scans a list for, in a fixed order and at a
     fixed width, so they form a column down the list rather than shifting with
     each title's length. */
  .issue-row__scan {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: none;
  }

  .issue-row__identifier {
    width: 68px;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  /* Only a cross-repository list draws this. There, which repository a row is
     in is as load-bearing as its number, so it takes a fixed width inside the
     scan column and forms a column of its own rather than sitting at the
     trailing edge, which drops. The full path is on the element's title, for
     the long name that clips. */
  .issue-row__repository {
    width: 6.5rem;
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.75rem;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .issue-row__title {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-decoration: none;
  }

  @media (hover: hover) {
    a.issue-row__title:hover {
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }

  .issue-row__trailing {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    flex: none;
    margin-inline-start: auto;
  }

  .issue-row__chips {
    display: none;
    align-items: center;
    gap: 6px;
  }

  .issue-row__due {
    color: var(--warning);
    font-size: 0.75rem;
  }

  .issue-row__date {
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  /* Discretionary fields drop from the least load-bearing inwards. The scan
     column and the title never drop: a row that cannot be identified is not a
     narrower row, it is a broken one. */
  @media (width >= 40rem) {
    .issue-row {
      gap: 12px;
      padding-inline: 24px;
    }

    .issue-row__chips {
      display: inline-flex;
    }

    .issue-row__repository {
      width: 10rem;
    }
  }

  .issue-card {
    display: flex;
    flex-direction: column;
    gap: 10px;
    padding: 12px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--ink-raised);
    box-shadow: var(--shadow-l1);
  }

  .issue-card__head,
  .issue-card__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }

  .issue-card__scan {
    display: inline-flex;
    align-items: center;
    gap: 8px;
  }

  .issue-card__title {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.875rem;
    font-weight: 500;
    text-decoration: none;
  }

  .issue-card__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
  }

  /* ── groups and boards ─────────────────────────────────────────────────── */

  .issue-group__head {
    position: sticky;
    top: 0;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    height: 40px;
    padding-inline: 24px;
    border-block-end: 1px solid var(--line-faint);
    /* The wash is mixed over the canvas rather than laid on it with alpha, so
       rows scrolling underneath a sticky header are covered instead of showing
       through it. */
    background: color-mix(in oklab, var(--issue-tint) 7%, var(--ink-surface));
  }

  .issue-group__name {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }

  .issue-group__label {
    overflow: hidden;
    color: var(--text-primary);
    font-size: 0.8125rem;
    font-weight: 600;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .issue-group__count {
    color: var(--text-dim);
    font-size: 0.8125rem;
    font-variant-numeric: tabular-nums;
  }

  .issue-group__actions {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
  }

  .issue-group[data-layout="board"] {
    display: flex;
    flex-direction: column;
    width: 320px;
    max-height: 100%;
    flex: none;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-lg);
    background: var(--ink-void);
  }

  .issue-group[data-layout="board"] .issue-group__head {
    padding-inline: 12px;
  }

  .issue-group[data-layout="board"] .issue-group__body {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 8px;
    overflow-y: auto;
  }

  /* Each column scrolls on its own. Scrolling the board as one surface pushes
     every header off the top to read the bottom of one column. */
  .issue-board {
    display: flex;
    gap: 12px;
    align-items: stretch;
    overflow-x: auto;
    padding-block-end: 4px;
  }

  /* ── filters ───────────────────────────────────────────────────────────── */

  .filter-bar {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 24px;
    border-block-end: 1px solid var(--line);
  }

  .filter-bar__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 8px;
  }

  .filter-bar__clear {
    flex: none;
    padding: 4px 8px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-muted);
    font-size: 0.75rem;
    cursor: pointer;
    transition: color var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .filter-bar__clear:hover {
      color: var(--text-primary);
    }
  }

  /* Three segments divided by hairlines rather than three separate controls:
     the chip is one filter, and splitting it visually is what says the
     operator can change without the filter being rebuilt. */
  .filter-chip {
    display: inline-flex;
    align-items: center;
    height: 28px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: 999px;
    background: var(--ink-raised);
    font-size: 0.75rem;
  }

  .filter-chip > * {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 100%;
    padding-inline: 10px;
  }

  .filter-chip > * + * {
    border-inline-start: 1px solid var(--line);
  }

  .filter-chip__subject {
    color: var(--text-body);
    font-weight: 500;
  }

  .filter-chip__operator {
    color: var(--text-dim);
  }

  .filter-chip__value {
    color: var(--text-primary);
  }

  .filter-chip__remove {
    border: 0;
    background: transparent;
    color: var(--icon-tertiary);
    cursor: pointer;
    transition:
      color var(--motion-fast) var(--ease),
      background var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .filter-chip__remove:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  /* ── headers ───────────────────────────────────────────────────────────── */

  .view-tabs {
    display: inline-flex;
    align-items: center;
    gap: 4px;
  }

  .view-tabs__tab {
    display: inline-flex;
    align-items: center;
    height: 28px;
    padding-inline: 10px;
    border: 1px solid transparent;
    border-radius: 999px;
    color: var(--text-muted);
    font-size: 0.75rem;
    font-weight: 500;
    text-decoration: none;
    transition:
      color var(--motion-fast) var(--ease),
      background var(--motion-fast) var(--ease),
      border-color var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .view-tabs__tab:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .view-tabs__tab[aria-current="page"] {
    border-color: var(--line);
    background: var(--wash-selected);
    color: var(--text-primary);
  }

  .issue-toolbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    min-height: 40px;
    padding: 4px 24px;
    border-block-end: 1px solid var(--line);
  }

  .issue-toolbar__leading,
  .issue-toolbar__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    min-width: 0;
  }

  /* ── command palette ───────────────────────────────────────────────────── */

  /* A native dialog: the browser owns the focus trap, the backdrop, Escape,
     and inertness of the page behind. Reimplementing those is where hand-built
     palettes usually go wrong. */
  .command-palette {
    width: min(640px, calc(100vw - 32px));
    max-width: none;
    /* A modal dialog is centred by the user agent's `margin: auto`, and the
       reset zeroes margin on every element -- so setting only the block-start
       left the inline margins at 0 and the palette opened against the left
       edge. Both axes are stated: `auto` inline to centre, 12vh above so it
       sits under the eye's resting point rather than in the middle. This is
       the same trap `.menu` hit with `margin: 0`. */
    margin: 12vh auto auto;
    padding: 0;
    border: 1px solid var(--line-strong);
    border-radius: var(--radius-xl);
    background: var(--ink-surface);
    color: var(--text-body);
    box-shadow: var(--shadow-l3);
  }

  .command-palette::backdrop {
    background: var(--scrim);
  }

  .command-palette__panel {
    display: flex;
    flex-direction: column;
    max-height: min(70vh, 32rem);
  }

  .command-palette__context {
    margin: 12px 12px 0;
    padding: 4px 8px;
    overflow: hidden;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    background: var(--wash-hover);
    color: var(--text-muted);
    font-size: 0.75rem;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .command-palette__search {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 14px 16px;
    border-block-end: 1px solid var(--line);
  }

  .command-palette__glyph {
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .command-palette__input {
    width: 100%;
    border: 0;
    background: transparent;
    color: var(--text-primary);
    font-size: 0.9375rem;
    outline: none;
  }

  .command-palette__input::placeholder {
    color: var(--text-dim);
  }

  .command-palette__list {
    padding: 6px;
    overflow-y: auto;
  }

  .command-palette__empty {
    padding: 24px 12px;
    color: var(--text-dim);
    font-size: 0.8125rem;
    text-align: center;
  }

  .command-group + .command-group {
    margin-block-start: 4px;
    padding-block-start: 4px;
    border-block-start: 1px solid var(--line-faint);
  }

  .command-group__heading {
    padding: 6px 10px;
    color: var(--text-dim);
    font-size: 0.6875rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .command-item {
    display: flex;
    align-items: center;
    gap: 10px;
    width: 100%;
    padding: 8px 10px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-body);
    font-size: 0.875rem;
    text-align: start;
    cursor: pointer;
  }

  /* Keyboard selection and pointer hover paint the same row the same way. Two
     different highlights in one list means the reader has to work out which
     one Enter will take. */
  .command-item[data-active],
  .command-item:focus-visible {
    background: var(--wash-selected);
    color: var(--text-primary);
    outline: none;
  }

  @media (hover: hover) {
    .command-item:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .command-item__glyph {
    flex: none;
    color: var(--icon-tertiary);
    font-size: 16px;
  }

  .command-item__label {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .command-item__keys {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    flex: none;
  }

  /* ── project, team, and member rows ────────────────────────────────────── */

  .project-row,
  .team-row,
  .member-row {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 48px;
    padding: 8px 24px;
    border-block-end: 1px solid var(--line-faint);
    font-size: 0.8125rem;
  }

  @media (hover: hover) {
    .project-row:hover,
    .team-row:hover,
    .member-row:hover {
      background: var(--wash-hover);
    }
  }

  .project-row__name,
  .team-row__name {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex: 1;
    min-width: 0;
  }

  .project-row__icon,
  .team-row__glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 24px;
    height: 24px;
    flex: none;
    border-radius: var(--radius-sm);
    background: var(--wash-hover);
    color: var(--icon-secondary);
    font-size: 13px;
  }

  .project-row__link,
  .team-row__link,
  .member-row__name {
    overflow: hidden;
    color: var(--text-primary);
    font-weight: 500;
    white-space: nowrap;
    text-overflow: ellipsis;
    text-decoration: none;
  }

  @media (hover: hover) {
    a.project-row__link:hover,
    a.team-row__link:hover,
    a.member-row__name:hover {
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }

  .team-row__identifier {
    flex: none;
    color: var(--text-dim);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
  }

  /* Health is a word. "At risk" and "off track" are different claims, and no
     reader should have to learn which shade of amber means which. */
  .project-row__health {
    flex: none;
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    color: var(--text-muted);
    font-size: 0.6875rem;
  }

  .project-row__health[data-health="on_track"] {
    border-color: color-mix(in oklab, var(--success) 40%, transparent);
    color: var(--success);
  }

  .project-row__health[data-health="at_risk"] {
    border-color: color-mix(in oklab, var(--warning) 40%, transparent);
    color: var(--warning);
  }

  .project-row__health[data-health="off_track"] {
    border-color: color-mix(in oklab, var(--danger) 40%, transparent);
    color: var(--danger);
  }

  .project-row__priority,
  .project-row__lead,
  .project-row__target,
  .project-row__issues,
  .project-row__status,
  .team-row__membership,
  .team-row__members,
  .team-row__metric,
  .member-row__role,
  .member-row__joined,
  .member-row__teams {
    display: none;
    align-items: center;
    gap: 6px;
    flex: none;
    color: var(--text-muted);
    font-size: 0.75rem;
  }

  .project-row__status,
  .member-row__role {
    display: inline-flex;
  }

  .project-row__percent {
    color: var(--text-body);
    font-variant-numeric: tabular-nums;
  }

  .team-row__joined {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
  }

  .member-row__identity {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    flex: 1;
    min-width: 0;
  }

  .member-row__names {
    display: flex;
    flex-direction: column;
    min-width: 0;
  }

  .member-row__handle {
    overflow: hidden;
    color: var(--text-dim);
    font-size: 0.75rem;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .member-row__role {
    padding: 2px 8px;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
  }

  .member-row__role[data-tone="accent"] {
    border-color: color-mix(in oklab, var(--info) 40%, transparent);
    color: var(--info);
  }

  /* Fixed widths only once the columns exist. Below this the rows are name and
     status, which is what a narrow screen can carry honestly. */
  @media (width >= 48rem) {
    .project-row__priority { display: inline-flex; width: 40px; }
    .project-row__lead { display: inline-flex; width: 40px; }
    .project-row__status { width: 96px; }
    .team-row__members { display: inline-flex; width: 140px; }
    .team-row__metric { display: inline-flex; width: 56px; }
    .member-row__role { width: 96px; }
    .member-row__teams { display: inline-flex; width: 160px; min-width: 0; }
  }

  @media (width >= 64rem) {
    .project-row__health { min-width: 96px; text-align: center; }
    .project-row__target { display: inline-flex; width: 96px; }
    .project-row__issues { display: inline-flex; width: 48px; }
    .team-row__membership { display: inline-flex; width: 96px; }
    .member-row__joined { display: inline-flex; width: 88px; }
  }
}

@layer components {
  /* The lockup is one link, so the mark and the word sit on one baseline with
     the same 10px the divider uses on its right -- the eye should read
     "OpenAgents" as labelling the glyph, not as a separate control. */
  .sidebar-brand__mark {
    display: inline-flex;
    align-items: center;
    gap: 10px;
  }

  .sidebar-brand__wordmark {
    color: var(--text-primary);
    font-size: 0.9375rem;
    font-weight: 700;
    letter-spacing: -0.01em;
  }
}

@layer components {
  /* The comment count sits with the other trailing facts, quiet until there is
     something to say. */
  .issue-row__comments {
    display: inline-flex;
    flex: none;
    align-items: center;
    gap: 4px;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-variant-numeric: tabular-nums;
  }

  .issue-row__comments .icon {
    width: 14px;
    height: 14px;
    font-size: 14px;
  }
}

@layer components {
  /* A project row plus its own destructive control. The row fills the width and
     the control sits outside it, so clicking the row can only ever navigate.

     Named for the index, not for "project list": the dashboard already had a
     `.project-list__row`, and this rule's `> :first-child { flex: 1 }` landed
     on that row's leading icon and stretched it, pushing the title into the
     middle of an empty row. Two different rows had the same name. */
  .project-index__row {
    display: flex;
    align-items: center;
    gap: 8px;
  }

  .project-index__row > :first-child {
    min-width: 0;
    flex: 1;
  }

  .project-index__delete {
    flex: none;
  }
}

/* ── Issue detail ─────────────────────────────────────────────────────────── */

/* The issue page and its editable properties, adapted from Circle's
 * `details/` directory (MIT, © 2025 lndev-ui). The source's properties panel
 * carries status, priority, cycle, project, relations and linked diffs; all
 * but state, labels, assignees and milestone are dropped, because GitHub has
 * no field for them and this application does not invent fields. See
 * docs/2026-08-20-linear-design-github-shape.md. */

@layer components {
  /* Areas rather than columns, because the rail changes position rather than
     disappearing: under the heading on a narrow screen, where state and
     assignee are the first things worth knowing, and beside the body when
     there is room. Circle hides it below `lg`; a phone is where an issue is
     most often read, so hiding it there is the wrong trade. */
  .issue-detail {
    display: grid;
    grid-template-areas:
      "head"
      "rail"
      "main";
    grid-template-columns: minmax(0, 1fr);
    gap: 24px;
  }

  .issue-detail__head {
    grid-area: head;
    min-width: 0;
  }

  .issue-detail__main {
    grid-area: main;
    min-width: 0;
    /* A measure, not the window. Prose read edge to edge on a wide screen
       loses its place between lines. */
    max-width: 68ch;
  }

  .issue-detail__rail {
    grid-area: rail;
    min-width: 0;
  }

  @media (width >= 64rem) {
    .issue-detail {
      grid-template-areas:
        "head head"
        "main rail";
      grid-template-columns: minmax(0, 1fr) 280px;
      column-gap: 40px;
    }

    .issue-detail__rail {
      padding-inline-start: 24px;
      border-inline-start: 1px solid var(--line);
    }
  }

  .properties-panel {
    display: flex;
    flex-direction: column;
    gap: 20px;
  }

  .properties-panel__heading {
    padding-block-end: 6px;
    color: var(--text-dim);
    font-size: 0.75rem;
    font-weight: 600;
  }

  .properties-panel__body {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 6px;
    color: var(--text-body);
    font-size: 0.8125rem;
  }

  /* The words a group says when it holds nothing. Muted enough to read as an
     absence rather than a value, but present, because the group is now a
     control and a control you cannot see is one you cannot use. */
  .properties-panel__none {
    color: var(--text-dim);
    font-size: 0.8125rem;
  }

  /* ── timeline ──────────────────────────────────────────────────────────── */

  .timeline {
    position: relative;
    display: flex;
    flex-direction: column;
    gap: 2px;
    padding-block: 4px;
    list-style: none;
  }

  /* The thread. Circle draws no line and its feed reads as loose rows; this is
     what turns a sequence of facts into one history. It stops short at both
     ends so it does not appear to continue past the first and last events. */
  .timeline::before {
    content: "";
    position: absolute;
    inset-block: 14px;
    inset-inline-start: 11px;
    width: 1px;
    background: var(--line);
  }

  .timeline-event {
    position: relative;
    display: flex;
    align-items: center;
    gap: 10px;
    padding-block: 6px;
    color: var(--text-muted);
    font-size: 0.8125rem;
  }

  .timeline-event__glyph {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    flex: none;
    /* Painted, not transparent: the glyph has to cover the thread behind it
       or the line runs through the middle of every marker. */
    border-radius: 50%;
    background: var(--ink-surface);
    box-shadow: 0 0 0 3px var(--ink-surface);
    color: var(--icon-tertiary);
    font-size: 13px;
  }

  .timeline-event[data-tone="info"] .timeline-event__glyph { color: var(--info); }
  .timeline-event[data-tone="success"] .timeline-event__glyph { color: var(--success); }
  .timeline-event[data-tone="warning"] .timeline-event__glyph { color: var(--warning); }
  .timeline-event[data-tone="danger"] .timeline-event__glyph { color: var(--danger); }

  .timeline-event__text {
    min-width: 0;
    display: inline-flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 4px;
  }

  .timeline-event__actor {
    color: var(--text-primary);
    font-weight: 500;
  }

  .timeline-event__at {
    flex: none;
    margin-inline-start: auto;
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  /* A card against the events' single lines. The contrast is what makes a
     thread of six label changes and one real comment scannable. */
  .timeline-comment {
    position: relative;
    margin-block: 8px;
    margin-inline-start: 32px;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--ink-raised);
  }

  .timeline-comment__head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 10px 14px;
    border-block-end: 1px solid var(--line);
    background: var(--wash-hover);
    border-start-start-radius: var(--radius-md);
    border-start-end-radius: var(--radius-md);
  }

  .timeline-comment__author {
    color: var(--text-primary);
    font-size: 0.8125rem;
    font-weight: 600;
  }

  .timeline-comment__badge {
    padding: 1px 6px;
    border: 1px solid var(--line-strong);
    border-radius: 999px;
    color: var(--text-dim);
    font-size: 0.6875rem;
  }

  .timeline-comment__at {
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  .timeline-comment__actions {
    margin-inline-start: auto;
  }

  .timeline-comment__body {
    padding: 14px;
    color: var(--text-body);
    font-size: 0.875rem;
    line-height: 1.6;
    overflow-wrap: anywhere;
  }

  .timeline-comment__body > :is(p, ul, ol, pre, blockquote, table) + * {
    margin-block-start: 0.75em;
  }

  .timeline-comment__body :is(ul, ol) {
    padding-inline-start: 1.25em;
    list-style: revert;
  }

  .timeline-comment__body code {
    padding: 1px 4px;
    border-radius: var(--radius-sm);
    background: var(--wash-strong);
    font-family: var(--font-mono);
    font-size: 0.9em;
  }

  .timeline-comment__body pre {
    padding: 10px 12px;
    overflow-x: auto;
    border-radius: var(--radius-sm);
    background: var(--ink-void);
  }

  .timeline-comment__body pre code {
    padding: 0;
    background: none;
  }

  .timeline-comment__body a {
    color: var(--text-primary);
    text-decoration: underline;
    text-underline-offset: 2px;
  }

  /* ── composer ──────────────────────────────────────────────────────────── */

  .comment-composer {
    display: flex;
    align-items: flex-start;
    gap: 10px;
    margin-block-start: 16px;
  }

  .comment-composer__face {
    padding-block-start: 8px;
  }

  /* The whole well is the control. A labelled box with a button loose
     underneath is what made the old composer read as a form field that
     happened to be on an issue page rather than as somewhere to write. */
  .comment-composer__well {
    flex: 1;
    min-width: 0;
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    background: var(--ink-raised);
    transition: border-color var(--motion-fast) var(--ease);
  }

  .comment-composer__well:focus-within {
    border-color: var(--line-strong);
  }

  .comment-composer__field {
    padding: 4px 4px 0;
  }

  /* The control inside comes from the caller, so it is stripped here rather
     than restyled: the well already draws the edge, and a second border inside
     it is the thing that made this look unfinished. */
  .comment-composer__field :is(textarea, input) {
    width: 100%;
    min-height: 84px;
    border: 0;
    background: transparent;
    resize: vertical;
  }

  .comment-composer__field :is(textarea, input):focus,
  .comment-composer__field :is(textarea, input):focus-visible {
    outline: none;
    box-shadow: none;
  }

  .comment-composer__field label {
    /* The label is the placeholder's job here; keeping both stacks two
       prompts over one empty box. It stays in the markup for the control's
       accessible name. */
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .comment-composer__foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 8px 12px;
    border-block-start: 1px solid var(--line);
  }

  .comment-composer__hint {
    color: var(--text-dim);
    font-size: 0.75rem;
  }

  /* ── field menus ───────────────────────────────────────────────────────── */

  .field-menu {
    display: inline-flex;
  }

  /* The trigger is the value: no chrome until it is hovered, so a row of them
     reads as facts rather than as a row of buttons. */
  .field-menu__trigger {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 2px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: inherit;
    font: inherit;
    cursor: pointer;
    transition: background var(--motion-quick) var(--ease);
  }

  @media (hover: hover) {
    .field-menu__trigger:hover {
      background: var(--wash-hover);
    }
  }

  .field-menu__panel {
    width: max-content;
    min-width: 200px;
    max-width: 280px;
    max-height: 320px;
    padding: 4px;
    overflow-y: auto;
  }

  @supports (position-area: bottom span-right) {
    .field-menu__panel[data-align="end"] {
      position-area: bottom span-left;
    }
  }

  .field-menu__item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 6px 8px;
    border: 0;
    border-radius: var(--radius-sm);
    background: transparent;
    color: var(--text-body);
    font-size: 0.8125rem;
    text-align: start;
    cursor: pointer;
  }

  @media (hover: hover) {
    .field-menu__item:hover {
      background: var(--wash-hover);
      color: var(--text-primary);
    }
  }

  .field-menu__item:focus-visible {
    background: var(--wash-selected);
    color: var(--text-primary);
    outline: none;
  }

  /* The tick keeps its column whether or not it is drawn, so the words below a
     selected option do not shift left. */
  .field-menu__mark {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 14px;
    flex: none;
    color: var(--text-primary);
    font-size: 13px;
  }

  .field-menu__glyph {
    flex: none;
    color: var(--icon-tertiary);
    font-size: 15px;
  }

  .field-menu__label {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
    text-overflow: ellipsis;
  }

  .issue-row__actions {
    display: inline-flex;
    align-items: center;
    flex: none;
  }

  /* A bare dot outside a label pill -- in a menu, beside the option it stands
     for. The pill's own rule cannot reach it, so the tone is read from the dot
     itself. */
  .issue-label__dot[data-tone="primary"] { background: var(--text-primary); }
  .issue-label__dot[data-tone="info"] { background: var(--info); }
  .issue-label__dot[data-tone="success"] { background: var(--success); }
  .issue-label__dot[data-tone="warning"] { background: var(--warning); }
  .issue-label__dot[data-tone="danger"] { background: var(--danger); }

  /* ── the issue page's own heading ──────────────────────────────────────── */

  .issue-heading__title {
    color: var(--text-primary);
    font-size: 1.75rem;
    font-weight: 600;
    line-height: 1.2;
    text-wrap: balance;
  }

  /* The number is part of the title, not a separate fact: it is how the issue
     is referred to out loud and in a commit message. Lighter, not smaller than
     the words it belongs to. */
  .issue-heading__number {
    color: var(--text-dim);
    font-weight: 400;
  }

  .issue-heading__meta {
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 6px;
    padding-block-start: 10px;
    color: var(--text-muted);
    font-size: 0.8125rem;
  }

  .issue-heading__dot {
    color: var(--text-dim);
  }

  .issue-heading__actions {
    display: flex;
    align-items: center;
    gap: 8px;
    padding-block-start: 16px;
  }

  .issue-editor {
    display: flex;
    flex-direction: column;
    gap: 12px;
  }

  .issue-editor__foot {
    display: flex;
    justify-content: flex-end;
    gap: 8px;
  }

  .issue-body {
    padding-block-end: 8px;
    border-block-end: 1px solid var(--line);
  }

  /* The control that opens a property's menu. Faint until hovered, because in
     a rail of four groups four bright plus signs read as the point of the
     panel rather than as a way to change what it says. */
  .properties-panel__add {
    width: 16px;
    height: 16px;
    color: var(--icon-faint);
    font-size: 16px;
    transition: color var(--motion-fast) var(--ease);
  }

  @media (hover: hover) {
    .field-menu__trigger:hover .properties-panel__add {
      color: var(--icon-primary);
    }
  }

  .properties-panel__link {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    color: var(--text-body);
    text-decoration: none;
  }

  @media (hover: hover) {
    .properties-panel__link:hover {
      color: var(--text-primary);
      text-decoration: underline;
      text-underline-offset: 2px;
    }
  }
}