/* ════════════════════════════════════════════════════
   spaces-tree — presentation for the shared Spaces tree
   ════════════════════════════════════════════════════

   A NAMESPACE OF ITS OWN, on purpose. The rules below started as a mechanical
   copy of the `.lp-scope-tree` block that used to live in
   controls/sidebar-nav.css, with two substitutions: the namespace, and
   `--chat` → `--item` for the content-row kind (the component uses one
   app-neutral row kind, so this file never has to know what an app calls its
   content).

   It was a copy rather than an edit because `.lp-scope-tree` was applied by
   Signal's hand-rolled sidebar, and editing it to suit this component would
   have restyled Signal without Signal being touched. D-1 froze it until
   Signal migrated and deleted it once it had no consumers left; Signal
   migrated in P5.3 (SIGNAL-SPACES-INTEGRATION-ARC.md) and the block is gone,
   so this namespace is now the only one — edit it freely.
   See docs/plans/SHARED-SPACES-TREE-ARC.md — D-1, and §5 for the blast radius.

   Reaching this tree unchanged from global rules, deliberately not copied:
   `.lp-scope-avatar*` (the identity avatar control) and
   `.ch-tree-children > .lp-sidebar-inline-create`. Both live in
   controls/sidebar-nav.css. The structure-create buttons
   (`.ch-tree-item-create`, `.ch-tree-section-action`) DO get scoped overrides
   at the foot of this file — see the note there for why.

   controls/shell-controls.css reaches NOTHING here, however much its
   `.ch-tree-*` rules look like they would: index.html loads sidebar-nav.css
   and this file, and shell-controls.css only ever loads in the controls-lab
   pages. So its base row styling — including `cursor: pointer` on every row —
   has never applied in the app, and anything this tree needs from that family
   has to be stated here. That is not a rule to fix by adding the link: the
   file is the LAB's copy of a tree that predates this component, and pulling
   it in would restyle rows this stylesheet already owns.
*/

/* ── The sidebar caption + edge contract ────────────────────────────
   Two surfaces share the panel and have to draw its furniture the same:
   this tree, and Chat's Recent list (css/apps/chat-features.css). They
   are different components in different files, and every round of
   matching them by restating numbers has drifted — the caption size, the
   row indent, the menu's right offset in turn.

   So the numbers live once, HERE and on :root, because index.html links
   this file and nothing else that both can read. chat-features.css
   consumes them; neither file owns a second copy.

   Values, not a treatment: the two captions differ in what they wrap
   (one carries a menu button, the other is a bare heading), so each still
   writes its own rule. What they must not do is disagree about 11px. */
:root {
    --lp-sidebar-caption-size: 11px;
    --lp-sidebar-caption-weight: 700;
    --lp-sidebar-caption-tracking: 0.08em;
    /* The caption's own indent, and the rows' — measured from the inside
       of the panel's 8px padding, which both blocks render within. */
    --lp-sidebar-caption-indent: 12px;
    --lp-sidebar-edge-pad: 7px;
    /* One box height for a caption, so its text sits centred with equal
       air above and below rather than being pushed off a top padding. */
    --lp-sidebar-caption-height: 24px;
    /* One ROW box and gap for every tree in a sidebar, not just this one.
       Promoted to :root because Chaos stacks two flat trees (Favorites,
       Recent) directly above the Spaces tree, and those were plain
       `.ch-tree-item`s at 5px/6px against this tree's 32px/7px — three row
       heights in one column. A token they can all read is what stops the
       three drifting apart again. The tree's own --spaces-tree-* below
       derive from these rather than restating them. */
    --lp-sidebar-row-height: 32px;
    --lp-sidebar-row-gap: 7px;
    --lp-sidebar-row-pad: 5px 7px;
    /* The air BETWEEN blocks, which is a margin rather than the caption's
       padding — padding would make the first block's caption taller than
       the panel's own top edge allows for. */
    --lp-sidebar-caption-lead: 10px;
}

/* Shared compact Space → Project → content presentation. */
/* Geometry of the three-dot menu glyph, drawn at the foot of this file, and
   the one place it is stated. Both roots carry it because the tree's header is
   a SIBLING of the tree rather than a descendant, so a property declared on
   the tree alone would never reach the header's own button — which is exactly
   how the two came to be drawn by different means, one an SVG at 16px and one
   these dots at 2px. Restating the values in the header rule would have fixed
   the look and left them free to drift; this cannot. */
.lp-spaces-tree,
.lp-spaces-tree-header {
    --spaces-tree-dot-size: 2px;
    --spaces-tree-dot-gap: 4px;

    /* S-03. The trailing slot's width: the box the count takes and the box the
       menu button takes, which are the same box. ONE value because the row
       hover now slides the count LEFT by exactly the button's width to make
       room beside it — if the two ever disagreed the count would land on the
       dots or leave a gap.

       S-55: MOVED HERE FROM `.lp-spaces-tree` ALONE, which is the drift the
       comment below names — "that sibling relationship is how every drift in
       this file so far has started" — arriving on a token that paragraph did
       not cover. `.lp-spaces-tree-header-menu` sizes itself from this var, the
       header is a SIBLING of the tree, so the var resolved to nothing and the
       button computed to 0x0: visible, enabled, focusable, and unclickable.
       Mouse users could not open the All Spaces menu at all while keyboard
       users could, because a zero-size button still takes focus and Enter.
       Measured in a real browser by injection, since jsdom has no layout and
       computes no rect, which is why no unit test could have held this:
           token at header (unset), at tree 22px
           button rect 0x0 -> inject the token on the header -> 22x22 -> remove
           it -> 0x0 again. */
    --spaces-tree-trailing-slot: 22px;

    /* ONE size for every word this tree puts on screen: "ALL SPACES", a Space,
       a Project, a folder, and whatever the app puts under them. The reference
       distinguishes them by weight and case, never by size, and a column that
       is one list should not read as several.

       Declared for both roots for the same reason as the dots above — the
       header is a SIBLING of the tree, so a token on the tree alone never
       reaches it. That sibling relationship is how every drift in this file so
       far has started. */
    --spaces-tree-label: 13.5px;

    /* The same size, corrected for being set in capitals.
       Type at one px value does not read as one size when the case differs:
       capitals fill the whole cap-height where a mixed-case word only fills
       its x-height, so "ALL SPACES" at 13.5px sits visibly larger than
       "Default" at 13.5px, and the letterspacing below widens it further.
       Matching the numbers is what produced the complaint this corrects.
       A factor of the shared token rather than a number of its own: there is
       still one size in this file, and this says out loud that it is that size
       adjusted for case, so changing the size carries the caption with it. */
    /* A STATED size, not one derived from the label above it. It used to be
       calc(label * 0.88), so the caption tracked the rows — which was right
       while this tree was the only thing wearing it. Chat's Recent list now
       carries the same caption treatment on .chat-sb-date-header, and that
       rule cannot see this token: deriving here would let the two drift apart
       the moment the label changed, which is the drift the derivation existed
       to prevent, one level up. The css-contract suites pin both sides. */
    --spaces-tree-caption-label: var(--lp-sidebar-caption-size);
}

