/* Haive — the layout layer: the shell, the rail, the app bar, the
 * crumbs, the tab row, and the two breakpoints
 * (T-0201, T-0203; DK-12, DK-15, DK-18, DK-24, DK-24a, DK-29;
 * Requirements A6, C5, E3, F1, G5, J1, J2, J3, A3).
 *
 * This file styles the structure `crates/design-html/src/chrome.rs`
 * already renders. It does not invent markup and it does not redesign
 * any of it: every selector here has a call site in `Chrome::render`,
 * `Sidebar::render`, `Tabs::render`, `Identity::render`, or
 * `Location::render`.
 *
 * **Nothing here is visible yet.** No route calls `Page::render` —
 * fourteen view files still build their own `<head>` and link
 * `/web/app.css`, which imports the same six sheets in the same order.
 * The rules land the moment EPIC-03 moves a view onto the crate.
 *
 * Three properties this file holds:
 *
 *   **No literal values.** Every size, space, colour, radius and
 *   duration is a `var(--…)` from `tokens.css`. The only bare values
 *   are `0`, `auto`, `none`, `inherit`, `100%`, `1fr` and `100dvh` —
 *   and the two breakpoint numbers in the media conditions, which
 *   cannot be tokens (see below).
 *
 *   **Breakpoints in `rem`, never `px`.** A person at 200% browser zoom
 *   on a 1280px display has a 640px effective viewport. A `px`
 *   breakpoint keeps the 15rem rail there and leaves them 200px of
 *   content — the accessibility case responsive behaviour exists for is
 *   the one `px` breakpoints get wrong.
 *
 *   **`@media` conditions cannot read a custom property.** CSS Custom
 *   Properties §3 excludes `var()` from media queries, so `48rem` and
 *   `90rem` are written out below. They are the *values* of
 *   `--bp-compact` and `--bp-wide`; those tokens stay the documented
 *   source (`design/tokens.md`), and this file is the one place the
 *   numbers are repeated. Repeating them is not optional and is not a
 *   shortcut — there is no CSS that avoids it without a build step
 *   (PLATFORM.md §2).
 */

