/* The component vocabulary (T-0103, DK-19).
 *
 * One `@component:` marker per entry. `cargo xtask design` check 12
 * (T-0402) asserts the markers and `design/vocabulary.md` agree in both
 * directions, so a rule with no component and a component with no rule
 * are both findings.
 *
 * Every value here is a `var(--…)` on a semantic token. No hex, no
 * literal size, no literal duration — check 4 and check 9 (T-0401).
 */

@layer components {
  /* @component: hv-visually-hidden */
  /* Available to assistive technology, absent from the page. Used by
     the loading placeholder, which must announce itself without
     relying on motion (Requirements I4). */
  .hv-visually-hidden {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* @component: hv-icon */
  /* Sized in `em` and coloured by `currentColor`, so an icon scales
     with whatever text it sits beside and never needs a size prop
     (DK-23).
     The stroke frame stays as presentation attributes on each `<svg>`,
     deliberately, and is NOT moved here: a glyph that carries state
     (Requirements C4) must not depend on an external stylesheet to be
     visible at all. The cost is ~135 bytes of repeated frame per icon;
     the benefit is that an icon cannot render as nothing. */
  .hv-icon {
    width: 1em;
    height: 1em;
    flex: none;
    vertical-align: -0.125em;
  }

  /* @component: hv-button */
  .hv-button {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    height: var(--control-height);
    padding-inline: var(--control-pad-x);
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-sm);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    line-height: var(--leading-ui);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
  }

  .hv-button--primary {
    background: var(--accent);
    color: var(--fg-on-accent);
  }

  .hv-button--neutral {
    background: var(--surface-default);
    color: var(--fg-default);
    border-color: var(--border-control);
  }

  .hv-button--danger {
    background: var(--danger);
    color: var(--fg-on-accent);
  }

  .hv-button--neutral:hover {
    border-color: var(--border-strong);
  }

  /* `aria-disabled` as well as `disabled`: an anchor cannot carry the
     attribute, so a disabled link says so the only way a link can —
     and it must look the same as a disabled button, or one of the two
     shapes reads as pressable when it is not. */
  .hv-button[disabled],
  .hv-button[aria-disabled='true'],
  .hv-button[aria-busy='true'] {
    cursor: not-allowed;
    opacity: 0.65;
  }

  /* A disabled link has no `href`, so it gets none of the anchor
     affordances either. */
  .hv-button[aria-disabled='true']:hover {
    border-color: var(--border-subtle);
  }

  /* The label stays visible while loading — the spinner sits beside
     it, never on top of it. `legacy.css`'s `.chat-send[disabled] {
     color: transparent }` hid the label from sighted users and left it
     in the accessibility tree. */
  .hv-button__spinner {
    width: 0.85em;
    height: 0.85em;
    border: var(--border-width-emphasis) solid currentColor;
    border-top-color: transparent;
    border-radius: var(--radius-full);
    flex: none;
  }

  /* The keystroke that does the same thing, drawn on the control.
     `design/findings/T-0503-command-surface.md` refuses a command
     palette because "a keyboard binding is a promise" nobody can see;
     a hint on the control is that promise made where it is kept. */
  /* `background: transparent` and `color: inherit` are load-bearing.
     `base.css` gives every `<kbd>` the sunken fill and the default
     border, which is right for a keystroke in prose and wrong inside a
     filled button: the label's white text on a light grey chip is an
     empty box, which is exactly what shipped for one screenshot. A
     hint drawn on a control belongs to the control's own colours. */
  .hv-button__key {
    flex: none;
    padding: 0 var(--space-1);
    background: transparent;
    color: inherit;
    border: var(--border-width) solid currentColor;
    border-radius: var(--radius-sm);
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    line-height: var(--leading-ui);
    opacity: 0.7;
  }

  /* A control and the sentence that says why it is in the state it is
     in. This replaces `Button::because`'s `title` attribute, which was
     invisible on touch, unreachable by keyboard, and the one place the
     platform broke its own rule that "hover is not perception"
     (T-0907). */
  .hv-explained {
    display: inline-flex;
    flex-direction: column;
    gap: var(--space-1);
    align-items: flex-start;
  }

  .hv-explained__why {
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  /* @component: hv-field */
  .hv-field {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    margin-bottom: var(--space-4);
  }

  .hv-field__label {
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--fg-default);
  }

  .hv-field__control {
    height: var(--control-height);
    padding-inline: var(--control-pad-x);
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-sm);
    background: var(--surface-default);
    color: var(--fg-default);
    font: inherit;
    font-size: var(--text-md);
  }

  textarea.hv-field__control {
    height: auto;
    padding-block: var(--space-2);
    line-height: var(--leading-prose);
    resize: vertical;
  }

  .hv-field__control:focus-visible {
    border-color: var(--accent);
    outline: var(--border-width-emphasis) solid var(--accent);
    outline-offset: 1px;
  }

  /* `--fg-muted`, not `--fg-subtle`. A hint is text somebody has to
     read; `--fg-subtle` is cleared only for text nobody has to read to
     act (design/tokens.md). */
  .hv-field__hint {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  .hv-field__error {
    display: flex;
    align-items: center;
    gap: var(--space-1);
    margin: 0;
    font-size: var(--text-sm);
    color: var(--danger);
  }

  .hv-field--error .hv-field__control {
    border-color: var(--danger);
  }

  /* @component: hv-panel */
  /* **A panel lifts; it does not outline.** `--border-default` on every
     panel edge made every block on a page weigh exactly the same — a
     screen of identical hairline rectangles reads as a form, and the
     eye is given nowhere to go. The hairline drops to
     `--border-subtle` and `--shadow-raised` carries the separation
     instead, which is what `--shadow-raised` was declared for and what
     the token's own note ("light elevation is shadow") already says. */
  .hv-panel {
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-raised);
    overflow: hidden;
  }

  /* Light elevation is the shadow; dark elevation is the fill step.
     Both are declared, and neither carries information alone (DK-10). */
  .hv-panel--raised {
    background: var(--surface-raised);
    box-shadow: var(--shadow-raised);
  }

  /* The other end of the same one-step scale: an inset well for content
     the page holds rather than states — a build log, a review diff.
     `--border-default` again, because `--surface-sunken` is what the
     edge is seen against and `--border-subtle` does not clear the
     divider floor there (check 7's own rows). */
  .hv-panel--sunken {
    background: var(--surface-sunken);
    border-color: var(--border-default);
  }

  /* `--border-default`, not `--border-subtle`: a raised panel's surface
     is `--surface-raised`, against which `--border-subtle` measures
     1.09 : 1 in dark — below the 1.20 divider floor. Found by check 7
     the moment it started reading real rules. */
  .hv-panel__header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    padding: var(--space-3) var(--space-4);
    border-bottom: var(--border-width) solid var(--border-default);
  }

  /* `flex: none` is load-bearing. The header is a flex row and the
     actions slot beside it can hold anything a caller passes; without
     this the title shrinks to its min-content width, and a wide action
     turned "Runs" into a column one letter tall per line. A panel's
     title is the one thing in its header that must always be
     readable. */
  /* `flex: 0 1 auto` with `min-width: 0`, not `flex: none`.
     `none` was right against the original failure — a wide action slot
     shrank "Runs" to a column one letter tall — and wrong against the
     opposite one: a conversation's title is its first sentence, and a
     panel pinned at the top of a bounded column clipped it at the
     header's edge with no ellipsis and nothing to scroll.

     Shrinking only happens when there is not enough room, and `nowrap`
     makes the title's flex base its full width, so a short title never
     gives anything up. What a long one gives up is its tail, and it
     says so. */
  .hv-panel__title {
    margin: 0;
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-xl);
    font-weight: var(--weight-bold);
  }

  /* And the actions take what is left rather than demanding room the
     title needs — they wrap, the title does not. */
  .hv-panel__actions {
    min-width: 0;
  }

  .hv-panel__actions {
    display: flex;
    gap: var(--space-2);
  }

  .hv-panel__body {
    padding: var(--space-4);
  }

  .hv-panel__footer {
    padding: var(--space-3) var(--space-4);
    border-top: var(--border-width) solid var(--border-default);
  }

  .hv-panel__placeholder {
    min-height: var(--space-8);
    border-radius: var(--radius-md);
    background: var(--surface-sunken);
  }

  /* The stat treatment (T-0304). A panel whose whole body is one
     measured number: the title already names what is being measured, so
     the value needs no label beside it — which is what the admin
     console's `metric` / `metric-label` / `metric-value` trio was, three
     class names and no rule anywhere.

     A part of `hv-panel`, not a component of its own: it has one slot,
     no states, and no meaning outside the panel whose title labels it.
     `--text-3xl` is the same step `hv-empty__heading` takes, so a
     number and an absence are set at one size. */
  .hv-panel__stat {
    margin: 0;
    font-size: var(--text-3xl);
    font-weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
  }

  /* @component: hv-badge */
  .hv-badge {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    padding: 0 var(--space-2);
    height: var(--space-5);
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-full);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    white-space: nowrap;
  }

  .hv-badge--success {
    background: var(--surface-success-soft);
    border-color: var(--border-status-success);
    color: var(--success);
  }

  .hv-badge--warning {
    background: var(--surface-warning-soft);
    border-color: var(--border-status-warning);
    color: var(--warning);
  }

  .hv-badge--danger {
    background: var(--surface-danger-soft);
    border-color: var(--border-status-danger);
    color: var(--danger);
  }

  .hv-badge--info {
    background: var(--surface-accent-soft);
    border-color: var(--border-default);
    color: var(--accent);
  }

  /* @component: hv-notice */
  /* One component replacing eight: .finding, .alerts, section.alerts,
     .error, .chat-error, .pipeline-gate-note, .pipeline-escalation,
     .chat-finalized-banner. */
  .hv-notice {
    display: flex;
    align-items: flex-start;
    gap: var(--space-3);
    padding: var(--space-3) var(--space-4);
    border: var(--border-width) solid transparent;
    border-radius: var(--radius-md);
    margin-bottom: var(--space-4);
  }

  .hv-notice__body {
    flex: 1;
    min-width: 0;
  }

  .hv-notice__heading {
    margin: 0 0 var(--space-1);
    font-weight: var(--weight-medium);
  }

  .hv-notice__text {
    margin: 0;
    font-size: var(--text-md);
  }

  .hv-notice--success {
    background: var(--surface-success-soft);
    border-color: var(--border-status-success);
    color: var(--success);
  }

  .hv-notice--warning {
    background: var(--surface-warning-soft);
    border-color: var(--border-status-warning);
    color: var(--warning);
  }

  .hv-notice--danger {
    background: var(--surface-danger-soft);
    border-color: var(--border-status-danger);
    color: var(--danger);
  }

  .hv-notice--info {
    background: var(--surface-accent-soft);
    border-color: var(--border-default);
    color: var(--fg-default);
  }

  /* A notice whose appearance a signal decides — the pipeline's
     disconnected stream (Requirements H3). Hidden by this rule from the
     first paint, never by an inline `style` and never by `data-show`:
     the pinned bundle's `show` plugin *removes* an inline `display`
     rather than setting one, so an element a class hides would stay
     hidden however true the signal got. `data-class` adds `--shown`
     instead. */
  .hv-notice--live {
    display: none;
  }

  .hv-notice--live.hv-notice--shown {
    display: flex;
  }

  /* @component: hv-row */
  .hv-row {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-height: var(--control-height);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-md);
    border-inline-start: var(--border-width-emphasis) solid transparent;
    color: inherit;
    text-decoration: none;
  }

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

  .hv-row__leading,
  .hv-row__trailing {
    display: inline-flex;
    align-items: center;
    gap: var(--space-1);
    flex: none;
  }

  /* A row's trailing slot is metadata — a timestamp, a count, a status.
     `--fg-muted` at `--text-sm`, never `--fg-subtle` below it: that
     pairing is `design/Vision.md` §1.5's contrast failure, and this is
     one of the six places it lived. */
  .hv-row__trailing {
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  a.hv-row:hover {
    background: var(--surface-sunken);
  }

  .hv-row--active {
    background: var(--surface-accent-soft);
    border-inline-start-color: var(--accent);
  }

  /* @component: hv-table */
  /* The builder always wraps a table in this container, so wide content
     scrolls inside itself and the document body never does
     (Requirements J1). */
  .hv-table {
    overflow-x: auto;
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-lg);
  }

  .hv-table table {
    width: 100%;
    border-collapse: collapse;
    font-size: var(--text-sm);
  }

  /* `--border-default` again: `thead th` is filled `--surface-sunken`,
     against which `--border-subtle` measures 1.05 : 1 in light. One
     token for both rows rather than two, so a body row and a header row
     never disagree about where a table's lines are. */
  .hv-table th,
  .hv-table td {
    padding: var(--space-2) var(--space-3);
    text-align: start;
    border-bottom: var(--border-width) solid var(--border-default);
    vertical-align: top;
  }

  .hv-table thead th {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--fg-muted);
    background: var(--surface-sunken);
  }

  .hv-table tbody tr:last-child td {
    border-bottom: none;
  }

  .hv-table__caption {
    padding: var(--space-2) var(--space-3);
    font-size: var(--text-sm);
    color: var(--fg-muted);
    text-align: start;
  }

  /* @component: hv-empty */
  /* DK-21's fixed anatomy, and nothing beside it: an icon, one heading
     at `--text-3xl`, one sentence at `--text-md` in `--fg-muted`, at
     most one action. There is no rule here for a fifth part because
     there is no fifth slot on the builder — Requirements F5 works in
     this direction too. */
  .hv-empty {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-7);
    text-align: center;
  }

  /* The glyph is sized by the container's `font-size`, never by a size
     prop: `.hv-icon` is `1em`, so an icon is exactly as big as the type
     it is set in (DK-23). */
  .hv-empty__icon {
    display: inline-flex;
    font-size: var(--text-3xl);
    color: var(--fg-subtle);
  }

  /* A `<p>` at heading size, not an `<h2>`. This renders inside a panel
     whose title is already an `<h2>`, inside a table, and inside the
     rail; a level that is right in one is wrong in the next, and an
     empty state names an absence rather than opening a section. */
  /* `--text-xl`, not `--text-3xl`. An empty state used to be set two
     steps larger than the panel title above it, which made an
     *absence* the loudest thing on the screen — "No usage this period"
     shouted while the page's own title whispered. The largest type on
     any screen is that title (`--text-4xl`, `base.css`), and everything
     else ranks under it. */
  .hv-empty__heading {
    margin: 0;
    font-size: var(--text-xl);
    font-weight: var(--weight-bold);
    line-height: var(--leading-ui);
    color: var(--fg-default);
  }

  .hv-empty__text {
    margin: 0;
    max-width: var(--measure);
    font-size: var(--text-md);
    line-height: var(--leading-prose);
    color: var(--fg-muted);
  }

  .hv-empty__action {
    margin-block-start: var(--space-2);
  }

  /* `--control-height-lg` is applied HERE, by the one container that
     owns a surface's single primary action — not by a size prop on
     `hv-button`. A size prop is how a two-size scale becomes a
     three-size scale becomes eighteen font sizes (DK-14, and the note
     on `Button` in `controls.rs`). */
  .hv-empty__action .hv-button {
    height: var(--control-height-lg);
    padding-inline: var(--space-4);
  }

  /* @component: hv-error */
  /* Two halves, because it renders in two contexts (DK-26). The block
     is `Chrome::Error`'s container, which also supplies the guaranteed
     way out and defaults it to `/`. The body is `components::ErrorState`,
     which renders on its own inside `Chrome::App`'s `<main>` when a
     session exists — so the body carries the page padding itself, and
     gives it back when it is inside the block. Padding once in each
     context, never twice. */
  .hv-error {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-7);
    text-align: center;
  }

  .hv-error__body {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--space-3);
    padding-block: var(--space-7);
    text-align: center;
  }

  .hv-error .hv-error__body {
    padding-block: 0;
  }

  /* `--danger` is DK-08's "status text and status icons". The triangle
     is the non-colour half of the same signal, so the state survives a
     person who cannot separate the hue (Requirements C4). */
  .hv-error__icon {
    display: inline-flex;
    font-size: var(--text-3xl);
    color: var(--danger);
  }

  .hv-error__heading {
    margin: 0;
    font-size: var(--text-3xl);
    font-weight: var(--weight-bold);
    line-height: var(--leading-ui);
    color: var(--fg-default);
  }

  .hv-error__what {
    margin: 0;
    max-width: var(--measure);
    font-size: var(--text-md);
    line-height: var(--leading-prose);
    color: var(--fg-muted);
  }

  .hv-error__back {
    margin: 0;
    font-size: var(--text-md);
  }

  .hv-error__back a {
    color: var(--accent);
  }

  /* @component: hv-object-header */
  /* DK-22's one anatomy for every detail surface: identifier, status,
     timestamp, actions — then content sections. It is a `<div>` and not
     a second `<header>`; `Chrome::App` already owns the page's one.

     `align-items: start` (T-0903) is the half the measure column cannot
     do: when the action row wraps (the model switcher + finalize + End
     on a narrow viewport), `center` would drag the title's baseline
     to the wrapped row's vertical centre, not to the top. `start`
     keeps the title at the top of its flex line; the actions sit at
     the top of theirs. The whole interview-page header is one band
     after this. */
  .hv-object-header {
    display: flex;
    flex-wrap: wrap;
    align-items: start;
    gap: var(--space-2) var(--space-4);
    padding-bottom: var(--space-4);
    margin-bottom: var(--space-5);
    border-bottom: var(--border-width) solid var(--border-default);
  }

  .hv-object-header__identity {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    flex: 1;
    min-width: 0;
  }

  .hv-object-header__identifier {
    margin: 0;
    font-size: var(--text-2xl);
    font-weight: var(--weight-bold);
    line-height: var(--leading-ui);
    overflow-wrap: anywhere;
  }

  /* An id is compared character by character. A proportional face makes
     `l1` and `0O` a guess, which on a run id is the difference between
     the build you approved and the one you did not. */
  .hv-object-header__identifier--mono {
    font-family: var(--font-mono);
    font-size: var(--text-xl);
    font-weight: var(--weight-medium);
  }

  .hv-object-header__timestamp {
    margin: 0;
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  .hv-object-header__actions {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--space-2);
  }

  .hv-object-header__actions form {
    display: flex;
    gap: var(--space-2);
  }

  /* @component: hv-prose */
  /* Rendered Markdown — a project specification, and today nothing
     else. The one place a `Markup::trusted_html` value lands on a page,
     which is why it is a named component and not an anonymous
     `<article>`: `design/audit-points.md` names the escape hatch, and
     this names where its output goes.

     `--measure` is the whole rule. Everything inside (headings, lists,
     code, tables) is already `base.css`'s element layer, which is what
     that layer exists for. */
  .hv-prose {
    max-width: var(--measure);
  }

  /* @component: hv-diff */
  /* `<ins>` and `<del>` already carry the success and danger tints from
     `base.css`. What this adds is the half that survives a reader who
     cannot separate the two hues (Requirements C4), on the one screen
     where a mistake is most expensive: a `+` and a `−` in the markup,
     so the two differ before any stylesheet has loaded. */
  .hv-diff {
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-prose);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }

  /* The tints and the strike-through come from `base.css`'s own `ins`
     and `del` element rules; these two exist so the diff's runs are
     named markup rather than bare elements, and so a later change to
     one side cannot silently move every `<ins>` on the platform. */
  .hv-diff__added,
  .hv-diff__removed {
    border-radius: var(--radius-sm);
  }

  .hv-diff__mark {
    font-weight: var(--weight-bold);
    padding-inline-end: var(--space-1);
  }

  /* @component: hv-message */
  /* One interview turn. The agent reads as flowing prose; the person
     keeps an inline-start accent rail, which is a shape rather than a
     hue and so still separates the two speakers in monochrome. */
  .hv-message {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    border-radius: var(--radius-lg);
    border-inline-start: var(--border-width-emphasis) solid transparent;
  }

  .hv-message--agent {
    font-size: var(--text-lg);
    line-height: var(--leading-prose);
  }

  .hv-message--person {
    background: var(--surface-default);
    border-inline-start-color: var(--accent);
  }

  /* `--fg-muted` at `--text-xs`, not `--fg-subtle` at 0.7rem — one of
     the six places `design/Vision.md` §1.5's contrast failure lived. */
  .hv-message__meta {
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
    color: var(--fg-muted);
  }

  .hv-message__body {
    margin: 0;
    max-width: var(--measure);
    overflow-wrap: anywhere;
  }

  /* The optimistic turn (T-0104's two declared-and-never-emitted
     states, now wired). Hidden by this rule from the first paint — the
     anti-flash guard `style="display:none"` used to be — and revealed
     by whichever of the two state classes `data-class` adds. */
  .hv-message--live {
    display: none;
  }

  .hv-message--live.hv-message--pending,
  .hv-message--live.hv-message--failed {
    display: flex;
  }

  .hv-message--failed {
    color: var(--danger);
  }

  .hv-message--failed .hv-message__dots,
  .hv-message--failed .hv-message__pending-text,
  .hv-message--pending .hv-message__failed-text {
    display: none;
  }

  .hv-message__dots {
    display: inline-flex;
    gap: var(--space-1);
    padding-inline-end: var(--space-2);
    vertical-align: middle;
  }

  .hv-message__dots > span {
    width: var(--space-2);
    height: var(--space-2);
    border-radius: var(--radius-full);
    background: var(--fg-subtle);
  }

  /* @component: hv-composer */
  /* The one place a person types into an interview. `--radius-lg`, not
     the retired `--radius-xl`; the textarea sits on `--font-ui` at
     `--text-lg`, the size the agent's own turns are set in, so writing
     an answer and reading one are the same act. */
  .hv-composer {
    display: flex;
    align-items: flex-end;
    gap: var(--space-3);
    padding: var(--space-3);
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-lg);
  }

  .hv-composer__input {
    flex: 1;
    min-width: 0;
    field-sizing: content;
    max-height: var(--space-8);
    padding: var(--space-2) var(--space-3);
    font-family: var(--font-ui);
    font-size: var(--text-lg);
    line-height: var(--leading-prose);
    color: var(--fg-default);
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-md);
    resize: none;
  }

  .hv-composer__actions {
    display: flex;
    gap: var(--space-2);
  }

  /* @component: hv-stream */
  /* The region a `datastar-patch-elements` with `mode: append` targets
     (DK-28). Its id is the patch's selector — the pinned bundle refuses
     any mode but `outer`/`replace` without one. */
  .hv-stream {
    display: flex;
    flex-direction: column;
    gap: var(--space-3);
  }

  /* **This is the whole scroll answer, and it needs no JavaScript.**
     A `column-reverse` flex container's scroll origin is its visual
     top, which here is the newest element — so an appended event is on
     screen without anything scrolling it there. DOM order stays
     chronological, which is what keeps a screen reader, a text
     selection, and the patch's own append semantics all agreeing.

     `flex: 1; min-height: 0` (T-0902) is the half that turns this
     from a bounded log into the conversation pane's free-space
     remainder: the column flexes to fill the rest of the page, and
     `min-height: 0` is the discipline that lets it actually shrink
     (a flex item's default `min-height: auto` is its min-content
     height — the full message list — which would push the composer
     off-screen). `max-height: var(--stream-height)` is the mobile
     upper bound: on a short viewport the `flex: 1` would still let
     the stream top out and the composer is still on screen. */
  .hv-stream--newest-first {
    flex: 1;
    min-height: 0;
    flex-direction: column-reverse;
    max-height: var(--stream-height);
    overflow-y: auto;
  }

  .hv-stream--mono {
    gap: 0;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-ui);
  }

  .hv-stream--mono > p {
    margin: 0;
    overflow-wrap: anywhere;
  }

  /* A run's own lifecycle, in among the toolchain's output. The log is
     mostly `cargo`'s stdout; these six or seven lines are the platform
     narrating what it is doing, and they are what a person scans for
     when they want to know where a run got to. */
  .hv-stream__status {
    color: var(--fg-muted);
  }

  /* The empty state is a sibling, not a child: rendered inside, it
     would still be there after the first append. `:empty` matches an
     element with no child nodes at all, and the builder emits no
     whitespace between the two tags for exactly this reason. */
  .hv-stream__empty {
    display: none;
  }

  .hv-stream:empty + .hv-stream__empty {
    display: block;
  }


  /* A long label truncates instead of blowing out the row it sits in.
     The span exists so the spinner can sit beside the text rather than
     on top of it; this is the work that earns it a rule (E5). */
  .hv-button__label {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* The action keeps its own width — without this it stretches to the
     notice's full height and reads as a second block. */
  .hv-notice__action {
    flex: none;
    align-self: center;
  }

  /* An empty table is a placeholder, not a broken table — and not a
     drop zone either. It used to carry a dashed edge, which is the
     visual language of "drag something here" and of an unfinished
     wireframe, around a box two hundred pixels tall. A quiet sunken
     fill says "nothing here yet" without borrowing another control's
     grammar. */
  .hv-table--empty {
    border-color: transparent;
    background: var(--surface-sunken);
  }

  /* @component: hv-theme-choice */
  /* The three-way radio group a person uses to pick a theme (T-0602).
     Native radios inside a native fieldset — the bundle is not
     involved, the choice is durable the moment the form is submitted,
     and the control works before the Datastar bundle has loaded.

     The fieldset is bordered at `--border-subtle` because the
     component is a single, named group: a panel inside a panel is
     one too many containers, and an unbordered fieldset of three
     radios reads as a list. The legend is the group's own name, not
     a separate label. */
  .hv-theme-choice {
    margin: 0;
  }

  .hv-theme-choice__field {
    margin: 0;
    padding: var(--space-3) var(--space-4);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-md);
  }

  .hv-theme-choice__legend {
    padding-inline: var(--space-2);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--fg-default);
  }

  .hv-theme-choice__options {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-block-end: var(--space-3);
  }

  /* The radio and its label are one clickable target: a label-wrapped
     input means the label's whole box, not just the dot, is the
     control. `--fg-default` for the chosen one, `--fg-muted` for the
     other two — the difference is the one pixel, not the hue, but
     matching word to weight (`weight-medium` on the active label) is
     what makes the active row read at a glance. */
  .hv-theme-choice__option {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-1) var(--space-2);
    border-radius: var(--radius-sm);
    cursor: pointer;
    color: var(--fg-muted);
  }

  .hv-theme-choice__option:has(input:checked) {
    color: var(--fg-default);
    font-weight: var(--weight-medium);
  }

  .hv-theme-choice__radio {
    margin: 0;
    accent-color: var(--accent);
  }

  .hv-theme-choice__submit {
    margin-block-start: var(--space-2);
  }

  /* @component: hv-menu */
  /* The identity menu's disclosure (T-0701, T-0702). A native
     `<details>`/`<summary>` whose summary is the existing identity
     chip; the body — "Account · Theme · Sign out" — opens beneath it
     in zero frames because the platform chose the element that already
     does the right thing.

     The summary is the identity chip itself, with no chevron of its
     own: the user agent's default disclosure marker is the only
     affordance a person needs to know it opens, and a second icon
     competes with the chip. `list-style: none` strips the marker
     list-style. We do not re-add one.

     The body sits flush against the chip, indented by the chip's own
     width on the right side: an identity menu, opened, should look
     like "the chip, then its actions" — the same shape every menu on
     the platform has. The panel that wraps the body is the menu's
     only visual surface.

     `--surface-raised` and `--shadow-overlay`, not the page's own fill:
     this panel floats over page content, and a floating panel that
     shares the fill behind it is read as part of it. `--shadow-overlay`
     is declared in `tokens.css` for exactly this and had no call site
     until now. The three tokens this rule used to name — `--bg-panel`,
     `--radius-2`, and a `--bg-panel-hover` on the rows below — were
     never declared anywhere on this platform, so `background` resolved
     to transparent and the page painted straight through the open
     menu. */
  .hv-menu {
    position: relative;
  }

  .hv-menu > .hv-menu__summary {
    list-style: none;
    cursor: pointer;
  }

  .hv-menu > .hv-menu__summary::-webkit-details-marker {
    display: none;
  }

  .hv-menu[open] > .hv-menu__summary {
    /* The chip is the same chip either way; opening does not change
       its appearance. A different selected state is what the rail
       has, not the menu — the menu is not navigational. */
  }

  .hv-menu > .hv-menu__items {
    position: absolute;
    inset-block-start: 100%;
    inset-inline-end: 0;
    margin-block-start: var(--space-1);
    min-inline-size: 12rem;
    padding: var(--space-1);
    background: var(--surface-raised);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-overlay);
    z-index: 1;
  }

  /* **The theme chooser sits in the menu.** It was three interactions
     and a page load away — open the menu, follow "Theme" to
     `/account#theme`, choose, submit — and the menu entry that
     promised it was an in-page anchor, so a person already on
     `/account` clicked it and watched nothing happen. It is the most
     used preference in a developer tool and it was the most buried
     control on the platform.

     Inside the menu the fieldset drops its own border and heading
     weight: the menu panel is already the border, and a legend reading
     "Theme" above three radios in a panel of four rows is a heading
     for a thing that is visibly one group. */
  .hv-menu__items .hv-theme-choice {
    padding: var(--space-1) var(--space-2);
    border-top: var(--border-width) solid var(--border-subtle);
    border-bottom: var(--border-width) solid var(--border-subtle);
  }

  .hv-menu__items .hv-theme-choice__legend {
    font-size: var(--text-xs);
    color: var(--fg-subtle);
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .hv-menu__items .hv-theme-choice__options {
    flex-direction: row;
    gap: var(--space-3);
  }

  .hv-menu__items .hv-theme-choice__submit {
    inline-size: 100%;
  }

  /* The three items T-0702 ships. Each is a single row of the menu;
     a sign-out form is a row, not a special case, so it sits inside
     the same `<ul>` and reads as a third option, not a button stuck
     on the end. The form is the row's tag, the button is the row's
     label, and the row carries no border of its own — the panel
     above is the only border. */
  .hv-menu__items > * + * {
    margin-block-start: var(--space-1);
  }

  .hv-menu__items a,
  .hv-menu__items button {
    display: block;
    inline-size: 100%;
    padding: var(--space-1) var(--space-2);
    text-align: start;
    background: none;
    border: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
    text-decoration: none;
    border-radius: var(--radius-sm);
  }

  .hv-menu__items a:hover,
  .hv-menu__items a:focus-visible,
  .hv-menu__items button:hover,
  .hv-menu__items button:focus-visible {
    background: var(--surface-sunken);
  }

  /* The sign-out row lives inside its own `<form>`, which is a
     block by default and would push the menu wider than its
     neighbours. Make the form itself the row, not a container. */
  .hv-menu__items form {
    margin: 0;
  }

  /* T-0703's account page. Not a new component — three panels in
     a vertical stack, each already styled by `hv-panel` itself. The
     two classes below are the only piece of presentation this
     ticket owns: the summary definition list (Person / Company /
     Role) and the danger form's confirmation row. Both are layout,
     not visual identity, and both use the platform's existing
     tokens — no colour literals, no new scale. */
  .hv-account-summary {
    display: grid;
    grid-template-columns: max-content 1fr;
    column-gap: var(--space-3);
    row-gap: var(--space-1);
    margin: 0 0 var(--space-3) 0;
  }

  .hv-account-summary dt {
    color: var(--fg-muted);
  }

  /* `base.css` sets every `dd` on `--font-mono`, which is right where a
     definition list holds a machine value and wrong here: this list
     holds a person's name, their company's name and their role, and a
     name set in a monospace face reads as an identifier the platform
     assigned rather than as who someone is. */
  .hv-account-summary dd {
    margin: 0;
    font-family: var(--font-ui);
  }

  .hv-account-delete {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-block-start: var(--space-3);
  }

  .hv-account-delete__confirm {
    display: flex;
    align-items: center;
    gap: var(--space-2);
  }

  /* T-0704: the per-row Revoke form on the active sessions table.
     Inline, not a full-width button — the table owns the rhythm, the
     button just sits at the end of the row. */
  .hv-account-revoke {
    display: inline-flex;
    margin: 0;
  }

  /* @component: hv-confirm */
  /* A destructive button that expands to a confirm panel
     (T-0803, EPIC-08). A native `<details>`/`<summary>` whose
     `<summary>` is styled as a destructive `hv-button`; the body
     is a danger notice carrying a verb-subject sentence and the
     form that posts to the action the caller gave.

     Why a disclosure, not a dialog: the affordance lives inside
     the page flow (the object header on a draft project's
     spec · review · thread · ticket · interview · pipeline page)
     and the page already has focus, a header, and a content
     section. A modal would require a focus trap, a scrim, an
     outside-click handler, and an `Escape` handler. `<details>`
     carries every accessibility affordance natively, in zero
     frames, and `hv-menu` and `hv-rail-disclosure` already use
     the same element for the same reason.

     The summary is styled as a destructive button — same
     `--control-height`, same `--radius-sm` — but it is still a
     `<summary>`, not a `<button>`. Setting `role="button"` would
     overwrite the disclosure's role and break keyboard
     navigation. The "this looks like a button" presentation is
     purely CSS. */
  .hv-confirm {
    display: inline-block;
    /* The danger colour sits in the token system; the second-step
       button already uses it. The summary is a softer read of the
       same colour, so the open and closed states are visually
       close — destructive but not shouty. */
  }

  .hv-confirm > .hv-confirm__summary {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--space-2);
    height: var(--control-height);
    padding-inline: var(--control-pad-x);
    border: var(--border-width) solid var(--danger);
    border-radius: var(--radius-sm);
    background: var(--surface-default);
    color: var(--danger);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    line-height: var(--leading-ui);
    text-decoration: none;
    white-space: nowrap;
    cursor: pointer;
    list-style: none;
  }

  .hv-confirm > .hv-confirm__summary::-webkit-details-marker {
    display: none;
  }

  .hv-confirm > .hv-confirm__summary:hover,
  .hv-confirm > .hv-confirm__summary:focus-visible {
    background: var(--surface-default);
    border-color: var(--danger);
  }

  /* The first-step label reads as a destructive action; once
     the panel is open, the summary is the trigger the person
     already pressed, and a fully filled destructive button
     would invite a second click by mistake. Mute it: keep the
     border, drop the colour. */
  .hv-confirm[open] > .hv-confirm__summary {
    color: var(--fg-muted);
    border-color: var(--border-strong);
  }

  /* The body sits below the disclosure, with the same spacing
     the object header's actions use. A confirm that opened
     above the disclosure (i.e. above the project name) would
     push the header around; below the row the person is
     already reading is where the affordance belongs. */
  .hv-confirm__body {
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    margin-block-start: var(--space-2);
    padding: var(--space-3);
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-sm);
    background: var(--surface-sunken);
  }

  .hv-confirm__sentence,
  .hv-confirm__consequence {
    margin: 0;
  }

  .hv-confirm__sentence strong {
    font-weight: var(--weight-medium);
  }

  .hv-confirm__form {
    margin-block-start: var(--space-2);
  }

  /* A keyboard user opening the disclosure lands inside it; the
     panel is the focus target so they can see it landed. */
  .hv-confirm[open]:focus-within > .hv-confirm__body {
    border-color: var(--accent);
  }

  /* Motion. Everything that moves lives in this one guarded block; with
     `prefers-reduced-motion: reduce`, none of it runs and every element
     is already in its final, usable state (Requirements C7, I1). */
  @media (prefers-reduced-motion: no-preference) {
    .hv-button,
    .hv-row,
    .hv-field__control {
      transition:
        background-color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out),
        box-shadow var(--motion-fast) var(--ease-out);
    }

    .hv-button__spinner {
      animation: hv-spin var(--motion-slow) linear infinite;
    }
  }

  @keyframes hv-spin {
    to {
      transform: rotate(1turn);
    }
  }

  /* @component: hv-spec-comment */
  /* The spec, and the form that turns a selection in it into a thread.
     The passage keeps `hv-prose`'s own measure and rhythm — this wrapper
     adds no typography, only the selection surface and the form beneath
     it. Without Datastar the form is simply always visible, so the rules
     below must read as deliberate in both states, not as a panel that
     lost its trigger. */
  .hv-spec-comment {
    /* Where the selection was, measured by the component's own
       `mouseup` handler and written here — the one thing about this
       overlay that a stylesheet cannot know. `static` until a
       selection places it, so with no bundle loaded the form is an
       ordinary block under the passage rather than an overlay parked
       over the first paragraph. */
    --spec-comment-position: static;
    --spec-comment-top: 0;
    --spec-comment-left: 0;
    --spec-comment-width: min(32rem, 100%);

    position: relative;
    display: flex;
    flex-direction: column;
    gap: var(--space-4);
  }

  .hv-spec-comment__passage {
    /* The one affordance: text here is worth selecting. */
    cursor: text;
  }

  /* **An overlay at the passage, not a form at the foot of the page.**
     A specification runs to several screens; a box below the whole
     document is never on screen at the same time as the sentence it is
     about. This one opens where the selection is.

     `min()` keeps it inside the column when the selection sits near the
     right edge — the overlay slides left instead of hanging off the
     side. `--shadow-overlay` is what says it floats above the document
     rather than being part of it; the two are already distinguished by
     that token, which is why there is no second border here.

     A column, not a row: the passage, what you want to say about it,
     and the two actions, in the order they are read and filled. */
  .hv-spec-comment__form {
    position: var(--spec-comment-position);
    top: calc(var(--spec-comment-top) + var(--space-2));
    left: min(var(--spec-comment-left), 100% - var(--spec-comment-width));
    z-index: 2;
    width: var(--spec-comment-width);
    display: flex;
    flex-direction: column;
    gap: var(--space-2);
    padding: var(--space-3);
    background: var(--surface-raised);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-overlay);
  }

  /* The passage reads as a quotation, because that is what it is: the
     rule and the muted foreground say "this is what you selected",
     while the comment field below carries the cursor. Editable, so a
     selection that caught half a word can be trimmed. */
  .hv-spec-comment__quote {
    min-width: 0;
    font: inherit;
    font-size: var(--text-sm);
    color: var(--fg-muted);
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-default);
    border-inline-start: var(--border-width-emphasis) solid var(--border-strong);
    border-radius: var(--radius-md);
    padding: var(--space-2);
    resize: vertical;
  }

  /* The overlay's own heading. It names what the box is for, which is
     the question somebody has the moment one appears under their
     cursor. */
  .hv-spec-comment__label {
    margin: 0;
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
    color: var(--fg-default);
  }

  .hv-spec-comment__body {
    min-width: 0;
    font: inherit;
    color: var(--fg-default);
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-default);
    border-radius: var(--radius-md);
    padding: var(--space-2);
    resize: vertical;
  }

  .hv-spec-comment__actions {
    display: flex;
    justify-content: flex-end;
    gap: var(--space-2);
  }

  /* A passage somebody has commented on, marked in the document itself
     and linking to the thread below it. Underline, not a fill: the spec
     is a document, and a block of colour through a sentence makes it
     harder to read the sentence — which is the thing the comment is
     about. */
  .hv-spec-comment__anchor {
    color: inherit;
    text-decoration: underline;
    text-decoration-color: var(--accent);
    text-decoration-thickness: var(--border-width-emphasis);
    text-underline-offset: var(--space-1);
  }

  .hv-spec-comment__anchor:hover {
    background: var(--surface-accent-soft);
  }

  /* ==================================================================
   * The console direction (design/CONSOLE.md) — four components whose
   * job is to say what state the work is in without being opened.
   * ================================================================== */

  /* @component: hv-time */
  /* A moment, in whatever words the surface needs. Tabular numerals
     because a column of times that does not line up is a column a
     person reads twice. No colour of its own: a time is metadata, and
     the row it sits in has already decided what muted means there. */
  .hv-time {
    font-variant-numeric: tabular-nums;
    color: var(--fg-subtle);
    white-space: nowrap;
  }

  /* @component: hv-status-strip */
  /* Where this object stands, under the tab row. One band, one line,
     rules between facts — not four cards, which is what the page used
     to do with the same information one level down.

     `--surface-default` and not `--surface-page`: the strip belongs to
     the band the app bar and tabs form, and a third fill under two
     would be a step the elevation scale does not have. */
  .hv-status-strip {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    padding: 0 var(--space-4);
    background: var(--surface-default);
    border-bottom: var(--border-width) solid var(--border-subtle);
    font-size: var(--text-sm);
  }

  .hv-status-strip__fact {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-4) var(--space-2) 0;
    margin-right: var(--space-4);
    border-right: var(--border-width) solid var(--border-subtle);
    color: var(--fg-muted);
    text-decoration: none;
  }

  /* The last fact carries no rule: a divider with nothing after it is
     a line pointing at the edge of the band. */
  .hv-status-strip__fact:last-child {
    border-right: 0;
    margin-right: 0;
  }

  a.hv-status-strip__fact:hover .hv-status-strip__value {
    text-decoration: underline;
  }

  .hv-status-strip__label {
    color: var(--fg-subtle);
  }

  .hv-status-strip__value {
    color: var(--fg-default);
    font-weight: var(--weight-medium);
  }

  /* The dot is the tone; the tone's word rides beside it in an
     `hv-visually-hidden` span, so hue is never the only carrier
     (Requirements C4). */
  .hv-status-strip__dot {
    flex: none;
    width: var(--space-2);
    height: var(--space-2);
    border-radius: var(--radius-full);
  }

  .hv-status-strip__dot--success { background: var(--success); }
  .hv-status-strip__dot--warning { background: var(--warning); }
  .hv-status-strip__dot--danger  { background: var(--danger); }
  .hv-status-strip__dot--info    { background: var(--accent); }

  /* @component: hv-steps */
  /* A long operation drawn as its phases. One bordered object holding
     rows, not one card per phase: the phases are a sequence, and a
     sequence of separate cards reads as a set of unrelated things. */
  .hv-steps {
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-lg);
    box-shadow: var(--shadow-raised);
    overflow: hidden;
  }

  .hv-steps__step {
    border-bottom: var(--border-width) solid var(--border-subtle);
  }

  .hv-steps__step:last-child {
    border-bottom: 0;
  }

  /* A step that is only a step, and the summary of one that discloses
     output, are the same row — so the two kinds line up exactly and a
     disclosure triangle is the only difference between them. */
  .hv-steps__step,
  .hv-steps__summary {
    display: flex;
    align-items: center;
    gap: var(--space-3);
    padding: var(--space-2) var(--space-4);
    min-height: var(--control-height);
    font-size: var(--text-md);
  }

  .hv-steps__summary {
    cursor: pointer;
  }

  .hv-steps__step--disclosing {
    padding: 0;
  }

  .hv-steps__summary:hover {
    background: var(--surface-sunken);
  }

  .hv-steps__label {
    flex: auto;
    min-width: 0;
    font-weight: var(--weight-medium);
  }

  .hv-steps__detail {
    display: block;
    font-weight: var(--weight-regular);
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  /* The instrument a developer actually uses to decide whether
     something has hung. Mono and tabular so a column of them compares
     at a glance. */
  .hv-steps__duration {
    flex: none;
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    font-variant-numeric: tabular-nums;
    color: var(--fg-subtle);
  }

  .hv-steps__glyph {
    flex: none;
    display: grid;
    place-items: center;
    width: var(--space-4);
    height: var(--space-4);
    border-radius: var(--radius-full);
    color: var(--fg-on-accent);
  }

  .hv-steps__glyph--done    { background: var(--success); }
  .hv-steps__glyph--failed  { background: var(--danger); }
  .hv-steps__glyph--waiting { background: var(--warning); }
  .hv-steps__glyph--running { background: var(--accent); }

  /* Not started: a hollow ring, because an empty disc and a filled one
     are a shape difference a person reads before they read a hue. */
  .hv-steps__glyph--queued {
    background: transparent;
    border: var(--border-width-emphasis) solid var(--border-control);
  }

  /* The step's own output, in the well the diff and the log already
     use. Bounded and scrolled: a `cargo build` log is thousands of
     lines and a page that grows by all of them is a page nobody can
     reach the bottom of. */
  .hv-steps__body {
    max-height: var(--stream-height);
    overflow: auto;
    padding: var(--space-3) var(--space-4);
    background: var(--surface-sunken);
    border-top: var(--border-width) solid var(--border-subtle);
    font-family: var(--font-mono);
    font-size: var(--text-sm);
    line-height: var(--leading-ui);
    white-space: pre-wrap;
    overflow-wrap: anywhere;
  }

  /* @component: hv-stat */
  /* One measure, drawn as a measure. The tile earns its border because
     a measure is a discrete object; the sentences it replaced did not,
     which is why they were a table inside a panel inside a panel. */
  .hv-stat {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    padding: var(--space-3) var(--space-4);
    background: var(--surface-default);
    border: var(--border-width) solid var(--border-subtle);
    border-radius: var(--radius-md);
    color: inherit;
    text-decoration: none;
  }

  a.hv-stat:hover {
    border-color: var(--border-control);
  }

  .hv-stat__label {
    font-size: var(--text-xs);
    color: var(--fg-subtle);
    letter-spacing: 0.04em;
    text-transform: uppercase;
  }

  .hv-stat__value {
    font-size: var(--text-2xl);
    font-weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
    line-height: var(--leading-ui);
  }

  .hv-stat__value--success { color: var(--success); }
  .hv-stat__value--warning { color: var(--warning); }
  .hv-stat__value--danger  { color: var(--danger); }
  .hv-stat__value--info    { color: var(--accent); }

  .hv-stat__note {
    font-size: var(--text-sm);
    color: var(--fg-muted);
  }

  .hv-stat__action {
    margin-top: var(--space-1);
  }

  /* The row a page lays tiles out in. Not a component — a grid, named
     once here rather than restated per page. */
  .hv-stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(var(--stat-min), 1fr));
    gap: var(--space-3);
  }

  /* ==================================================================
   * The platform's one animation
   * ================================================================== */

  /* Liveness, and nothing else. A 1.6 s halo on the two elements that
     mean "this is happening now" — a rail row whose project is
     building, and a run's current step. Everything else on the
     platform moves for 120 ms on hover and not otherwise.

     Inside `prefers-reduced-motion: no-preference`, like every other
     transition here: with the animation removed both elements keep
     their fill and their halo, so the state is still a shape. */
  @media (prefers-reduced-motion: no-preference) {
    .hv-steps__glyph--running,
    .hv-sidebar .hv-row__dot--running {
      animation-name: hv-pulse;
      animation-duration: var(--motion-pulse);
      animation-timing-function: var(--ease-out);
      animation-iteration-count: infinite;
    }
  }

  @keyframes hv-pulse {
    from { box-shadow: 0 0 0 0 var(--surface-accent-soft); }
    to   { box-shadow: 0 0 0 var(--space-2) transparent; }
  }
  /* The keyboard layer renders nothing. It is one element carrying
     one Datastar attribute, the same way `LiveRegion` is — a class so
     that check 11 has a rule to find, and no appearance because there
     is nothing to appear. */
  .hv-shortcuts {
    display: none;
  }
  /* @component: hv-disclosure */
  /* A list that is there when you look for it and out of the way when
     you are not. Native `<details>`, closed — the same mechanism
     `hv-confirm` and the compact rail already use.

     It carries no border and no fill: it sits *inside* a panel, under
     the rows it is the continuation of, and a second bordered box
     there would read as a different kind of thing rather than as the
     rest of the same list. The rule above it is the whole separation
     it needs. */
  .hv-disclosure {
    border-top: var(--border-width) solid var(--border-subtle);
  }

  .hv-disclosure__summary {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    padding: var(--space-2) var(--space-3);
    min-height: var(--control-height);
    font-size: var(--text-sm);
    color: var(--fg-muted);
    cursor: pointer;
    border-radius: var(--radius-md);
  }

  .hv-disclosure__summary:hover {
    background: var(--surface-sunken);
    color: var(--fg-default);
  }

  /* The native triangle is suppressed on every disclosure on this
     platform, because a browser's own marker is not on the icon set.
     One from the set takes its place, a quarter turn when open — the
     rotation is what says "this is the thing that opened", and without
     any marker at all the row reads as inert text. */
  .hv-disclosure__summary::-webkit-details-marker {
    display: none;
  }

  .hv-disclosure__summary {
    list-style: none;
  }

  .hv-disclosure__marker {
    display: inline-flex;
    flex: none;
    color: var(--fg-subtle);
  }

  .hv-disclosure[open] > .hv-disclosure__summary .hv-disclosure__marker {
    transform: rotate(90deg);
  }

  @media (prefers-reduced-motion: no-preference) {
    .hv-disclosure__marker {
      transition: transform var(--motion-fast) var(--ease-out);
    }
  }

  .hv-disclosure__label {
    font-weight: var(--weight-medium);
  }

  /* How much is in there, answered from the outside. A disclosure
     whose label does not say that is a disclosure nobody opens. */
  .hv-disclosure__note {
    color: var(--fg-subtle);
    font-variant-numeric: tabular-nums;
  }

  .hv-disclosure__body {
    padding-bottom: var(--space-2);
  }

  /* Settled work is read, not scanned for action: it is the record of
     how something was decided, and the open rows above it are what
     needs a person. The step down in weight is the difference. */
  .hv-disclosure__body .hv-row__content {
    color: var(--fg-muted);
  }

  /* ==================================================================
   * Narrow
   * ================================================================== */

  /* **A page title comes down two steps.** `--text-4xl` is 36px, and at
     400px "A word frequency counter for the command line." set at 36px
     is one word per line for six lines before the page starts. The step
     is a type decision on a wide screen and a layout failure on a
     narrow one.

     Here and not in `layout.css`: `@layer components` sits above
     `@layer layout` in the order `tokens.css` declares, so a rule
     written there could never beat the one above — the same trap that
     file's own note at the rail already records. */
  @media (width <= 48rem) {
    /* **The header stacks.** Identity, badge and timestamp share one
       flex line, which on a 400px screen leaves the title about 150px —
       and "A word frequency counter for the command line." came out one
       word per line for six lines before the page began. The title is
       the widest thing on the page and gets the whole width; the badge
       and the timestamp follow under it, which is also the order a
       person reads them in.

       `--text-xl` rather than `--text-2xl`: a title that has the room
       still wants to be a title, not a heading the size of a paragraph
       beneath it. */
    .hv-object-header {
      gap: var(--space-2);
    }

    .hv-object-header__identity {
      flex: 1 0 100%;
      flex-wrap: wrap;
      align-items: flex-start;
      gap: var(--space-2);
    }

    .hv-object-header__identifier {
      flex: 1 0 100%;
      font-size: var(--text-xl);
    }

    .hv-object-header__timestamp {
      flex: 1 0 100%;
    }

    .hv-object-header__actions {
      flex: 1 0 100%;
    }

    /* A panel's header is a title beside its actions. At this width the
       actions are a stacked form, and a title vertically centred
       against three stacked fields floats in the middle of them. */
    .hv-panel__header {
      flex-wrap: wrap;
      align-items: flex-start;
    }

    .hv-panel__actions {
      flex: 1 0 100%;
    }
  }
}