.lp-spaces-tree {
    display: grid;
    min-width: 0;
    /* Between Space blocks, not between rows: the reference groups each Space
       with its contents and leaves air before the next one, which is what makes
       the tree read as several lists rather than one long one. Row spacing is
       the rows' own margin, further down. */
    gap: 10px;
    color: var(--text-secondary);
    font-size: var(--spaces-tree-label);

    /* Row gaps, shared with the negative margin that tucks the chevron into
       the glyph beside it — the two must agree or the arrow drifts off it. */
    --spaces-tree-header-gap: 6px;
    /* Derived, not restated — the same gap every tree in a sidebar uses. */
    --spaces-tree-row-gap: var(--lp-sidebar-row-gap, 7px);

    /* The glyph column: one width for all three kinds — a Space's initials, a
       Project's mark, a folder's shut/open icon — so they stack in a single
       column whatever a row holds. It also centres the collapse arrow, and the
       guide line under a scope is drawn from it, so this one number decides
       where the whole tree lines up.

       It OVERRIDES the 18px the avatar control ships with
       (controls/sidebar-nav.css) rather than describing it: the override is at
       the foot of this section, and a folder icon — which has no width at all
       in the app, its only sizing rule living in the shell-controls.css copy
       nothing loads — is given the same box there. */
    --spaces-tree-glyph: 22px;
    --spaces-tree-initials: 13px;


    /* The collapse arrow, drawn as two borders on a rotated square: the box is
       the square, the stroke is those borders. Sized with the glyph it lands
       in — an arrow that stayed put while the box around it grew would read as
       a smaller arrow, which is what it would be. */
    --spaces-tree-arrow: 6px;
    --spaces-tree-arrow-stroke: 1.75px;

    /* Where a row's content starts, per container. The guide line is drawn at
       pad + glyph/2 — the centre of the glyph the line descends from — so
       these must match what actually indents a row: the header's own padding
       for a section, and the engine's basePadPx (passed at the mount, not
       inherited) for a row. */
    /* The rows' distance from the panel edge, which Recent's rows take too.
       One value on :root, so the two surfaces cannot line up differently. */
    --spaces-tree-header-pad: var(--lp-sidebar-edge-pad);
    --spaces-tree-row-pad: 12px;

    /* The Project mark a Project avatar shows in place of initials. Traced from
       the same 'project' entry in controls/icon.js that the Project menu item
       uses, so the row and its menu carry one mark. Stroked, not filled, and
       black only because a mask reads alpha — the colour comes from
       currentColor at the use site. */
    --spaces-tree-project-mark:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 2h10'/%3E%3Cpath d='M5 6h14'/%3E%3Crect width='18' height='12' x='3' y='10' rx='2'/%3E%3C/svg%3E");
    --spaces-tree-project-mark-size: 15px;

    /* A folder shows its own state: shut when collapsed, open when not. Both
       traced in the house icon style, the closed one matching the 'folder'
       entry in controls/icon.js so a folder reads the same here as anywhere. */
    --spaces-tree-folder-shut:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M22 19a2 2 0 01-2 2H4a2 2 0 01-2-2V5a2 2 0 012-2h5l2 3h9a2 2 0 012 2z'/%3E%3C/svg%3E");
    --spaces-tree-folder-open:
        url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 14 1.45-2.9A2 2 0 019.24 10H20a2 2 0 011.94 2.5l-1.55 6a2 2 0 01-1.94 1.5H4a2 2 0 01-2-2V5a2 2 0 012-2h3.9a2 2 0 011.69.9l.81 1.2a2 2 0 001.67.9H18a2 2 0 012 2v2'/%3E%3C/svg%3E");
    --spaces-tree-folder-mark-size: 16px;
}

.lp-spaces-tree .ch-tree-section {
    min-width: 0;
    margin: 0;
}

.lp-spaces-tree .ch-tree-section-header {
    display: flex;
    min-width: 0;
    min-height: calc(var(--spaces-tree-glyph) + 10px);
    align-items: center;
    gap: var(--spaces-tree-header-gap);
    padding: 6px var(--spaces-tree-header-pad) 5px;
    border-radius: var(--radius-md, 8px);
    color: var(--text-secondary);

    /* Every one of these is DECLARED, and that is the point.
       .ch-tree-section-header carries a caption treatment in the shared
       shell-controls.css — 10px, 600, uppercase, 0.5px letterspacing — which
       is right for the engine's own idiom, where a section header names a group
       of rows. Here it is not a caption: it is the outermost scope, and it
       carries a name somebody typed.
       This rule used to say nothing about type, on the reasoning that saying
       nothing left it matching the rows. It does not: silence inherits the
       base rule, so a Space read as a 10px uppercase caption for as long as
       that comment claimed otherwise. Nothing here may be removed on the
       grounds that it looks redundant. */
    font-size: var(--spaces-tree-label);
    font-weight: 600;
    letter-spacing: normal;
    text-transform: none;
}

/* The same lift a row's text gets. It used to go muted → secondary, which was
   the row's RESTING colour: hovering a Space brightened it to where a Project
   already sat. */
.lp-spaces-tree .ch-tree-section-header:hover {
    color: var(--text-primary);
}