@layer layout {

  /* ==================================================================
   * The skip link — the first thing in the document, and invisible
   * until it is focused (T-0203's own gap, found by the console review)
   * ================================================================== */

  /* Every page began with a full project rail in the tab order: on a
     rail of a dozen projects that is a dozen links before the thing a
     person came for, on every page, forever. This is the standard
     answer and the platform had none.

     Not `display: none` and not `visibility: hidden`, either of which
     takes it out of the tab order and makes it unreachable by the one
     input method it exists for. It is positioned off the top of the
     viewport and comes back on focus — the same mechanism
     `hv-visually-hidden` uses, with a focus state added. */
  /* **Clipped, not translated off the top.** The first version parked
     it above the viewport with a transform — which still leaves a
     44px-tall absolutely-positioned box in the document's own
     scrollable overflow, so every page was 39px taller than its
     content and a conversation page that had just been told not to
     scroll scrolled anyway. Nothing on the page explained why, because
     the thing doing it was invisible.

     The same technique `hv-visually-hidden` uses, which contributes no
     overflow at all, plus a focus state that puts it back. Not
     `display: none` and not `visibility: hidden`: either takes it out
     of the tab order and makes it unreachable by the one input method
     it exists for. */
  .hv-skip {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  .hv-skip:focus {
    z-index: 2;
    width: auto;
    height: auto;
    overflow: visible;
    clip-path: none;
    padding: var(--space-2) var(--space-4);
    background: var(--surface-raised);
    color: var(--fg-default);
    border: var(--border-width) solid var(--border-control);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-overlay);
  }

  /* `<main>` carries `tabindex="-1"` so the jump actually moves focus
     rather than only scrolling — without it, the next Tab returns to
     the rail the person just skipped. The outline would then be drawn
     around the whole page, which is noise: the heading inside it is
     what the person is looking at. */
  .hv-main:focus {
    outline: none;
  }

  /* ==================================================================
   * The shell — one rail, one header, one optional tab row (DK-24)
   * ================================================================== */

  .hv-shell {
    display: grid;
    grid-template-columns: var(--rail-width) 1fr;
    /* **The row is declared, and that is load-bearing.** With no
       `grid-template-rows`, the implicit row is `auto` — it sizes to
       the tallest item and then *grows past its own container*, which
       the bounded variant below can only clip. The rail's
       `block-size: 100%` resolved against that grown row, so it never
       capped; `hv-sidebar__items` was never told to shrink; and the
       document scrolled by exactly the rail's overflow while the
       conversation inside it scrolled correctly. `minmax(0, 1fr)`
       makes the row the container's height and lets items with
       `min-height: 0` do the shrinking they already ask for. */
    grid-template-rows: minmax(0, 1fr);
    min-height: 100dvh;
  }

  /* **A conversation is bounded by the window; every other page is
     bounded by its content.**

     `min-height` is right for a document: the shell fills the viewport
     when the page is short and grows when it is long, and the document
     scrolls. It is wrong for a chat, and it is why the composer scrolled
     away — `hv-stream`'s own `overflow-y: auto` below never engaged,
     because nothing in its ancestor chain had a height for it to
     overflow. A box that can always grow never scrolls.

     So the one page whose body scrolls inside itself gets an exact
     height, and `min-height: 0` on the chain down to the stream, which
     is what lets a flex item shrink below its content (its automatic
     minimum is `auto`, and that is the whole of this bug in four
     words).

     Selected with `:has()` because the constraint belongs to the
     ancestor and the page identifies itself at `<main>`; the same
     pattern `.hv-main--centered:has(> .sc-view)` in `pages.css` already
     uses. Where `:has()` is unavailable the rule is dropped and the
     document scrolls as it did — quieter than correct, never wrong. */
  /* **A conversation page is exactly the viewport, and says so at the
     document.**

     The shell is already `100dvh` and clipped, and the transcript
     already scrolls inside it — but the *document* kept a few dozen
     pixels of scroll anyway, from rounding between `100dvh` and the
     visual viewport and from boxes the shell clips but the initial
     containing block still counts. The effect a person sees is the
     whole chat drifting under a header that was supposed to be fixed,
     which is the complaint this exists to answer.

     Declaring it here is not a patch over that arithmetic; it is the
     statement the layout has been making since T-0902 — on these
     pages the document does not scroll, one region does. Every other
     page is untouched, and the column's own `overflow-y` below keeps
     the composer reachable when a narrow screen cannot fit everything
     at once. */
  html:has(.hv-main--conversation),
  body:has(.hv-main--conversation) {
    overflow: hidden;
  }

  .hv-shell:has(.hv-main--conversation) {
    height: 100dvh;
    min-height: 0;
    overflow: hidden;
  }

  .hv-shell:has(.hv-main--conversation) .hv-pane {
    min-height: 0;
  }

  /* **Not optional.** A grid item's default `min-width: auto` is its
     min-content width, so one wide `<table>` inside `<main>` pushes the
     `1fr` column past its track and the *document* scrolls sideways —
     the exact failure Requirements J1 forbids. Declared on both the
     grid item and the flex item inside it, because each is a separate
     automatic minimum. */
  .hv-pane,
  .hv-main {
    min-width: 0;
  }

  .hv-pane {
    display: flex;
    flex-direction: column;
    background: var(--surface-page);
  }

  .hv-main {
    flex: auto;
    width: 100%;
    padding: var(--space-5) var(--space-6);
    /* `anywhere`, not `break-word`: `anywhere` also reduces the element's
       min-content width, which is what stops a long run id or URL from
       widening the grid column in the first place (DK-29). `pre` is
       unaffected — `white-space: pre` does not wrap and `base.css`
       already gives it `overflow-x: auto`. */
    overflow-wrap: anywhere;
  }

  /* `Chrome::SignedOut` and `Chrome::Error`: one centred column, no
     rail, no app bar. `min()` is DK-15's whole mobile-prose story —
     `68ch` exceeds a 360px viewport, and `100%` is the other branch.
     Deliberately not vertically centred: a flex `center` clips the top
     of anything taller than the viewport, and the two surfaces that
     render here already own their vertical rhythm (`pages.css`'s
     `.hv-signin`, `components.css`'s `.hv-error`). */
  .hv-main--centered {
    width: min(var(--measure), 100%);
    margin-inline: auto;
    padding: var(--space-6);
  }

  /* **The conversation page (T-0901, T-0902).** The interview's
     <main> opts into a measure column so the title, status and
     timestamp on its object header (T-0903) read as one bounded
     band rather than scattered to opposite edges of a 1280px
     viewport, and the column becomes a flex column (T-0902) so the
     stream takes the free space and the composer anchors to the
     bottom of the pane — the alternative was a composer floating at
     a third height with an ocean of empty page beneath it. Same
     width shape as `.hv-main--centered`; not the same width: this
     one keeps the chrome's `--space-5` / `--space-6` page padding
     (so the rail's own `hv-pane` continues to own the rhythm
     between header and body) rather than re-padding. Centred
     horizontally, in the same way, so the band is the same width on
     a 1024px viewport and a 1920px one. `min-height: 0` is the
     flex-item discipline that lets the stream actually shrink —
     without it, the stream's min-content height (its full message
     list) makes the whole column grow to fit. */
  .hv-main--conversation {
    width: min(var(--measure), 100%);
    margin-inline: auto;
    display: flex;
    flex-direction: column;
    min-height: 0;
    /* The column is exactly the pane's remaining height, and what
       scrolls is inside it. Without this the column is as tall as its
       content and the stream has nothing to overflow. */
    overflow: hidden;
  }

  /* **T-0902's other half, which the ticket described and did not
     ship.** The declarations that make a stream take the remainder were
     written onto `.hv-stream--newest-first` — the pipeline log's
     modifier — and the interview renders a plain `.hv-stream`, so the
     conversation column stretched to the viewport and its contents
     stacked at the top: a composer floating at a third height with an
     ocean of empty page beneath it, which is the exact sentence
     REDESIGN §2's R9 uses.

     Two declarations, and neither is `flex: 1` on the stream. The
     column's children are the header, the stream, the empty state, the
     *live* agent turn (a sibling of the stream, not a child of it — it
     is what Datastar patches while an answer streams) and the
     composer. A growing stream would push that live turn down to sit
     against the composer, detached from the transcript it belongs to.
     So the stream only learns to *shrink* — `min-height: 0` undoes a
     flex item's `auto` minimum, and a long transcript scrolls inside it
     — and the composer takes the free space as a margin, which is what
     anchors it to the bottom of the pane whatever is above it. */
  /* **The transcript takes the free space, and it is the only thing
     that scrolls.**

     It used to only learn to *shrink* (`min-height: 0` alone) while the
     composer claimed the free space with an automatic margin. That
     anchors the composer to the bottom of a column that is as tall as
     its content — which is the bottom of a very long page, not the
     bottom of the window. `flex: 1` on the stream and a bounded column
     above put it where a chat's input belongs: on screen, always, with
     the conversation scrolling behind it.

     The header above and the composer, notice and live turn below stay
     `flex: none`, so a long transcript takes space from nothing but
     itself. */
  .hv-main--conversation > .hv-stream {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }

  /* **A conversation that lives in a panel scrolls inside it too.**
     The interview renders its transcript as bare children of the
     column, and the rules above bound it. A conversation's own
     permalink renders the same transcript inside an `hv-panel` —
     because the panel carries the title and the drift badge, and
     because the spec page renders that panel inline beside a
     document — so the column's direct child is the panel, and none of
     the rules above reached it. The whole page scrolled: the title,
     the composer and the rail's worth of chrome all moving while a
     person read.

     The panel becomes the column, its body becomes the transcript,
     and its header and footer stay put — so the thing that scrolls is
     the messages and nothing else. */
  .hv-main--conversation > .hv-panel {
    flex: 1;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }

  .hv-main--conversation > .hv-panel > .hv-panel__header,
  .hv-main--conversation > .hv-panel > .hv-panel__footer {
    flex: none;
  }

  .hv-main--conversation > .hv-panel > .hv-panel__body {
    flex: 1;
    min-height: 0;
    overflow-y: auto;
  }

  .hv-main--conversation > .hv-object-header,
  .hv-main--conversation > .hv-stream__empty,
  .hv-main--conversation > .hv-message,
  .hv-main--conversation > .hv-notice,
  .hv-main--conversation > .hv-composer {
    flex: none;
  }

  /* ==================================================================
   * The rail — the object switcher, on its own ramp (DK-12)
   * ================================================================== */

  /* **The rail is a third theme, so it re-points the semantic layer
     inside its own subtree.** This is DK-11's mechanism ("a theme block
     re-points the semantic layer and nothing else") applied to a scope
     rather than to a document, and it is what DK-12's "its own semantic
     group" means in CSS: a component dropped into the rail resolves the
     rail's ramp because the *names* resolve differently here, not
     because every component grows a rail variant.

     It is also the only mechanism available. `@layer components` sits
     ABOVE `@layer layout` in the order `tokens.css` declares, so
     `.hv-sidebar .hv-row--active` written here could never beat
     `.hv-row--active` written there, at any specificity — layers settle
     that before specificity is consulted. A custom property is resolved
     at *use* time from the element's inherited environment, so it
     reaches where a selector cannot. Concretely, this is what turns
     `hv-row`'s `background: var(--surface-accent-soft)` (a near-white
     tint, which would render the rail's active label at 1.02 : 1) into
     `--rail-bg-active`, and its hover `--surface-sunken` into
     `--rail-bg-hover`.

     Every mapping below lands on a pair `design/contrast.md`'s rail
     section already computes. `--accent` is deliberately NOT re-pointed:
     the active-item marker is `--accent` on `--rail-bg` at 3.15 : 1,
     which is the row the table reserves for it. */
  .hv-sidebar {
    --surface-page:        var(--rail-bg);
    --surface-default:     var(--rail-bg);
    --surface-raised:      var(--rail-bg-active);
    --surface-sunken:      var(--rail-bg-hover);
    --accent:              var(--rail-accent);
    --surface-accent-soft: var(--rail-bg-active);
    --fg-default:          var(--rail-fg);
    --fg-muted:            var(--rail-fg-muted);
    --fg-subtle:           var(--rail-fg-subtle);
    --border-subtle:       var(--rail-border);
    --border-default:      var(--rail-border);
    --border-control:      var(--rail-border-control);
    --border-strong:       var(--rail-fg-subtle);

    display: flex;
    flex-direction: column;
    gap: var(--space-3);
    /* The rail fills its grid track whether the track holds the `<aside>`
       itself or the `::details-content` box the disclosure below may put
       between them. Both are stretched grid items, so the percentage
       resolves against a definite height in either case. */
    block-size: 100%;
    min-height: 0;
    padding: var(--space-3);
    background: var(--rail-bg);
    border-inline-end: var(--border-width) solid var(--rail-border);
    color: var(--rail-fg);
    font-size: var(--text-md);
  }

  .hv-sidebar__brand {
    display: flex;
    align-items: center;
    min-height: var(--control-height);
    padding-inline: var(--space-2);
    font-size: var(--text-lg);
    font-weight: var(--weight-bold);
    color: var(--rail-fg);
  }


  /* **Hai · the mark · ve.** The three parts sit on one baseline, so
     the hexagon is sized against the wordmark's own cap height rather
     than against a fixed pixel box — change the brand's font size and
     the mark follows. */
  /* The wordmark is a link and must not read as body copy's accent
     link: `base.css` gives every `<a>` `--accent` and an underline, and
     `layout` sits above `base`. Written on `.hv-brand` rather than on
     `.hv-sidebar__brand a`, because the mark appears in the app bar too
     — scoping this to the rail is what left it underlined and violet
     the first time it did. */
  a.hv-brand {
    color: inherit;
    text-decoration: none;
  }

  .hv-brand {
    display: inline-flex;
    align-items: center;
    /* No `gap`: the mark stands in for a letter, so it sits at letter
       spacing, and any gap here would be an off-scale literal
       (D1–D5) — check 9 is right about that. */
    letter-spacing: -0.01em;
  }

  /* `--accent`, not `--rail-accent`, and that is the whole trick: the
     rail re-points `--accent` to its own violet inside `.hv-sidebar`
     (above), so this one declaration draws the mark in `--rail-accent`
     on the rail and in the content theme's accent in the app bar, where
     the same mark appears on a light surface below `--bp-compact`. The
     mark and the active project's marker are the same colour by
     construction, in both places, from one line. */
  .hv-brand__mark {
    height: .92em;
    width: auto;
    color: var(--accent);
  }

  /* The console says which product it is, quietly — the rails are
     otherwise identical and someone in an incident has both open.

     `--fg-muted`, not `--rail-fg-muted`, for the same reason
     `.hv-brand__mark` takes `--accent`: the rail re-points the name
     inside `.hv-sidebar` (above) and the wordmark also appears on the
     console's sign-in screen, which is a light content surface.
     `--rail-fg-muted` measures 2.00 : 1 there — a rail token reaching
     into the content pane is exactly what DK-12 forbids, and this is
     what it looks like when it happens. */
  .hv-brand__suffix {
    margin-inline-start: var(--space-2);
    font-weight: var(--weight-regular);
    color: var(--fg-muted);
  }

  /* The create action. Its fill, its label colour and its edge all come
     from the re-pointed names above — `--surface-default` → `--rail-bg`,
     `--fg-default` → `--rail-fg`, `--border-control` →
     `--rail-border-control` (3.03 : 1, the row the table reserves for a
     control living in the rail). Nothing here restates a colour. */
  .hv-sidebar__new {
    margin: 0;
  }

  .hv-sidebar__new .hv-button {
    width: 100%;
    justify-content: center;
  }

  /* The inbox row: what needs this person, across every project, with
     the count. Above the list, because it is not a project — and
     visually a row of the same height as one, so the rail reads as one
     list with a head rather than as two lists.

     `--rail-bg-active` and not the accent: the row is not *current*,
     it is *standing*. The accent is spent on the count. */
  .hv-sidebar__inbox {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-2);
    padding: var(--space-2);
    min-height: var(--control-height);
    border-radius: var(--radius-md);
    background: var(--rail-bg-active);
    color: var(--rail-fg);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    text-decoration: none;
  }

  .hv-sidebar__inbox:hover {
    background: var(--rail-bg-hover);
  }

  /* The number is the reason the row exists, so it is the one place in
     the rail the accent is spent. `--rail-accent` against
     `--fg-on-accent` is the pair `design/contrast.md` already holds a
     row for. */
  .hv-sidebar__count {
    flex: none;
    min-width: var(--space-5);
    padding: 0 var(--space-2);
    border-radius: var(--radius-full);
    background: var(--rail-accent);
    color: var(--fg-on-accent);
    font-size: var(--text-xs);
    font-weight: var(--weight-bold);
    font-variant-numeric: tabular-nums;
    text-align: center;
    line-height: var(--space-5);
  }

  /* `min-height: 0` is what makes this actually scroll: without it the
     flex item's automatic minimum is its content height and the rail
     grows instead of the list scrolling. */
  .hv-sidebar__items {
    display: flex;
    flex-direction: column;
    gap: var(--space-1);
    flex: auto;
    min-height: 0;
    overflow-y: auto;
    scrollbar-width: thin;
  }

  /* T-0908: the rail's recency headings (Today · Previous 7 days ·
     Older). Sticky, because the list scrolls and the heading a person
     is reading under is the one thing that must not scroll away from
     the rows it names. `--rail-bg` behind it so rows do not show
     through; no new token, no new colour. */
  .hv-sidebar__group {
    position: sticky;
    top: 0;
    z-index: 1;
    margin: 0;
    padding: var(--space-1) var(--space-2);
    background: var(--rail-bg);
    font-size: var(--text-xs);
    color: var(--rail-fg-muted);
  }

  .hv-sidebar__empty {
    margin: 0;
    padding: var(--space-2);
    font-size: var(--text-sm);
    color: var(--rail-fg-muted);
  }

  /* The marker is drawn on the rail's own fill, at the edge, and never
     inside the active row's fill: `--accent` measures 3.15 : 1 against
     `--rail-bg` and 2.50 : 1 against `--rail-bg-active`, so the second
     placement would be below the 3.00 graphical floor. `padding-box`
     stops the row's own fill at the padding edge, which leaves the 2px
     `border-inline-start` `hv-row` already declares painted directly on
     the rail. */
  .hv-sidebar .hv-row {
    background-clip: padding-box;
  }

  /* **Two lines, not one truncated one.** The label used to be cut to
     42 characters in Rust before it ever reached a stylesheet, which
     made "A word frequency counter for the command line." into "A word
     frequency counter for the command l" — in the rail, in the crumb,
     and as the object header of six pages. The full text is in the DOM
     now and the *drawing* is what stops: two lines here, an ellipsis
     in the crumb, and the whole sentence everywhere a heading has room
     for it. */
  .hv-sidebar .hv-row__label {
    flex: auto;
    min-width: 0;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
    line-height: var(--leading-ui);
  }

  /* What this project is waiting on, or what it is doing. Mono because
     it is a count and counts line up; `--rail-fg-subtle` because it is
     the least important thing in the row until it is an ask. */
  .hv-sidebar .hv-row__note {
    flex: none;
    align-self: center;
    font-family: var(--font-mono);
    font-size: var(--text-xs);
    font-variant-numeric: tabular-nums;
    color: var(--rail-fg-subtle);
  }

  /* An ask is the one thing in the rail that is allowed to raise its
     voice. `--warning` on the dark rail ramp is the same hue the status
     strip and the badge use for "waiting for you", so a person learns
     it once. */
  .hv-sidebar .hv-row__note--ask {
    color: var(--warning);
    font-weight: var(--weight-medium);
  }

  /* ------------------------------------------------------------------
   * The rail's second level — a project's own open conversations
   * ------------------------------------------------------------------ */

  /* **Why the rail became a tree.** It was a flat list of projects, and
     each row was labelled by the first thing said in the project's
     interview and led straight to that interview — so it read as a list
     of conversations while claiming to be a list of projects, and a
     project's *other* conversations existed nowhere in the navigation.

     The row is the project now and leads to its conversations; these
     are the conversations, and they lead to themselves.

     Drawn only under the project a person is inside: a dozen projects
     with every conversation expanded is a rail nobody can find a
     project in. */
  .hv-subrows {
    display: flex;
    flex-direction: column;
    gap: 0;
    margin: 0 0 var(--space-1);
    padding: 0;
    list-style: none;
  }

  /* The indent is a rule, not a margin. A child list that is only
     shifted right is a child list whose relationship to its parent has
     to be inferred from an alignment; the line makes the branch
     visible, and it is the one the active row lights up. */
  .hv-subrow {
    display: block;
    margin-inline-start: var(--space-4);
    padding: var(--space-1) var(--space-2);
    border-inline-start: var(--border-width) solid var(--rail-border);
    color: var(--rail-fg-muted);
    font-size: var(--text-sm);
    line-height: var(--leading-ui);
    text-decoration: none;
    overflow: hidden;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }

  .hv-subrow:hover {
    color: var(--rail-fg);
    border-inline-start-color: var(--rail-fg-muted);
  }

  /* The conversation being read. The accent on the branch line, which
     is the same violet the active project's own marker uses — one
     colour meaning "this is where you are", at two levels. */
  .hv-subrow--active {
    color: var(--rail-fg);
    border-inline-start-color: var(--rail-accent);
    border-inline-start-width: var(--border-width-emphasis);
    background: var(--rail-bg-active);
  }

  /* Filled or hollow, never hue alone — and never the status ramp.
     Since T-0907 this is C4's *whole* visual answer for a rail row: the
     word that used to sit beside it renders into an
     `hv-visually-hidden` span, so it is read aloud and not drawn. That
     is sound because the difference below is a shape, not a hue.
     `design/contrast.md`'s rail rows put a status dot on the *dark*
     ramp's `--success` / `--warning` / `--danger` in both themes, and
     DK-12's eight rail tokens hold no name for that, so pinning it would
     mean either a palette reference outside `tokens.css` (DK-07) or
     three new rail colour tokens. Shape carries the difference instead,
     on a ramp that is already cleared: `--rail-fg-muted` measures
     6.51 – 7.39 : 1 against every rail fill a row can have. */
  .hv-sidebar .hv-row__dot {
    flex: none;
    width: var(--space-2);
    height: var(--space-2);
    border: var(--border-width) solid var(--rail-fg-muted);
    border-radius: var(--radius-full);
  }

  .hv-sidebar .hv-row__dot--finalized {
    background: var(--rail-fg-muted);
  }

  /* Running: filled in the rail's own accent, with a halo. The third
     shape, and the only animated element on the platform — see the
     reduced-motion block at the foot of this sheet, which removes the
     animation and leaves the halo, so the state survives as a shape
     for anyone who has asked for less movement. */
  .hv-sidebar .hv-row__dot--running {
    background: var(--rail-accent);
    border-color: var(--rail-accent);
    box-shadow: 0 0 0 var(--border-width-emphasis) var(--rail-bg-active);
  }

  /* ==================================================================
   * The app bar — the only place that answers "where am I, as whom,
   * in which tenant" (DK-24)
   * ================================================================== */

  /* One bar, not two. The current interview page stacks `.project-nav`
     on `.chat-header`, both on the same fill, and `legacy.css` carries a
     comment explaining that the first one's `border-bottom` was removed
     because two near-touching divider lines "read as a stray artifact".
     That comment is the design saying it had one bar too many. */
  .hv-appbar {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-4);
    min-height: var(--control-height-lg);
    /* Vertical padding as well as horizontal: the bar used to be
       exactly one control tall, which put the crumb chain and the
       identity chip hard against both edges and made the whole top of
       every page read as a toolbar rather than as chrome. */
    padding: var(--space-3) var(--space-5);
    background: var(--surface-default);
    border-block-end: var(--border-width) solid var(--border-subtle);
  }

  /* The rail owns the wordmark at every width where the rail is
     visible. This is the same mark, in the app bar, for the one width
     where it is not — shown by the compact block below and hidden
     here, rather than the other way round, so the default is the
     arrangement most viewports get. */
  .hv-appbar__brand {
    display: none;
  }

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

  /* ---------- crumbs ---------- */

  .hv-crumbs {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
    font-size: var(--text-sm);
  }

  .hv-crumbs__step {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--fg-muted);
    text-decoration: none;
  }

  a.hv-crumbs__step:hover {
    color: var(--fg-default);
    text-decoration: underline;
  }

  .hv-crumbs__sep {
    flex: none;
    color: var(--fg-subtle);
  }

  /* The terminal crumb is the answer to "where am I", so it is the one
     element in this row that never shrinks and never truncates. */
  .hv-crumbs__step--current {
    flex: none;
    color: var(--fg-default);
    font-weight: var(--weight-medium);
  }

  /* ---------- the place row ---------- */

  /* Underline, not pill. A solid `--accent` fill at `--text-md` reads as
     a button, and the four places are not peers of the one action on
     the page that cannot be undone.

     **One band, one line.** The app bar and this strip are the same
     chrome surface, and the line belongs under the *pair* — the app
     bar drops its own whenever a strip follows it, so a page never
     draws two rules within 40px, which is the artifact this shell
     exists to remove. That single line is also what the active place's
     2px underline sits on, which is what makes a tab read as attached
     to the content below it rather than floating above it. Where
     `:has()` is unavailable both lines are drawn: quieter than
     correct, never wrong.

     `--space-5`, matching the app bar's own inline padding, so the
     first place and the crumb chain above it share a left edge.
     `<main>` keeps its wider `--space-6`: it is the page, not the
     chrome. */
  /* `flex: none` is load-bearing, not tidiness. This strip is a flex
     item of `.hv-pane` *and* a scroll container (`overflow-x: auto`),
     and a scroll container's automatic minimum size is zero rather than
     its content — so on the one page where the pane's height is capped
     (`.hv-shell:has(.hv-main--conversation)`, which is what keeps the
     composer on screen) it was squeezed to nothing. The four places a
     project has vanished from the only page a project opens on, which
     made Spec, Work and Overview unreachable by any link. */
  .hv-tabs {
    display: flex;
    flex: none;
    gap: var(--space-4);
    padding-inline: var(--space-5);
    background: var(--surface-default);
    border-block-end: var(--border-width) solid var(--border-subtle);
    overflow-x: auto;
    scrollbar-width: thin;
  }

  /* The bar above it, for the same reason and pre-emptively: it is not
     a scroll container today, so it does not collapse today. It is a
     flex item of a height-capped column all the same, and a person who
     later gives it `overflow` should not have to rediscover this. */
  .hv-appbar {
    flex: none;
  }

  .hv-appbar:has(+ .hv-tabs) {
    border-block-end: 0;
  }

  .hv-tabs__tab {
    display: inline-flex;
    align-items: center;
    flex: none;
    min-height: var(--control-height-lg);
    border-block-end: var(--border-width-emphasis) solid transparent;
    color: var(--fg-muted);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
    text-decoration: none;
    white-space: nowrap;
  }

  .hv-tabs__tab:hover {
    color: var(--fg-default);
  }

  /* The active place's underline overlaps the band's own line rather
     than sitting above it — a 2px mark on a 1px rule, one edge, not
     two stacked. */
  .hv-tabs__tab--active {
    margin-block-end: calc(-1 * var(--border-width));
    border-block-end-color: var(--accent);
    color: var(--fg-default);
  }

  /* ---------- identity (DK-24a) ---------- */

  /* Two names, never an identifier. The current `.sidebar-identity`
     renders a raw UUID at 0.7rem in `--sidebar-text-faint` (3.59 : 1) —
     two problems in one element, and both go. */
  .hv-identity {
    display: flex;
    align-items: center;
    gap: var(--space-2);
    min-width: 0;
  }

  .hv-identity__avatar {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    flex: none;
    width: var(--control-height);
    height: var(--control-height);
    border-radius: var(--radius-full);
    background: var(--surface-sunken);
    color: var(--fg-default);
    font-size: var(--text-xs);
    font-weight: var(--weight-medium);
  }

  .hv-identity__names {
    display: flex;
    flex-direction: column;
    min-width: 0;
    line-height: var(--leading-ui);
  }

  .hv-identity__company,
  .hv-identity__person {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-size: var(--text-sm);
  }

  /* The company sits above the person because identity is scoped *by*
     tenant — which is the whole reason this element moved out of the
     rail and into the bar (DK-24). */
  .hv-identity__company {
    color: var(--fg-muted);
  }

  .hv-identity__person {
    color: var(--fg-default);
  }

  /* ==================================================================
   * The console band — the admin console's persistent indicator (DK-27)
   * ================================================================== */

  /* Warning-toned, never danger. Being in the console is consequential,
     not wrong, and `hv-notice--danger` appears on these same pages when
     something has actually failed — a permanent danger band would make
     that one unreadable. */
  .hv-console-band {
    margin: 0;
    padding: var(--space-2) var(--space-4);
    background: var(--surface-warning-soft);
    border-block-end: var(--border-width) solid var(--border-status-warning);
    color: var(--warning);
    font-size: var(--text-sm);
    font-weight: var(--weight-medium);
  }

  /* ==================================================================
   * The rail as a disclosure (DK-29, T-0203)
   * ================================================================== */

  /* The rail's markup is emitted **once**, by `Chrome::render`, and
     appears in two layouts by CSS alone. `<details>` needs no signal, no
     scrim, no outside-click handler, no `Escape` handler, no focus trap,
     no `aria-expanded` bookkeeping and no `inert` on the background —
     the element already implements every one of them, correctly, and
     before the Datastar bundle has loaded. Rendering the rail twice is
     the obvious alternative and means two subtrees claiming to be the
     same navigation, two sets of ids, and a stale one.

     Below the breakpoint this needs no rule at all: it is a `<details>`,
     and a `<details>` already discloses. Only the summary's appearance
     is stated here. */
  .hv-rail-disclosure__summary {
    min-height: var(--control-height-lg);
    padding: var(--space-2) var(--space-4);
    background: var(--rail-bg);
    border-block-end: var(--border-width) solid var(--rail-border);
    color: var(--rail-fg);
    font-size: var(--text-md);
    font-weight: var(--weight-medium);
  }

  /* ==================================================================
   * At and below `--bp-compact` — 48rem, which is 768 CSS px at the
   * default root size
   * ================================================================== */

  @media (width <= 48rem) {
    /* **The strip stays one line and scrolls sideways.**

       It stacked here first, one fact per line, on the reasoning that a
       divider to the right of a full-width row points at nothing. True,
       and it cost 148px of a 760px screen — a fifth of a phone, spent
       on a band of metadata, which then squeezed the conversation
       column until the composer fell off the bottom of the page.

       A strip is a strip. It keeps its dividers, it keeps its height,
       and what does not fit scrolls — the same treatment a wide table
       gets (Requirements J1), and for the same reason: the band
       overflows inside itself so the document never does. Clipping
       mid-fact is also the honest signal that there is more. */
    .hv-status-strip {
      flex-wrap: nowrap;
      overflow-x: auto;
      scrollbar-width: none;
    }

    /* **The column scrolls when its own furniture will not fit.**
       A phone gives the conversation about 500px once the app bar, the
       tabs and the strip have taken theirs, and on a finalized project
       the header and the read-only notice can take more than that
       between them. `overflow: hidden` then clips the composer off the
       bottom of a page that cannot scroll to reach it — a box you
       cannot type into.

       So down here the column scrolls itself. The transcript still
       takes the free space and still scrolls first; the column only
       moves when there is no free space to give, and the app bar, tabs
       and strip stay where they are either way. */
    .hv-main--conversation {
      overflow-y: auto;
    }


    /* One column, and two rows sized explicitly. Left to `auto auto`,
       grid's default `align-content: stretch` would hand the closed
       disclosure half the viewport. */
    .hv-shell {
      grid-template-columns: 1fr;
      /* `minmax(0, 1fr)`, not `1fr`, for the same reason the wide
         layout declares its single row that way: a `1fr` row still
         takes its *automatic minimum* from its content, so the pane
         grew past a shell that had been told to be exactly 100dvh and
         the document scrolled by the difference. Two rows here — the
         rail's own disclosure summary, then the pane. */
      grid-template-rows: auto minmax(0, 1fr);
    }

    /* `--control-height` is 32px, which clears WCAG 2.5.8's 24 × 24
       minimum with a mouse and is not comfortable with a thumb. One
       declaration steps the whole control scale to 40px below this
       width — `hv-row`'s `min-height`, the tab targets, the rail's
       create action, every field on every form. Re-pointing the token is
       what reaches `hv-row`, which is styled in `@layer components`,
       above this layer. */
    /* The rail is a closed disclosure here, and the wordmark is inside
       it. Without this the narrowest viewport is the only one that
       never names the product. */
    .hv-appbar__brand {
      display: flex;
      align-items: center;
    }

    /* And with the mark showing, the chain's root crumb says "Haive"
       one centimetre from a wordmark reading Haive, linking to the same
       place. The mark is the way home here; the crumb and the
       separator after it go, and the chain starts at the first thing
       that is actually a location. */
    .hv-appbar__brand + .hv-crumbs > .hv-crumbs__step:first-child,
    .hv-appbar__brand + .hv-crumbs > .hv-crumbs__step:first-child + .hv-crumbs__sep {
      display: none;
    }

    .hv-shell,
    .hv-main--centered {
      --control-height: var(--control-height-lg);
    }

    /* The rail is a full-width band above the pane now, not a column
       beside it, so its edge is the one underneath it. */
    .hv-sidebar {
      block-size: auto;
      border-inline-end: 0;
      border-block-end: var(--border-width) solid var(--rail-border);
    }

    /* Crumbs and identity stop competing for one line and stack. Neither
       is dropped: the crumbs answer "where am I" and the identity
       answers "as whom, in which tenant", and Requirements J3 forbids
       making either unreachable at any width. */
    .hv-appbar {
      flex-wrap: wrap;
      padding-block: var(--space-2);
    }

    /* Both mains, named rather than caught: `--space-6` of page padding
       either side of a 360px viewport spends 64 of 360 px on nothing. */
    .hv-main,
    .hv-main--centered {
      padding: var(--space-4);
    }

    /* **The middle collapses; the first and last crumbs always
       survive.** Truncating the last one defeats the purpose — it is the
       answer to "where am I". `Location` renders `step, sep, step, …,
       sep, step`, so the four children kept are the root, the separator
       after it, the separator before the current page, and the current
       page itself. With two crumbs (three children) every child is one
       of those four and nothing collapses.

       `display: none` also takes the collapsed crumbs out of the
       accessibility tree, and that is the deliberate half of the choice:
       the alternative — collapsing them to zero width — leaves a
       keyboard user tabbing into links they cannot see, which is worse
       than not having them. Nothing becomes a dead end (Requirements
       G2): the root crumb, the rail's disclosure and the tab row all
       still carry hrefs at this width. */
    .hv-crumbs > :not(:first-child):not(:nth-child(2)):not(:nth-last-child(2)):not(:last-child) {
      display: none;
    }

    /* An ellipsis, so a collapsed chain does not read as the whole
       chain. Five children is three crumbs, which is the shortest chain
       that loses anything. If `:has()` is unavailable the rule is
       dropped and the two separators simply sit together — quieter than
       correct, never wrong. */
    .hv-crumbs:has(> :nth-child(5)) > :nth-child(2)::after {
      content: '…';
      margin-inline-start: var(--space-2);
    }
  }

  /* ==================================================================
   * Above `--bp-compact` — the disclosure is not a disclosure
   * ================================================================== */

  @media (width > 48rem) {
    /* `display: contents` removes the wrapper's box, so the browser lays
       the rail out exactly as if the `<details>` were not there: the
       `<aside>` becomes the shell's first grid item. It also removes the
       wrapper from the accessibility tree in some engines, which is
       precisely right here — above this width it is not a disclosure,
       and nothing should announce it as one. Below the breakpoint it is
       a real `<details>` again, with all of its semantics. */
    .hv-rail-disclosure {
      display: contents;
    }

    .hv-rail-disclosure__summary {
      display: none;
    }

    /* The second half of the same statement, and the reason it is a
       separate rule: a `<details>` hides its content when it is closed,
       and above this width there is no open/closed — the rail is simply
       there. `::details-content` is the pseudo-element the HTML
       rendering spec hides for a closed `<details>`; overriding its
       `content-visibility` is the declared way to say "not here".
       Kept in its own rule so that an engine which does not know the
       selector drops only this rule and keeps the two above. */
    .hv-rail-disclosure::details-content {
      content-visibility: visible;
    }
  }

  /* ==================================================================
   * Above `--bp-wide` — 90rem, 1440 CSS px at the default root size —
   * grow or centre (DK-29)
   * ================================================================== */

  @media (width > 90rem) {
    /* `<main>` stops growing and centres; the rail does not stretch,
       because its track is a fixed `--rail-width` and only the `1fr`
       column absorbs the extra. `width: 100%` is load-bearing: a flex
       item with an `auto` cross size and an `auto` cross margin is
       sized by `fit-content` instead of stretching, so the cap would
       never bind.

       `.hv-pane >`, not a bare `.hv-main`: the signed-out column is
       also an `.hv-main`, it is already `min(var(--measure), 100%)`
       wide, and a bare selector here — later in the file, at equal
       specificity — would silently widen the sign-in screen to
       `--content-max` on a large display. */
    .hv-pane > .hv-main {
      width: 100%;
      max-width: var(--content-max);
      margin-inline: auto;
    }
  }

  /* ==================================================================
   * Motion (Requirements C7, I1)
   * ================================================================== */

  /* Every transition in this file is inside this one block, so
     `prefers-reduced-motion: reduce` — the media query's "otherwise"
     branch — removes all of it. Nothing here is needed to reach a
     usable final state: each only softens a colour change that has
     already happened. */
  @media (prefers-reduced-motion: no-preference) {
    .hv-crumbs__step,
    .hv-tabs__tab,
    .hv-rail-disclosure__summary {
      transition:
        color var(--motion-fast) var(--ease-out),
        background-color var(--motion-fast) var(--ease-out),
        border-color var(--motion-fast) var(--ease-out);
    }
  }
}