.lp-spaces-tree .ch-tree-section-label {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* ── The Space the reader entered this view through ──
   A dot rather than a bolder label: weight is already spent on the section
   heading itself, and a second weight step doesn't read as "you are here". */
.lp-spaces-tree .ch-tree-section-dot {
    flex: 0 0 auto;
    width: 6px;
    height: 6px;
    margin-left: 2px;
    border-radius: 50%;
    background: var(--accent, #4c8dff);
}

.lp-spaces-tree .ch-tree-chevron,
.lp-spaces-tree .ch-tree-item-chevron {
    display: inline-block;
    width: calc(var(--spaces-tree-arrow) + 4px);
    height: calc(var(--spaces-tree-arrow) + 4px);
    flex: 0 0 calc(var(--spaces-tree-arrow) + 4px);
    overflow: hidden;
    color: transparent;
    font-size: 0;
}

.lp-spaces-tree .ch-tree-chevron::before,
.lp-spaces-tree .ch-tree-item-chevron::before {
    content: "";
    display: block;
    width: var(--spaces-tree-arrow);
    height: var(--spaces-tree-arrow);
    margin: 1px;
    border-right: var(--spaces-tree-arrow-stroke) solid var(--text-muted);
    border-bottom: var(--spaces-tree-arrow-stroke) solid var(--text-muted);
    transform: rotate(-45deg);
    transition: transform 150ms ease;
}

.lp-spaces-tree .ch-tree-chevron.open::before,
.lp-spaces-tree .ch-tree-item-chevron.open::before {
    transform: rotate(45deg);
}

/* ── The chevron sits inside the glyph, not beside it ───────────────────
   Hovering a Space or Project's coloured initials fades them out and puts the
   arrow in their place; a folder's icon does the same. So the arrow costs no
   column of its own — which is the point of moving it in there.

   The chevron cannot be reparented into the avatar: the tree engine builds
   both and is shared with Chaos and Signal, and `setActive()` rebuilds every
   row, so a DOM move would be undone by the first click on a structure row.
   Instead the chevron collapses to a zero-size box and its arrow — already a
   `::before` — is positioned over the glyph that follows it. Being relative to
   the chevron rather than the row is what makes this hold at every indent: the
   engine sets a per-depth padding this stylesheet cannot read.

   `pointer-events: none` is load-bearing. The arrow overlaps the glyph, so if
   it took the pointer the glyph would stop being hovered the instant the arrow
   appeared, and it would flicker.

   Applied only where there IS a glyph. identity is opt-in, so a row without an
   avatar keeps the arrow in its own column rather than losing it entirely. */
.lp-spaces-tree .ch-tree-section-header:has(> .lp-scope-avatar) > .ch-tree-chevron,
.lp-spaces-tree .ch-tree-item:has(> .lp-scope-avatar) > .ch-tree-item-chevron,
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-chevron {
    position: relative;
    /* Load-bearing. The arrow is drawn over the glyph that FOLLOWS it, and a
       Project avatar is positioned too (it draws its mark on a ::before). Two
       positioned siblings at z-index auto paint in DOM order, so the avatar —
       being later, and opaque — painted straight over the arrow: on a Project
       the mark vanished on hover and nothing replaced it, while a Space worked
       because its avatar is not positioned. Anything that gains a position in
       this row needs to stay under this. */
    z-index: 1;
    width: 0;
    height: 0;
    flex: 0 0 0;
    overflow: visible;
    opacity: 0;
    pointer-events: none;
    transition: opacity 120ms ease;
}

/* Cancelling the row's own gap is what puts the glyph where the arrow was. */
.lp-spaces-tree .ch-tree-section-header:has(> .lp-scope-avatar) > .ch-tree-chevron {
    margin-right: calc(-1 * var(--spaces-tree-header-gap));
}

.lp-spaces-tree .ch-tree-item:has(> .lp-scope-avatar) > .ch-tree-item-chevron,
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-chevron {
    margin-right: calc(-1 * var(--spaces-tree-row-gap));
}

.lp-spaces-tree .ch-tree-section-header:has(> .lp-scope-avatar) > .ch-tree-chevron::before,
.lp-spaces-tree .ch-tree-item:has(> .lp-scope-avatar) > .ch-tree-item-chevron::before,
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-chevron::before {
    position: absolute;
    top: 50%;
    left: calc(var(--spaces-tree-glyph) / 2);
    margin: 0;
    /* Over a tinted avatar the muted grey reads as smudge, not arrow. */
    border-right-color: var(--text-primary);
    border-bottom-color: var(--text-primary);
    transform: translate(-50%, -50%) rotate(-45deg);
}

.lp-spaces-tree .ch-tree-section-header:has(> .lp-scope-avatar) > .ch-tree-chevron.open::before,
.lp-spaces-tree .ch-tree-item:has(> .lp-scope-avatar) > .ch-tree-item-chevron.open::before,
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-chevron.open::before {
    transform: translate(-50%, -50%) rotate(45deg);
}

/* Anywhere on the row, not just the glyph. Aiming at an 18px square to find
   out whether a Space had anything in it was work; the whole row is the
   affordance, and the whole row is what a pointer is already over. Keyboard
   focus of the glyph counts as well, since hover alone puts this out of reach
   without a pointer.

   The children of a Space, Project or folder live in a SIBLING container, not
   inside the row — so hovering a chat inside a folder does not light up the
   folder's own arrow. */
.lp-spaces-tree .ch-tree-section-header:hover > .ch-tree-chevron,
.lp-spaces-tree .ch-tree-section-header:has(> .lp-scope-avatar:focus-visible) > .ch-tree-chevron,
.lp-spaces-tree .ch-tree-item:hover > .ch-tree-item-chevron,
.lp-spaces-tree .ch-tree-item:has(> .lp-scope-avatar:focus-visible) > .ch-tree-item-chevron {
    opacity: 1;
}

/* The initials or mark step aside on the same trigger, and ONLY where an arrow
   is going to take their place. The engine draws no chevron for a scope with
   nothing in it, so hiding the glyph unconditionally left an empty coloured box
   on every empty Space and Project — a hover that erased the only thing in the
   box and put nothing back. */
.lp-spaces-tree .ch-tree-section-header:has(> .ch-tree-chevron):hover > .lp-scope-avatar,
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-chevron):hover > .lp-scope-avatar,
.lp-spaces-tree .ch-tree-section-header:has(> .ch-tree-chevron) > .lp-scope-avatar:focus-visible,
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-chevron) > .lp-scope-avatar:focus-visible,
.lp-spaces-tree .ch-tree-item--folder:has(> .ch-tree-item-chevron):hover > .ch-tree-item-icon {
    color: transparent;
}

/* ── A Project shows its mark, a Space shows initials ──────────────────
   Keyed on the kind the shared avatar control stamps on the element, so only
   Project avatars change. A Space keeps its letters: there is one Space per
   name and the initials tell them apart, while every Project carries the same
   mark and is told apart by the row it sits on.

   Everything that makes the avatar look like itself is untouched — the tinted
   background, the border, the hover lift and the focus ring all come from the
   shared rules in sidebar-nav.css and are keyed on the element, not its
   contents. The mark is painted in currentColor, which on this element IS
   the configured Space/Project colour, so a recoloured Project recolours its
   mark with no extra rule.

   Absolutely positioned for the same reason the dots are: the avatar is a
   centring grid, and a text node beside a ::before would stack into two rows
   and push the mark off-centre. font-size: 0 collapses the initials the
   control wrote; hiding them with color alone would leave them taking width.

   On hover the mark disappears with the initials — the rule above sets
   color: transparent, and the mask paints in currentColor — which is what
   lets the collapse arrow have the box. */
/* The one column all three glyph kinds occupy. The avatar control ships 18px
   from controls/sidebar-nav.css and a folder icon ships no width at all — its
   only sizing rule is in the shell-controls.css copy this app never loads, so
   its box was whatever the hidden 14px <svg> inside it happened to be. Both
   are given the glyph token here, which is what puts initials, marks and icons
   in one column and centres the arrow on any of them.

   place-items keeps the contents centred as the box grows: the avatar is
   already a centring grid, the icon span was not one at all.

   Every icon, not only a folder's — an app that decorates its own rows
   (Signal puts an icon on each) would otherwise sit in a narrower column than
   the folders above it, and its labels would start somewhere else. */
.lp-spaces-tree .lp-scope-avatar,
.lp-spaces-tree .ch-tree-item-icon {
    display: inline-grid;
    width: var(--spaces-tree-glyph);
    height: var(--spaces-tree-glyph);
    flex: 0 0 var(--spaces-tree-glyph);
    place-items: center;
}

.lp-spaces-tree .lp-scope-avatar {
    /* Grown with the box, or the initials would swim in it. The radius follows
       too — 5px on an 18px square and 5px on a 22px one are not the same
       corner. */
    border-radius: 6px;
    /* The control sets --font-mono (controls/sidebar-nav.css). Two initials in
       a monospace face next to a label in the UI face read as two typefaces in
       one row, which is what this tree stopped doing: one family throughout,
       from the sidebar it sits in. The weight the control sets is kept — that
       is what makes two letters legible at this size, not the face. */
    font-family: inherit;
    font-size: var(--spaces-tree-initials);
}

.lp-spaces-tree .lp-scope-avatar[data-scope-kind="project"] {
    position: relative;
    font-size: 0;
}

.lp-spaces-tree .lp-scope-avatar[data-scope-kind="project"]::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--spaces-tree-project-mark-size);
    height: var(--spaces-tree-project-mark-size);
    transform: translate(-50%, -50%);
    background-color: currentColor;
    -webkit-mask: var(--spaces-tree-project-mark) center / contain no-repeat;
    mask: var(--spaces-tree-project-mark) center / contain no-repeat;
}

/* ── A folder shows shut or open ───────────────────────────────────────
   Replaces the arrow a folder used to borrow on hover: the state is readable
   without hovering at all, and in the place the eye is already looking.

   The engine writes an <svg> into this span and CSS cannot swap its path, so
   that is hidden and both states are drawn as masks — painted in currentColor,
   which keeps the folder the same colour it was. */
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-icon {
    position: relative;
}

/* The hidden <svg> would otherwise stretch the centring grid it sits in. */
/* Sized as well as hidden: it is still laid out, and at its authored 14px it
   would stretch the centring grid the mark is drawn in. */
.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-icon > svg {
    width: var(--spaces-tree-folder-mark-size);
    height: var(--spaces-tree-folder-mark-size);
    visibility: hidden;
}

.lp-spaces-tree .ch-tree-item--folder > .ch-tree-item-icon::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--spaces-tree-folder-mark-size);
    height: var(--spaces-tree-folder-mark-size);
    transform: translate(-50%, -50%);
    background-color: currentColor;
    -webkit-mask: var(--spaces-tree-folder-shut) center / contain no-repeat;
    mask: var(--spaces-tree-folder-shut) center / contain no-repeat;
}

/* Open when its own chevron says so — that class is the engine's record of
   the collapse state, and it stays correct through a re-render. */
.lp-spaces-tree .ch-tree-item--folder:has(> .ch-tree-item-chevron.open)
    > .ch-tree-item-icon::before {
    -webkit-mask-image: var(--spaces-tree-folder-open);
    mask-image: var(--spaces-tree-folder-open);
}

/* ── Row tone: how a row READS, not what it does ────────────────────────
   S-10(iii). The owner asked for the chat icon and colour to differ by mode,
   so a glance says which chats read their project and which do not.

   The tint is on the GLYPH, not the row. A whole row in accent colour competes
   with the active row and with the Space colours already in this tree; the
   icon is the part that already says "what kind of thing is this", and mode is
   a kind of thing.

   DEFINED HERE rather than in controls/shell-controls.css, which owns
   `.ch-tree-item` itself: Chat does not load that file — the note further down
   about the dots on row hover is the fourth restatement this file has needed
   for the same reason — and both mounts that need tone (Chat and Chaos) render
   through `.lp-spaces-tree`. The component's TREE_ROW_TONES is the list these
   two rules must cover. */
.lp-spaces-tree .ch-tree-item--tone-accent > .ch-tree-item-icon {
    color: var(--accent);
}

.lp-spaces-tree .ch-tree-item--tone-neutral > .ch-tree-item-icon {
    color: var(--text-muted);
}

/* ── The trailing slot: a count at rest, a menu on hover ────────────────
   One place at the end of a row, showing the count of what is inside until
   the row is hovered and then the button that opens its menu. Two elements
   sharing one slot, which is why the button leaves the flow: the engine
   appends them in a different order for a scope row (button, then badge) than
   for a content row (badge, then button), so anything in-flow would land the
   button 22px from where the count had been in one of the two cases, and the
   swap would read as a jump.

   Hidden with opacity rather than display: "display: none" takes the button
   out of the tab order, and the row menu is the only way to rename or delete a
   folder. "pointer-events" follows the opacity so an invisible button cannot
   be clicked — a click needs a hover, which reveals it, so this only closes a
   gap that a stray synthetic event could reach through.

   Moving the pointer onto the opened menu leaves the row, so the button fades
   and the count comes back behind the menu. That is what every sidebar of this
   shape does, and the alternative — keeping it visible while its menu is open —
   needs a state nothing here can report honestly: the confirm submenu tears its
   parent menu down through the path that deliberately skips onClose, so the
   flag would stick on. Focus is the one case worth holding, for a keyboard user
   who has tabbed to a button they cannot otherwise see.

   THREE states, not two, on owner instruction. Hovering the row used to swap
   the count for the dots, which meant the count vanished the moment you moved
   towards anything — including a row you were only passing over. So the row
   hover stopped swapping and the count stayed readable for the whole approach.

   S-03 kept the three states and gave the middle one something to SAY. It had
   made the button reachable while leaving it INVISIBLE, so on the way in there
   was still nothing to tell anyone a menu existed — the owner reported exactly
   that: a Project row showing '0', and dots that only appeared once the pointer
   was already on them. Now the row hover slides the count left by the button's
   width and fades the dots in beside it at a little over half strength; the
   slot hover takes them to full. Nothing is traded away in either state, and
   the count is readable throughout, including while the menu is being opened.

   Reachable-but-invisible is why pointer-events moves on row hover and opacity
   does not: the button must not intercept clicks at a row's right edge while it
   is not being offered, and outside a hover there is no pointer to offer it to.

   The right offset is the header pad rather than 4px so this column agrees
   with the section headers and the tree own header, which both sit at that
   distance — a 3px disagreement nobody sees until they look straight at it.
   It also lands the button exactly where the count sits at rest, which is what
   makes the slot one hover target instead of two — and, since S-03, what makes
   the count's leftward slide land it flush beside the dots rather than under
   them. The two distances are one value, --spaces-tree-trailing-slot. */
/* ── A Space's menu button ──────────────────────────────────────────────
   The engine gives a section action opacity: 1, so a Space's dots sat on
   every row at rest. Hover-only on owner instruction, which also matches what
   a Project and a folder already do.

   The trigger is the whole header rather than the name alone: the dots sit at
   the far end of it, so a rule keyed to the text would hide them the moment
   the pointer set off towards them. Focus is kept for a keyboard user, who has
   no hover to offer.

   No count to trade the slot with, unlike a row — a section header shows none —
   so this is the simpler two-state case. */
.lp-spaces-tree .ch-tree-section-action[data-create-action] {
    opacity: 0;
    pointer-events: none;
}

.lp-spaces-tree .ch-tree-section-header:hover
    > .ch-tree-section-action[data-create-action],
.lp-spaces-tree .ch-tree-section-action[data-create-action]:focus-visible,
.lp-spaces-tree .ch-tree-section-action[data-create-action][aria-expanded="true"] {
    opacity: 1;
    pointer-events: auto;
}

.lp-spaces-tree .ch-tree-item > .ch-tree-item-create,
.lp-spaces-tree .ch-tree-item > .ch-tree-item-action {
    position: absolute;
    top: 50%;
    right: var(--spaces-tree-header-pad);
    margin: 0;
    transform: translateY(-50%);
    opacity: 0;
}

/* S-03. WHAT THE MIDDLE STATE ACTUALLY DID, measured rather than read.
   The block above says a row hover leaves the button reachable and INVISIBLE.
   That is true of these rules alone and false of the stylesheet: the rule near
   the end of this file ("The dots become VISIBLE on row hover"), added because
   Chat does not load shell-controls.css, overrides it to opacity 1. Computed
   style on a hovered Project row in Chat confirms it — the dots were already
   fully visible.

   So the reported defect was never "the dots are invisible on the way in". It
   was that they arrive ON TOP OF THE COUNT: the button is absolutely positioned
   at exactly the count's place, so hovering a row put dots over the number and
   the slot read as one ambiguous thing.

   The fix is therefore the SLIDE alone, below — the count steps aside so the
   dots land beside it and both are legible at once. A fade-in at half strength
   was drafted here and REMOVED: it was dead (the later rule wins) and it left
   the stylesheet contradicting itself in two places.

   REJECTED SEPARATELY: making the count itself the button. A number that is a
   button is still not a menu, and it would have to stop being a count to look
   like one. */
.lp-spaces-tree .ch-tree-item > .ch-tree-item-create:hover,
.lp-spaces-tree .ch-tree-item > .ch-tree-item-action:hover,
.lp-spaces-tree .ch-tree-item > .ch-tree-item-create:focus-visible,
.lp-spaces-tree .ch-tree-item > .ch-tree-item-action:focus-visible {
    opacity: 1;
}

/* S-03. THE COUNT NO LONGER GIVES THE SLOT UP — it moves over. It used to go
   `visibility: hidden` the moment the pointer reached the slot, so reading the
   number and opening the menu were mutually exclusive; now both are on screen
   together and the count is still readable while the menu is being opened.

   The shift is keyed to the ROW, not the slot, so it happens on the approach
   rather than on arrival — that is what makes the dots appear beside a count
   that is already out of their way, instead of swapping under the pointer.

   Focus gets the same shift with no hover, for a keyboard user who has tabbed
   to a button they cannot otherwise see. */
.lp-spaces-tree .ch-tree-item > .ch-tree-item-badge {
    /* Short and on transform only: the slide should read as the row opening
       up, not as an animation anybody waits for. */
    transition: transform 120ms ease;
}

/* AND IT NEVER GIVES THE SLOT BACK (Ruling 142). The shift used to be keyed
   to hover and focus, which left the count sitting UNDER the invisible button
   at rest — the tree's version of the defect Chat's clock had, and the reason
   the tree needed `pointer-events: none` at all. With the count inboard from
   the start the slot is the button's alone and the pair is unnecessary: the
   button is clickable at rest, which is the one thing the pair prevented.

   Keyed to rows that HAVE a trailing control, so a row with neither keeps its
   count where it always was. */
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-create) > .ch-tree-item-badge,
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-action) > .ch-tree-item-badge {
    transform: translateX(calc(-1 * var(--spaces-tree-trailing-slot)));
}

/* No hover on touch: the row's only affordance must not be invisible there. */
@media (hover: none) {
    .lp-spaces-tree .ch-tree-item > .ch-tree-item-create,
    .lp-spaces-tree .ch-tree-item > .ch-tree-item-action { opacity: 1; }
}

/* While a folder is being renamed the row is an input, and neither belongs on
   it: the count would sit past the field, and the button would float over it
   and open a menu from a click meant for the text. */
.lp-spaces-tree .ch-tree-item:has(> .lp-spaces-name-row--inplace)
    > .ch-tree-item-badge,
.lp-spaces-tree .ch-tree-item:has(> .lp-spaces-name-row--inplace)
    > .ch-tree-item-create,
.lp-spaces-tree .ch-tree-item:has(> .lp-spaces-name-row--inplace)
    > .ch-tree-item-action {
    display: none;
}

/* The count takes the same 22px box as the button that replaces it, and
   centres in it. Without a width it shrank to its digits and sat flush against
   the row's padding, ~8px right of where the dots centre — so the number read
   as pushed into the corner, and the swap moved it. */
.lp-spaces-tree .ch-tree-item-badge {
    flex: 0 0 auto;
    width: var(--spaces-tree-trailing-slot);
    text-align: center;
    padding: 0;
    border-radius: 0;
    background: transparent;
    color: var(--text-muted);
    /* Smaller than a label, since a count that shouts is a count in the way,
       but the same family as one. tabular-nums is what the monospace face was
       really for — digits of equal width, so the column does not shuffle as
       the numbers change — and the UI face has them. */
    font-size: 10px;
    font-variant-numeric: tabular-nums;
}

/* ── The guide line under a scope ───────────────────────────────────────
   Drawn at the centre of the glyph it descends from — pad + half the glyph —
   which is the whole of why the engine's per-depth row padding is turned off
   at the mount: a container and the row above it share an origin, so this
   holds at every level, but only while a row's own padding is the same at
   every level.

   The padding past the border is what keeps a child row's hover background
   off the line. */
.lp-spaces-tree .ch-tree-items {
    min-width: 0;
    margin-left: calc(var(--spaces-tree-header-pad) + var(--spaces-tree-glyph) / 2);
    padding-left: 4px;
    border-left: 1px solid var(--border);
}

.lp-spaces-tree .ch-tree-item {
    display: flex;
    position: relative;
    min-width: 0;
    /* One height for every row: a chat carries no glyph, and a row that shrank
       to its text would sit shorter than the folder above it. Now the shared
       row box, so Chaos's flat trees above this one match it. It is still
       exactly `glyph + 10px` — the token records that number rather than
       replacing the reasoning for it. */
    min-height: var(--lp-sidebar-row-height, calc(var(--spaces-tree-glyph) + 10px));
    align-items: center;
    gap: var(--spaces-tree-row-gap);
    margin: 1px 0;
    padding: 5px 7px;
    overflow: hidden;
    border-radius: var(--radius-md, 8px);
    color: var(--text-secondary);
    text-transform: none;
}

.lp-spaces-tree .ch-tree-item:hover {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.lp-spaces-tree .ch-tree-item--active {
    background: var(--bg-surface);
    color: var(--accent);
}

.lp-spaces-tree .ch-tree-item-label {
    min-width: 0;
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.lp-spaces-tree .ch-tree-children {
    min-width: 0;
    margin-left: calc(var(--spaces-tree-row-pad) + var(--spaces-tree-glyph) / 2);
    padding-left: 4px;
    border-left: 1px solid var(--border);
}

/* ── What is clickable looks clickable ─────────────────────────────────
   A Space header gets its pointer inline from the engine. Nothing else did:
   the base rule that would have covered these rows lives in
   shell-controls.css, which this app does not load (see the note at the top).

   Gated on there being a chevron, the same way the hover arrow is. An empty
   Space or Project has nothing to fold, so a click does nothing and a pointer
   would promise otherwise. */
.lp-spaces-tree .ch-tree-item--project:has(> .ch-tree-item-chevron),
.lp-spaces-tree .ch-tree-item--folder:has(> .ch-tree-item-chevron) {
    cursor: pointer;
}

/* A content row opens the thing it names, so it is a pointer as well — until
   it is picked up, when grab says what is about to happen instead. */
.lp-spaces-tree .ch-tree-item--item {
    cursor: pointer;
}

/* ── What an empty scope says when opened ──────────────────────────────
   A real row rather than a styled void, because the engine draws a fold
   arrow only for a row that HAS children — so this row is what makes an
   empty Project or folder openable at all.

   It reads as absence: muted, italic, and inert. No hover highlight and no
   pointer, since clicking it does nothing; a drop still lands on it, and
   resolves to the scope holding it. */
.lp-spaces-tree .ch-tree-item--empty {
    min-height: 26px;
    color: var(--text-muted);
    font-style: italic;
    cursor: default;
}

.lp-spaces-tree .ch-tree-item--empty:hover {
    background: transparent;
    color: var(--text-muted);
}

/* Archived rows. shell-controls.css has these, but only the controls-lab loads it. */
.lp-spaces-tree .ch-tree-item--muted,
.lp-spaces-tree .ch-tree-section-header--muted {
    font-style: italic;
    opacity: 0.55;
}

.lp-spaces-tree .ch-tree-item-badge-icon {
    display: inline-grid;
    width: 14px;
    height: 14px;
    flex: 0 0 14px;
    place-items: center;
    opacity: 0.7;
}

/* Sized to match the options button a scope row carries, so a column of rows
   has one column of buttons. The dots themselves are drawn at the foot of this
   file, from the same rule. */
.lp-spaces-tree .ch-tree-item-action {
    /* S-03: the same one value the count's box and its slide use, so the three
       cannot drift apart. The existing pin asserts this width EQUALS the
       badge's; making both the variable keeps that true by construction rather
       than by two people typing 22 twice. */
    width: var(--spaces-tree-trailing-slot);
    height: var(--spaces-tree-trailing-slot);
    flex: 0 0 var(--spaces-tree-trailing-slot);
    margin-left: 0;
    padding: 0;
    border: 0;
    border-radius: 5px;
    background: transparent;
    color: var(--text-muted);
    line-height: 1;
}

.lp-spaces-tree .ch-tree-item-action:hover,
.lp-spaces-tree .ch-tree-item-action[aria-expanded="true"] {
    background: var(--bg-hover);
    color: var(--text-primary);
}

.lp-spaces-tree .ch-tree-item-action:focus-visible {
    outline: 2px solid var(--accent);
    outline-offset: 1px;
}

/* ── Structure-create buttons: three dots, not a plus ───────────────────
   The engine writes the glyph as a TEXT NODE (`'+'`) and offers no label or
   icon option, so the swap happens here rather than in sidebar-tree.js —
   which Chaos's sidebar, its Recent view and both Chaos skins also render
   through. `font-size: 0` collapses that text node; the dots are drawn on
   `::before`, absolutely positioned so the button's grid centring cannot
   push them off-axis.

   `currentColor` is load-bearing: the unnamespaced `:hover` /
   `[aria-expanded="true"]` rules in sidebar-nav.css change `color`, and the
   dots must follow.

   The item-action button — the one a content row opens its own menu from — is
   matched too. It carries the engine's text ellipsis, and a typographic `…`
   next to three drawn circles reads as a different control at a glance: the
   weight, the spacing and the baseline all differ, and the two sit on adjacent
   rows. One glyph for "open this row's menu", whatever kind of row it is.

   The SECTION action without [data-create-action] is still left alone: that is
   an app's own header menu (Chaos routes it that way) and is hover-only, so it
   is a different affordance rather than the same one drawn twice. */
.lp-spaces-tree .ch-tree-section-action[data-create-action],
.lp-spaces-tree .ch-tree-item-create,
.lp-spaces-tree .ch-tree-item-action,
.lp-spaces-tree-header-menu {
    position: relative;
    font-size: 0;
}

.lp-spaces-tree .ch-tree-section-action[data-create-action]::before,
.lp-spaces-tree .ch-tree-item-create::before,
.lp-spaces-tree .ch-tree-item-action::before,
.lp-spaces-tree-header-menu::before {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: var(--spaces-tree-dot-size);
    height: var(--spaces-tree-dot-size);
    transform: translate(-50%, -50%);
    border-radius: 50%;
    background: currentColor;
    box-shadow:
        calc(-1 * var(--spaces-tree-dot-gap)) 0 0 currentColor,
        var(--spaces-tree-dot-gap) 0 0 currentColor;
}


/* ── The folder naming row ──────────────────────────────────────────────
   Sits where the folder itself will: same height, same indent, same icon
   column, so committing the name swaps an input for a row without anything
   moving. No submit button — Enter or clicking away saves, which is what
   makes the button unnecessary rather than merely hidden. */
.lp-spaces-tree .lp-spaces-name-row {
    display: flex;
    min-width: 0;
    min-height: calc(var(--spaces-tree-glyph) + 10px);
    align-items: center;
    gap: var(--spaces-tree-row-gap);
    margin: 1px 0;
    padding: 5px var(--spaces-tree-header-pad) 5px var(--spaces-tree-row-pad);
}

.lp-spaces-tree .lp-spaces-name-icon {
    display: inline-grid;
    width: var(--spaces-tree-glyph);
    height: var(--spaces-tree-glyph);
    flex: 0 0 var(--spaces-tree-glyph);
    place-items: center;
    color: var(--text-muted);
}

.lp-spaces-tree .lp-spaces-name-row input {
    min-width: 0;
    height: 26px;
    flex: 1;
    padding: 0 8px;
    border: 1px solid var(--border);
    border-radius: 6px;
    outline: 0;
    background: var(--bg-surface);
    color: var(--text-primary);
    font: inherit;
}

.lp-spaces-tree .lp-spaces-name-row input::placeholder {
    color: var(--text-muted);
}

/* Focus is the row's whole state — it opens focused and closes when it loses
   focus — so the ring is on always rather than only for :focus-visible. */
.lp-spaces-tree .lp-spaces-name-row input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 35%, transparent);
}

.lp-spaces-tree .lp-spaces-name-row input:disabled {
    opacity: 0.6;
}

/* Renaming: the row already supplies the icon and the indent, so this one
   only replaces the label and must not add either again. */
.lp-spaces-tree .lp-spaces-name-row--inplace {
    min-height: 0;
    flex: 1;
    margin: 0;
    padding: 0;
}

/* ── Where a dragged row would land ────────────────────────────────────
   THE LINE, everywhere, and never an area highlight. One indicator for one
   question — "where does this land" — on owner instruction, twice: first that
   lighting up the scope as well as drawing the line said two things at once,
   then that the places showing no line at all showed nothing whatsoever.

   The engine draws the line itself for the before/after zones of a row. Three
   places it does not, each with its own reason, and all three are covered
   below rather than left blank:

   A FOLDER's middle band is the engine's "inside" zone, which it paints as an
   outline rather than a line. This tree has no separate "into this row"
   gesture to paint — arranging collapses inside to after (scope-reorder), and
   moving a row into a folder is the container drop tree-interactions handles —
   so the zone gets the same line as `after`, which is what it will do.

   A SECTION HEADER gets no drag handlers from the engine at all: it wires
   rows, and a header is not one. tree-interactions marks it instead, and the
   line goes under the header — where the row will appear, as its first child.

   The component's own ring stays gone. Its LOGIC is still live and still
   load-bearing: the same branch calls preventDefault(), which is what permits
   a drop at all. */
.lp-spaces-tree .ch-tree-item--drop-inside {
    outline: none;
    background: transparent;
}

/* THE LINE ITSELF, stated here rather than inherited.
   The before/after rules live in controls/shell-controls.css, which — as the
   note at the top of this file says — reaches nothing in the app: index.html
   loads sidebar-nav.css and this one. Chaos happens to pull shell-controls.css
   in separately (chaos-css.js), so the line drew there and NOWHERE ELSE, which
   is why reordering in Chat showed nothing while the same drag in Chaos showed
   a line. Anything this tree needs from that family has to be restated here,
   and this is the piece that was missed. */
.lp-spaces-tree .ch-tree-item--drop-before::before,
.lp-spaces-tree .ch-tree-item--drop-after::after,
.lp-spaces-tree .ch-tree-item--drop-inside::after,
.lp-spaces-tree .ch-tree-section-header.lp-spaces-tree-drop-into::after {
    content: '';
    position: absolute;
    left: 8px;
    right: 8px;
    height: 2px;
    border-radius: 1px;
    background: var(--accent, #00B4EA);
    pointer-events: none;
}

.lp-spaces-tree .ch-tree-item--drop-before::before {
    top: 0;
}

.lp-spaces-tree .ch-tree-item--drop-after::after,
.lp-spaces-tree .ch-tree-item--drop-inside::after,
.lp-spaces-tree .ch-tree-section-header.lp-spaces-tree-drop-into::after {
    bottom: 0;
}

/* The header is the containing block for its own line, as a row already is. */
.lp-spaces-tree .ch-tree-section-header {
    position: relative;
}

/* The row being carried, so it is clear WHICH row is in flight when several
   look alike. Set on dragstart and cleared on dragend — :active would have
   dimmed the row on a plain mousedown, before any drag began. */
.lp-spaces-tree .lp-spaces-tree-dragging {
    opacity: 0.45;
}

/* A row that cannot be dragged should not look like it can. */
.lp-spaces-tree .ch-tree-item--item[draggable="true"] {
    cursor: grab;
}

@media (prefers-reduced-motion: reduce) {
    .lp-spaces-tree .ch-tree-chevron,
    .lp-spaces-tree .ch-tree-item-chevron,
    .lp-spaces-tree .ch-tree-chevron::before,
    .lp-spaces-tree .ch-tree-item-chevron::before,
    /* The clock still steps aside — it must, or the dots land on it — but
       it arrives rather than slides. */
    .lp-spaces-tree .chat-sb-autodelete {
        transition: none;
    }
}

/* ════════════════════════════════════════════════════
   Design-reference sizing (Chat's Claude-design pass)
   ════════════════════════════════════════════════════
   Kept as its own block, appended last, rather than edited into the rules
   above — every property here narrows or restates one already declared
   above (same selector, later in the file wins), so nothing above is
   removed or rewritten to land these; a diff against this file shows
   exactly what the design pass changed and nothing else. */

/* The design reference's active-row wash — a flat neutral background reads
   as inert; this brings the row back in line with the rest of the app's own
   active-state convention (an accent tint, not a plain surface colour). Only
   `background` is restated — `color: var(--accent)` above already matches. */
.lp-spaces-tree .ch-tree-item--active {
    background: color-mix(in srgb, var(--accent) 12%, var(--bg-surface));
}

/* Only a content row's own active weight — matching .lp-chat-item.active in
   the design reference. Scope/Project/folder rows are covered by the active
   background above and need no extra weight on top of it. */
.lp-spaces-tree .ch-tree-item--item.ch-tree-item--active {
    font-weight: 500;
}

/* ── The header above the tree ──────────────────────────────────────────
   "ALL SPACES" and the ⋯ that opens what applies to the whole tree. Its own
   namespace, like everything else here: the app sidebars have a
   a .lp-sidebar-spaces-heading of their own in the frozen sidebar-nav.css, and
   this must not inherit from it or a change there would move this. */
.lp-spaces-tree-header {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 6px;
    min-height: var(--lp-sidebar-caption-height);
    /* The right edge puts this menu's dots in the column the section and row
       dots sit in (--spaces-tree-header-pad / .ch-tree-item-action's right +
       its box); the left is the caption indent both surfaces share. Read from
       :root rather than from the tree's own tokens — this header is a SIBLING
       of .lp-spaces-tree, not a child, so those never reached it. */
    padding: 0 var(--lp-sidebar-edge-pad) 0 var(--lp-sidebar-caption-indent);
    margin: 0 0 2px;
}

/* The uppercase, the letterspacing and the weight are what set this apart —
   not the size, which is the shared one corrected for case (see the token).
   It was 10px, which opened the tree on a caption smaller than anything it
   introduced; matching the number exactly then read as larger than everything
   below it, which is the same mistake from the other side. */
.lp-spaces-tree-header-label {
    color: var(--text-muted);
    font-size: var(--spaces-tree-caption-label);
    font-weight: var(--lp-sidebar-caption-weight);
    letter-spacing: var(--lp-sidebar-caption-tracking);
    text-transform: uppercase;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Hover-revealed, like the row actions in this tree — but it stays put once
   its menu is open, or the menu would be attached to something invisible. */
/* Always visible, unlike a row's dots. A row has its name to hover; this
   header's only other content is a label, so a hover-revealed button here
   would be a control with nothing to hint at it — and it is the one way to
   reach New Space and the archived switch. */
.lp-spaces-tree-header-menu {
    display: inline-grid;
    /* S-03: the same trailing-slot value as a row's dots — an existing pin
       asserts the two boxes are equal, and one variable keeps that true rather
       than two people typing 22 in two places. */
    width: var(--spaces-tree-trailing-slot);
    height: var(--spaces-tree-trailing-slot);
    padding: 0;
    place-items: center;
    border: none;
    border-radius: 4px;
    background: none;
    color: var(--text-muted);
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
}

.lp-spaces-tree-header-menu:hover,
.lp-spaces-tree-header-menu[aria-expanded="true"] {
    background: var(--bg-hover);
    color: var(--text-primary);
}

/* Anchored to the button rather than the pointer: this menu belongs to the
   header, so it opens in the same place however it was invoked. */
/* Placed in viewport space from the button's own rect, the same way and at the
   same offset as every scope menu in this tree (contextMenu: rect.left,
   rect.bottom + 4, then clamped). It used to be absolute and right-aligned
   inside the header, so it grew LEFTWARDS and stayed over the sidebar while
   the scope menus spill out to the right of it — two menus a row apart opening
   in different directions.
   Fixed rather than absolute for the same reason contextMenu is: the offsets
   are the button's viewport coordinates. It stays a CHILD of the header, which
   the outside-click test depends on — that test asks whether the click was
   inside the header, so a popover moved out of it would dismiss itself on its
   own switch. */
.lp-spaces-tree-header-popover {
    position: fixed;
    z-index: 40;
    min-width: 208px;
    padding: 4px;
    border: 1px solid var(--border);
    border-radius: var(--radius-md, 8px);
    background: var(--bg-panel, var(--bg-surface));
    box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18);
}

.lp-spaces-tree-header-popover[hidden] {
    display: none;
}

.lp-spaces-tree-header-item {
    display: flex;
    align-items: center;
    gap: 8px;
    width: 100%;
    padding: 7px 8px;
    border: none;
    border-radius: 6px;
    background: none;
    color: var(--text-primary);
    font-size: 13px;
    text-align: left;
    cursor: pointer;
}

button.lp-spaces-tree-header-item:hover {
    background: var(--bg-hover);
}

.lp-spaces-tree-header-item > svg {
    flex: 0 0 auto;
    color: var(--text-secondary);
}

/* The switch carries its own label, so the row lets it take the width and
   pushes the control to the end. */
.lp-spaces-tree-header-item--toggle {
    cursor: default;
}

.lp-spaces-tree-header-item--toggle > label {
    flex: 1 1 auto;
    /* row-reverse, so the switch sits after the words. toggle() builds
       slider-then-label, and its label is the control's accessible name — so
       the order is flipped here rather than the label dropped and replaced
       with text of our own, which would leave the switch unnamed. */
    display: flex;
    flex-direction: row-reverse;
    align-items: center;
    justify-content: flex-end;
    gap: 8px;
    margin: 0;
}

.lp-spaces-tree-header-item--toggle > label > .ch-toggle-label {
    flex: 1 1 auto;
}

/* ── A row's own indicator ──────────────────────────────────────────
   The auto-delete clock a chat wears while it is armed. Same class as
   Chat's Recent list uses, and stated here for the same reason as the
   drop line above: its rule lives in css/apps/chat-components.css,
   which the Chat app loads and index.html does not — so a chat row in
   Chaos's tree would get the icon with none of its sizing or colour.

   It rests at the right end of the row — in the slot the "…" button is
   drawn over — and STEPS ASIDE while that button is showing. The button
   is absolutely positioned, so a clock sharing the slot sits underneath
   it and the hovered row hands the pointer to the button instead: the
   remaining time became unreachable exactly when it was wanted.

   Stepping aside rather than reserving the gap permanently, because the
   gap is only real while the dots are there. Held open at all times it
   reads as a clock pushed away from an edge with nothing beside it.

   The count solves the same collision by hiding, which it can afford —
   it has nothing to say on hover. The clock does. */
.lp-spaces-tree .chat-sb-autodelete {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    margin-left: auto;
    margin-right: 0;
    color: var(--text-muted, #8a8f98);
    opacity: 0.7;
    transition: margin-right 120ms ease;
}
.lp-spaces-tree .chat-sb-autodelete svg { width: 13px; height: 13px; }

/* The dots' box plus the row gap, measured from the padding edge both are
   laid out against. Keyed to the same states that reveal the button, so
   the clock moves exactly when something arrives to need the room —
   :focus-within included, or a keyboard user reaching the button would
   land it on top of the clock. */
.lp-spaces-tree .ch-tree-item:hover > .chat-sb-autodelete,
.lp-spaces-tree .ch-tree-item:focus-within > .chat-sb-autodelete {
    margin-right: calc(var(--spaces-tree-trailing-slot) + var(--spaces-tree-row-gap, 6px));
}

/* A row carrying both keeps ONE gap: the count is already in flow at the
   button's width, so the clock must not reserve a second one. */
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-badge):hover > .chat-sb-autodelete,
.lp-spaces-tree .ch-tree-item:has(> .ch-tree-item-badge):focus-within > .chat-sb-autodelete {
    margin-right: var(--spaces-tree-row-gap, 6px);
}

/* Hovering the clock itself only darkens it — by then the row is hovered
   and the clock has already settled, so the tooltip never chases. */
.lp-spaces-tree .chat-sb-autodelete:hover { opacity: 1; }

/* The dots become VISIBLE on row hover, not merely clickable.
   `.ch-tree-item:hover .ch-tree-item-action { opacity: 1 }` lives in
   controls/shell-controls.css, which only Chaos loads — so in Chat the
   button stayed at opacity 0 until the pointer found it, and the clock
   would have stepped aside for something the reader could not see. The
   fourth restatement this file has needed for that reason. */
.lp-spaces-tree .ch-tree-item:hover > .ch-tree-item-create,
.lp-spaces-tree .ch-tree-item:hover > .ch-tree-item-action {
    opacity: 1;
}
